Compare commits
434 Commits
Author | SHA1 | Date | |
---|---|---|---|
d95b1a0a41 | |||
d9cc4980e8 | |||
5e987fa8b6 | |||
42001be9ec | |||
dc198fec8c | |||
acd47d5ec9 | |||
72e3fb5bfe | |||
b2539b843b | |||
be5dff8472 | |||
76037e8b3a | |||
11f4bd503b | |||
6688b279e7 | |||
1ca38015bc | |||
656269ff17 | |||
bd727b825d | |||
e04c1e7dc9 | |||
615df76dd5 | |||
112c6252d6 | |||
b13370bf0d | |||
88aa5d3fdb | |||
b187d8f836 | |||
1763a1a717 | |||
62b61ed980 | |||
c11034b9bc | |||
58e8fe0bd0 | |||
a0c8765588 | |||
9022059dc6 | |||
7f790be1e4 | |||
93791c999d | |||
5e9f1437ad | |||
f9655213b3 | |||
008d608ec4 | |||
78c8d12ad8 | |||
df0ad4d875 | |||
776e0fcd11 | |||
6ec3bad49a | |||
52f44c3ea6 | |||
941d36ebfe | |||
db8243b4b4 | |||
f919b7360e | |||
8e1b7c0036 | |||
9b0e0fa9c2 | |||
565d7afa92 | |||
c914ba946f | |||
6f9280f64a | |||
8fe460e401 | |||
b9fe359d23 | |||
2c6d494c32 | |||
dbd1279226 | |||
b463fcf61b | |||
82b4f5125d | |||
3f89d6d009 | |||
676f843c92 | |||
c2387dc120 | |||
9a8e1534c0 | |||
dbc4964e94 | |||
00b263f345 | |||
62d03b0d41 | |||
b5a4b293a9 | |||
bfcfdb83a7 | |||
4ccbfa8164 | |||
675d10c8a6 | |||
2cde7336dc | |||
169490dbec | |||
3ceb297276 | |||
12633bfed6 | |||
5958bac2a2 | |||
37f2d5b8b0 | |||
47891d2953 | |||
af68571f4e | |||
d0ec925ca3 | |||
939194158a | |||
576265e09c | |||
dfaf45344c | |||
6c378957e9 | |||
e8f9bc80a0 | |||
a30b8b21e4 | |||
12204852aa | |||
edba980b56 | |||
ba666ddbfa | |||
35f9f59c57 | |||
ac1f493338 | |||
1c3c70d460 | |||
e8e7bdf9e0 | |||
d263e0e60c | |||
028d86c0bb | |||
f8b6830013 | |||
49a40c50e8 | |||
2ba48995fe | |||
3cc8ade6d8 | |||
39c9a0a299 | |||
3ad317fb6d | |||
bd46440d12 | |||
f3a28814ae | |||
9f8f64b9ec | |||
1e524a49c0 | |||
467c276fca | |||
f610e39418 | |||
27d977b2fa | |||
b36e72bfcc | |||
e49701228d | |||
48f8b33d7d | |||
d87ace8c89 | |||
b1326d4145 | |||
7c2862c958 | |||
0a4f5ad64d | |||
c617a11c55 | |||
053167965a | |||
a7ac45b937 | |||
5482bbf4bd | |||
0a58e106b5 | |||
a1395a5808 | |||
a0d50ef03a | |||
685e2c8b6d | |||
c6d9a20fe5 | |||
4a952d867b | |||
cb4cf43fcf | |||
1bce7a832b | |||
574234f70f | |||
42e5470dd0 | |||
8199365324 | |||
86c92eb31e | |||
d9fd952c03 | |||
967c7ded8d | |||
a4bf847b56 | |||
d6917155e8 | |||
3f024c1ef4 | |||
96d253f0f9 | |||
9b166fb9a9 | |||
b8ae8cd452 | |||
ca82b227b9 | |||
862496495f | |||
8bb9a8c5d1 | |||
00cb66484b | |||
cabe2ae18d | |||
665a3f3180 | |||
3b5d7eaab6 | |||
aa2358aa03 | |||
a7decc1948 | |||
38b48604f3 | |||
60856cb7b9 | |||
350d013043 | |||
70c92fea15 | |||
6211b126a9 | |||
54c3fcc72a | |||
27c9088ddb | |||
b8c2d42cad | |||
1f5ddd9530 | |||
2896ce0dad | |||
29bcde145c | |||
11db429bcc | |||
75aea9f885 | |||
c80559005f | |||
9b927cfcc2 | |||
4db7931aa0 | |||
1e67329c64 | |||
6d17e4d538 | |||
350f58ec9d | |||
de9478a992 | |||
70a2c985cf | |||
78037dc9ec | |||
9b11efd1e5 | |||
3c2ee8fbb3 | |||
163cc3f795 | |||
041382b02f | |||
837bfc3aa5 | |||
5ba1176f14 | |||
f08649b02d | |||
edbe5a254b | |||
cfedb30628 | |||
aa18b88a61 | |||
05962e71e3 | |||
bafc3d0082 | |||
308f1b44c3 | |||
cd17789529 | |||
bf988d89c4 | |||
b1e842ae47 | |||
fcc3c35ae2 | |||
e2524e43cf | |||
6aac2d62be | |||
95e2636f23 | |||
7565492bb9 | |||
89f7f12f92 | |||
cdd15ca818 | |||
11f2d88b16 | |||
8066d540e0 | |||
c3091a7346 | |||
9cadebcd50 | |||
3e54eb7520 | |||
068f19c895 | |||
3651cce542 | |||
9e0b9d9dda | |||
f194e2a1be | |||
f56c6f2836 | |||
ec896461a7 | |||
8eeed821d3 | |||
80c1689b24 | |||
7c29b566be | |||
920d595c12 | |||
5d7174b2a7 | |||
3c60f47e3f | |||
c4abc59673 | |||
ff4cdd82ee | |||
1c6e9caa40 | |||
07ec04ddc6 | |||
d6b3f5af81 | |||
ce1fe9321c | |||
2c88e4e3ba | |||
fed37c9dc0 | |||
e14eefdc31 | |||
2525d369d4 | |||
0600481a67 | |||
f0324e4755 | |||
00f0f957c0 | |||
9c652d784d | |||
eb2fa74661 | |||
146c599deb | |||
574c4033ab | |||
9f122eec18 | |||
eb0f6a04d8 | |||
c7230befe4 | |||
9a316ae1a9 | |||
df4364714e | |||
a1cd2b39eb | |||
8a7a15a361 | |||
2cdeecb6e0 | |||
638e4a5ac1 | |||
93b4ef5f17 | |||
26d490f74a | |||
01a1190524 | |||
2073090628 | |||
6d00cb208d | |||
13b9bf687d | |||
56f7da34d7 | |||
0f34440b64 | |||
bbcc7cca4e | |||
0453afcb0e | |||
cafecd1e19 | |||
4b968a9474 | |||
9244945e69 | |||
78819c1733 | |||
394e18f76e | |||
40eb950e94 | |||
90636a5329 | |||
2fc6d4cd21 | |||
b20bdf3c4e | |||
a20726a301 | |||
39727a1c9f | |||
168f46a436 | |||
4ec07a6dc7 | |||
798a6295ee | |||
73cb8da8c1 | |||
3167ce9785 | |||
63b7b71b49 | |||
9965af9ccd | |||
ba5d2c925a | |||
867be09e29 | |||
8362a92898 | |||
162482dbc4 | |||
c0f14db5bb | |||
3c561914c6 | |||
34c6f1bf4d | |||
2187898494 | |||
d4bc6ae7a1 | |||
81cdb15353 | |||
5cfa9d4bc5 | |||
92da453233 | |||
2aedfedbd3 | |||
64c9b11c09 | |||
5f87c87751 | |||
70f9022aae | |||
43683fe554 | |||
002de6f5e4 | |||
63a388236e | |||
9560791002 | |||
ed93b5a2c5 | |||
21e4cc9f4d | |||
73e628cbdf | |||
7ebd0bf5d4 | |||
3f1902e0fe | |||
3e546eaa21 | |||
e70deb155d | |||
4f289c790b | |||
c15f525167 | |||
bcbf642122 | |||
1559727f2d | |||
a596793a9a | |||
862bf3f619 | |||
83a86c06a4 | |||
06a69c09a0 | |||
6a24e88d9a | |||
be6a5b859e | |||
2444191bf2 | |||
9226b1eb4a | |||
3f53629175 | |||
baef329a4d | |||
b103fc7bfb | |||
a3385bd2ac | |||
7cb50c654b | |||
52ba538037 | |||
0a0460529f | |||
fc0a884d5f | |||
dbbea29e75 | |||
bcd0aae8c6 | |||
50d3e6aff2 | |||
1d45926122 | |||
45626c947c | |||
d7ca6d4327 | |||
6411bd56e9 | |||
1486e7b8f6 | |||
e73f2bcb2b | |||
a412569ff7 | |||
387194d651 | |||
13d0359041 | |||
365d927efb | |||
aa76379d6c | |||
ef72de89b3 | |||
e2962a4b8d | |||
a563986ce4 | |||
a4e294634a | |||
067122f8f4 | |||
6a1a744eff | |||
d3cf273a45 | |||
52e267468a | |||
e80b58899d | |||
25f652c1eb | |||
0dde4c9f80 | |||
a512b82196 | |||
7f119e95e1 | |||
bf957d2509 | |||
1e314b505f | |||
ef9a80ebfd | |||
84d0afae4c | |||
48a53ad436 | |||
2f4aee49e2 | |||
f3f5b9eeb3 | |||
541d5420bb | |||
0383e2a1e1 | |||
084604cf3c | |||
8ab9c06778 | |||
0afd4c94b4 | |||
aaae4d5a1f | |||
9da713ab06 | |||
540d56f0b8 | |||
160eaa6901 | |||
180c8228e1 | |||
c182055dbe | |||
97a289f02a | |||
089c0b2b1b | |||
3b109ec578 | |||
45158c64a2 | |||
fe7b075f7b | |||
a350940cd1 | |||
efec79d8de | |||
62950128a2 | |||
aca69ce03c | |||
bf8ce354e5 | |||
8d09d865b1 | |||
8848146e8d | |||
480078da42 | |||
46a5355377 | |||
3fb226ec11 | |||
7471984ecf | |||
df9b345541 | |||
d75c641848 | |||
a484a6e24d | |||
a7fd74396c | |||
8977c5ddbf | |||
bbb63b4aa6 | |||
f492e9ec1b | |||
03e2991ced | |||
80136834b5 | |||
3b2f71cc8a | |||
572c44b78b | |||
783acf6d8c | |||
fc22f76708 | |||
6ec5d26f0c | |||
53ce9cac4c | |||
51800d5e4b | |||
627b36d2a5 | |||
70ccd137e1 | |||
9a04371680 | |||
f799ff3af1 | |||
6db1d0b031 | |||
d599aaa410 | |||
09bc79ef84 | |||
6e39a1e666 | |||
bb51b59400 | |||
75de814b15 | |||
b4c348aef7 | |||
3476d3bcf3 | |||
45c442000e | |||
a07db5625b | |||
3df50f15f7 | |||
4b7a55c89f | |||
3ff5cdf43f | |||
dbfd6b1e17 | |||
4b9a05198c | |||
d62f57eab3 | |||
96c3ab7f9d | |||
38ebeab163 | |||
fcfaa21a24 | |||
0f0d11a828 | |||
6b2da2fe6b | |||
74525e8e8e | |||
cbcc8af3bd | |||
ab7b0c442c | |||
39c022dbf3 | |||
b00917aa60 | |||
4a0328cd8f | |||
b96af3a1f1 | |||
28e12a973f | |||
1883f3c0e7 | |||
c6747439fb | |||
0b9c814ea5 | |||
f09f5eb0f1 | |||
95ac5245e1 | |||
dd607e5c07 | |||
eb55d5465f | |||
500b212a25 | |||
7554a86d23 | |||
43d4dbbfbd | |||
90e49613f9 | |||
d3ff79d800 | |||
bfcbd1b065 | |||
b488c349e8 | |||
590f43297f | |||
b8720566d7 | |||
d0d3e06c81 | |||
48f38a8625 | |||
13f34fc600 | |||
f679392c61 | |||
f78e9d47d1 | |||
92a3c1a5a0 |
2
.bowerrc
@ -1,3 +1,3 @@
|
||||
{
|
||||
"directory": "lemur/static/app/vendor/bower_components"
|
||||
"directory": "bower_components"
|
||||
}
|
||||
|
3
.gitattributes
vendored
@ -1 +1,2 @@
|
||||
* text=auto
|
||||
* text=auto
|
||||
version.py export-subst
|
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/.cache
|
||||
.coverage
|
||||
.tox
|
||||
.DS_Store
|
||||
|
@ -8,7 +8,7 @@
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"latedef": false,
|
||||
"newcap": false,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
@ -22,6 +22,8 @@
|
||||
"angular": false,
|
||||
"moment": false,
|
||||
"toaster": false,
|
||||
"d3": false,
|
||||
"self": false,
|
||||
"_": false
|
||||
}
|
||||
}
|
||||
|
11
.travis.yml
@ -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:
|
||||
@ -23,9 +21,6 @@ env:
|
||||
global:
|
||||
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
|
||||
|
||||
install:
|
||||
- make dev-postgres
|
||||
|
||||
before_script:
|
||||
- psql -c "create database lemur;" -U postgres
|
||||
- psql -c "create user lemur with password 'lemur;'" -U postgres
|
||||
@ -36,4 +31,4 @@ script:
|
||||
|
||||
notifications:
|
||||
email:
|
||||
kglisson@netflix.com
|
||||
kglisson@netflix.com
|
||||
|
4
AUTHORS
@ -1 +1,3 @@
|
||||
- Kevin Glisson (kglisson@netflix.com)
|
||||
- Kevin Glisson <kglisson@netflix.com>
|
||||
- Jeremy Heffner <jheffner@netflix.com>
|
||||
|
||||
|
109
CHANGELOG.rst
Normal file
@ -0,0 +1,109 @@
|
||||
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
|
||||
private key material (default is True)
|
||||
* Closed `#231 <https://github.com/Netflix/lemur/issues/231>`_ - Authorities were not respecting 'owning' roles and their
|
||||
users
|
||||
* Closed `#228 <https://github.com/Netflix/lemur/issues/228>`_ - Fixed documentation with correct filter values
|
||||
* Closed `#226 <https://github.com/Netflix/lemur/issues/226>`_ - Fixes issue were `import_certificate` was requiring
|
||||
replacement certificates to be specified
|
||||
* Closed `#224 <https://github.com/Netflix/lemur/issues/224>`_ - Fixed an issue where NPM might not be globally available (thanks AlexClineBB!)
|
||||
* Closed `#221 <https://github.com/Netflix/lemur/issues/234>`_ - Fixes several reported issues where older migration scripts were
|
||||
missing tables, this change removes pre 0.2 migration scripts
|
||||
* Closed `#218 <https://github.com/Netflix/lemur/issues/234>`_ - Fixed an issue where export passphrases would not validate
|
||||
|
||||
|
||||
0.2.1 - 2015-12-14
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Fixed bug with search not refreshing values
|
||||
* Cleaned up documentation, including working supervisor example (thanks rpicard!)
|
||||
* Closed #165 - Fixed an issue with email templates
|
||||
* Closed #188 - Added ability to submit third party CSR
|
||||
* Closed #176 - Java-export should allow user to specify truststore/keystore
|
||||
* Closed #176 - Extended support for exporting certificate in P12 format
|
||||
|
||||
|
||||
0.2.0 - 2015-12-02
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Closed #120 - Error messages not displaying long enough
|
||||
* Closed #121 - Certificate create form should not be valid until a Certificate Authority object is available
|
||||
* Closed #122 - Certificate API should allow for the specification of preceding certificates
|
||||
You can now target a certificate(s) for replacement. When specified the replaced certificate will be marked as
|
||||
'inactive'. This means that there will be no notifications for that certificate.
|
||||
* Closed #139 - SubCA autogenerated descriptions for their certs are incorrect
|
||||
* Closed #140 - Permalink does not change with filtering
|
||||
* Closed #144 - Should be able to search certificates by domains covered, included wildcards
|
||||
* Closed #165 - Cleaned up expiration notification template
|
||||
* Closed #160 - Cleaned up quickstart documentation (thanks forkd!)
|
||||
* Closed #144 - Now able to search by all domains in a given certificate, not just by common name
|
||||
|
||||
|
||||
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.
|
||||
see: `Upgrading Lemur <https://lemur.readthedocs.com/adminstration#UpgradingLemur>`_
|
1
LICENSE
@ -1,4 +1,3 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include setup.py version.py package.json bower.json gulpfile.js README.rst MANIFEST.in LICENSE AUTHORS
|
||||
recursive-include lemur/plugins/lemur_email/templates *
|
||||
recursive-include lemur/static *
|
||||
global-exclude *~
|
4
Makefile
@ -6,11 +6,11 @@ develop: update-submodules setup-git
|
||||
npm install
|
||||
pip install "setuptools>=0.9.8"
|
||||
# order matters here, base package must install first
|
||||
# this is temporary until the version we need is released
|
||||
pip install -e 'git+https://git@github.com/pyca/cryptography.git#egg=cryptography-1.0.dev1'
|
||||
pip install -e .
|
||||
pip install "file://`pwd`#egg=lemur[dev]"
|
||||
pip install "file://`pwd`#egg=lemur[tests]"
|
||||
node_modules/.bin/gulp build
|
||||
node_modules/.bin/gulp package
|
||||
@echo ""
|
||||
|
||||
dev-docs:
|
||||
|
1
OSSMETADATA
Normal file
@ -0,0 +1 @@
|
||||
osslifecycle=active
|
27
README.rst
@ -1,31 +1,32 @@
|
||||
Lemur
|
||||
=====
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/lemur.svg
|
||||
:target: https://pypi.python.org/pypi/lemur/
|
||||
:alt: Latest Version
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
: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://readthedocs.org/projects/lemur/badge/?version=latest
|
||||
:target: https://lemur.readthedocs.org
|
||||
:alt: Latest Docs
|
||||
|
||||
.. image:: https://magnum.travis-ci.com/Netflix/lemur.svg?branch=master
|
||||
:target: https://magnum.travis-ci.com/Netflix/lemur
|
||||
.. image:: https://travis-ci.org/Netflix/lemur.svg
|
||||
:target: https://travis-ci.org/Netflix/lemur
|
||||
|
||||
.. image:: https://requires.io/github/Netflix/lemur/requirements.svg?branch=master
|
||||
:target: https://requires.io/github/Netflix/lemur/requirements/?branch=master
|
||||
:alt: Requirements Status
|
||||
|
||||
Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults.
|
||||
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.
|
||||
|
||||
It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on OS X.
|
||||
|
||||
It works on CPython 2.7. It is known
|
||||
to work on Ubuntu Linux and OS X.
|
||||
|
||||
Project resources
|
||||
=================
|
||||
|
||||
- `Lemur Blog Post <http://techblog.netflix.com/2015/09/introducing-lemur.html>`_
|
||||
- `Documentation <http://lemur.readthedocs.org/>`_
|
||||
- `Source code <https://github.com/netflix/lemur>`_
|
||||
- `Issue tracker <https://github.com/netflix/lemur/issues>`_
|
||||
|
||||
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
: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
|
||||
- `Docker <https://github.com/Netflix/lemur-docker>`_
|
||||
|
62
bower.json
@ -6,38 +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-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",
|
||||
"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-chart.js": "~0.7.1",
|
||||
"satellizer": "~0.9.4",
|
||||
"angularjs-toaster": "~0.4.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "~1.3",
|
||||
"angular-scenario": "~1.3"
|
||||
"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": [
|
||||
"**/.*",
|
||||
|
@ -2,7 +2,7 @@ Configuration
|
||||
=============
|
||||
|
||||
.. warning::
|
||||
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configruation
|
||||
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configuration
|
||||
file. It is highly advised that you do not store your secrets in this file! Lemur provides functions
|
||||
that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management <CredentialManagement>`
|
||||
for more information.
|
||||
@ -24,7 +24,6 @@ Basic Configuration
|
||||
|
||||
LOG_FILE = "/logs/lemur/lemur-test.log"
|
||||
|
||||
|
||||
.. data:: debug
|
||||
:noindex:
|
||||
|
||||
@ -34,7 +33,6 @@ Basic Configuration
|
||||
|
||||
debug = False
|
||||
|
||||
|
||||
.. warning::
|
||||
This should never be used in a production environment as it exposes Lemur to
|
||||
remote code execution through the debug console.
|
||||
@ -72,9 +70,7 @@ Basic Configuration
|
||||
.. data:: LEMUR_TOKEN_SECRET
|
||||
:noindex:
|
||||
|
||||
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and be kept private.
|
||||
|
||||
See `SECRET_KEY` for methods on secure secret generation.
|
||||
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and kept private.
|
||||
|
||||
::
|
||||
|
||||
@ -89,31 +85,85 @@ Basic Configuration
|
||||
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))
|
||||
|
||||
|
||||
.. data:: LEMUR_ENCRYPTION_KEY
|
||||
.. data:: LEMUR_ENCRYPTION_KEYS
|
||||
:noindex:
|
||||
|
||||
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
|
||||
to start.
|
||||
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
|
||||
to start. Multiple keys can be provided to facilitate key rotation. The first key in the list is used for
|
||||
encryption and all keys are tried for decryption until one works. Each key must be 32 URL safe base-64 encoded bytes.
|
||||
|
||||
See `LEMUR_TOKEN_SECRET` for methods of secure secret generation.
|
||||
Running lemur create_config will securely generate a key for your configuration file.
|
||||
If you would like to generate your own, we recommend the following method:
|
||||
|
||||
>>> import os
|
||||
>>> import base64
|
||||
>>> base64.urlsafe_b64encode(os.urandom(32))
|
||||
|
||||
::
|
||||
|
||||
LEMUR_ENCRYPTION_KEY = 'supersupersecret'
|
||||
LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']
|
||||
|
||||
|
||||
Certificate Default Options
|
||||
---------------------------
|
||||
|
||||
Lemur allows you to fine tune your certificates to your organization. The following defaults are presented in the UI
|
||||
and are used when Lemur creates the CSR for your certificates.
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_COUNTRY
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_COUNTRY = "US"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_STATE
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_STATE = "California"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_LOCATION
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_LOCATION = "Los Gatos"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ORGANIZATION = "Netflix"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION_UNIT
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations"
|
||||
|
||||
|
||||
Notification Options
|
||||
--------------------
|
||||
|
||||
Lemur currently has very basic support for notifications. Notifications are sent to the certificate creator, owner and
|
||||
security team as specified by the `SECURITY_TEAM_EMAIL` configuration parameter.
|
||||
Lemur currently has very basic support for notifications. Currently only expiration notifications are supported. Actual notification
|
||||
is handled by the notification plugins that you have configured. Lemur ships with the 'Email' notification that allows expiration emails
|
||||
to be sent to subscribers.
|
||||
|
||||
The template for all of these notifications lives under lemur/template/event.html and can be easily modified to fit your
|
||||
needs.
|
||||
Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs.
|
||||
Notifications are sent to the certificate creator, owner and security team as specified by the `LEMUR_SECURITY_TEAM_EMAIL` configuration parameter.
|
||||
|
||||
Certificates marked as in-active will **not** be notified of upcoming expiration. This enables a user to essentially
|
||||
silence the expiration. If a certificate is active and is expiring the above will be notified at 30, 15, 5, 2 days
|
||||
respectively.
|
||||
Certificates marked as inactive will **not** be notified of upcoming expiration. This enables a user to essentially
|
||||
silence the expiration. If a certificate is active and is expiring the above will be notified according to the `LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS` or
|
||||
30, 15, 2 days before expiration if no intervals are set.
|
||||
|
||||
Lemur supports sending certification expiration notifications through SES and SMTP.
|
||||
|
||||
@ -121,124 +171,189 @@ Lemur supports sending certification expiration notifications through SES and SM
|
||||
.. data:: LEMUR_EMAIL_SENDER
|
||||
:noindex:
|
||||
|
||||
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
|
||||
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
|
||||
|
||||
.. note::
|
||||
If using STMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
|
||||
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
|
||||
.. note::
|
||||
If using SMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
|
||||
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
|
||||
|
||||
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
|
||||
you can send any mail. See: `Verifying Email Address in Amazon SES <http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html>`_
|
||||
|
||||
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
|
||||
you can send any mail. See: `Verifying Email Address in Amazon SES <http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html>`_
|
||||
|
||||
.. data:: LEMUR_MAIL
|
||||
:noindex:
|
||||
|
||||
Lemur sender's email
|
||||
Lemur sender's email
|
||||
|
||||
::
|
||||
|
||||
LEMUR_MAIL = 'lemur.example.com'
|
||||
LEMUR_MAIL = 'lemur.example.com'
|
||||
|
||||
|
||||
.. data:: LEMUR_SECURITY_TEAM_EMAIL
|
||||
:noindex:
|
||||
|
||||
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
|
||||
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
|
||||
|
||||
::
|
||||
|
||||
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
|
||||
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
|
||||
|
||||
|
||||
Authority Options
|
||||
-----------------
|
||||
|
||||
Authorities will each have their own configuration options. There are currently two plugins bundled with Lemur,
|
||||
Verisign/Symantec and CloudCA
|
||||
|
||||
.. data:: VERISIGN_URL
|
||||
.. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS
|
||||
:noindex:
|
||||
|
||||
This is the url for the verisign API
|
||||
Lemur notification intervals
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
|
||||
|
||||
.. data:: VERISIGN_PEM_PATH
|
||||
Authentication Options
|
||||
----------------------
|
||||
Lemur currently supports Basic Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
|
||||
If you are not using an authentication provider you do not need to configure any of these options.
|
||||
|
||||
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
|
||||
|
||||
.. data:: ACTIVE_PROVIDERS
|
||||
:noindex:
|
||||
|
||||
This is the path to the mutual SSL certificate used for communicating with Verisign
|
||||
::
|
||||
|
||||
|
||||
.. data:: CLOUDCA_URL
|
||||
:noindex:
|
||||
|
||||
This is the URL for CLoudCA API
|
||||
|
||||
|
||||
.. data:: CLOUDCA_PEM_PATH
|
||||
:noindex:
|
||||
|
||||
This is the path to the mutual SSL Certificate use for communicating with CLOUDCA
|
||||
|
||||
.. data:: CLOUDCA_BUNDLE
|
||||
:noindex:
|
||||
|
||||
This is the path to the CLOUDCA certificate bundle
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
Lemur currently supports Basic Authentication and Ping OAuth2, additional flows can be added relatively easily
|
||||
If you are not using PING you do not need to configure any of these options
|
||||
ACTIVE_PROVIDERS = ["ping", "google"]
|
||||
|
||||
.. data:: PING_SECRET
|
||||
:noindex:
|
||||
|
||||
::
|
||||
::
|
||||
|
||||
PING_SECRET = 'somethingsecret'
|
||||
PING_SECRET = 'somethingsecret'
|
||||
|
||||
.. data:: PING_ACCESS_TOKEN_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
::
|
||||
|
||||
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
|
||||
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
|
||||
|
||||
|
||||
.. data:: PING_USER_API_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
::
|
||||
|
||||
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
|
||||
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
|
||||
|
||||
.. data:: PING_JWKS_URL
|
||||
:noindex:
|
||||
|
||||
::
|
||||
::
|
||||
|
||||
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
|
||||
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
|
||||
|
||||
.. data:: PING_NAME
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_NAME = "Example Oauth2 Provider"
|
||||
|
||||
.. data:: PING_CLIENT_ID
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_CLIENT_ID = "client-id"
|
||||
|
||||
.. data:: GOOGLE_CLIENT_ID
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
GOOGLE_CLIENT_ID = "client-id"
|
||||
|
||||
.. data:: GOOGLE_SECRET
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
GOOGLE_SECRET = "somethingsecret"
|
||||
|
||||
|
||||
Plugin Specific Options
|
||||
-----------------------
|
||||
|
||||
Verisign Issuer Plugin
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur,
|
||||
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
|
||||
for those plugins.
|
||||
|
||||
.. data:: VERISIGN_URL
|
||||
:noindex:
|
||||
|
||||
This is the url for the Verisign API
|
||||
|
||||
|
||||
.. data:: VERISIGN_PEM_PATH
|
||||
:noindex:
|
||||
|
||||
This is the path to the mutual TLS certificate used for communicating with Verisign
|
||||
|
||||
|
||||
.. data:: VERISIGN_FIRST_NAME
|
||||
:noindex:
|
||||
|
||||
This is the first name to be used when requesting the certificate
|
||||
|
||||
|
||||
.. data:: VERISIGN_LAST_NAME
|
||||
:noindex:
|
||||
|
||||
This is the last name to be used when requesting the certificate
|
||||
|
||||
.. data:: VERISIGN_EMAIL
|
||||
:noindex:
|
||||
|
||||
This is the email to be used when requesting the certificate
|
||||
|
||||
|
||||
.. data:: VERISIGN_INTERMEDIATE
|
||||
:noindex:
|
||||
|
||||
This is the intermediate to be used for your CA chain
|
||||
|
||||
|
||||
.. data:: VERISIGN_ROOT
|
||||
:noindex:
|
||||
|
||||
This is the root to be used for your CA chain
|
||||
|
||||
|
||||
|
||||
AWS Plugin Configuration
|
||||
========================
|
||||
AWS Source/Destination Plugin
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In order for Lemur to manage it's own account and other accounts we must ensure it has the correct AWS permissions.
|
||||
In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions.
|
||||
|
||||
.. note:: AWS usage is completely optional. Lemur can upload, find and manage SSL certificates in AWS. But is not required to do so.
|
||||
.. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so.
|
||||
|
||||
Setting up IAM roles
|
||||
--------------------
|
||||
""""""""""""""""""""
|
||||
|
||||
Lemur's aws plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
|
||||
Lemur's AWS plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
|
||||
|
||||
In order to limit the permissions we will create a new two IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
|
||||
In order to limit the permissions, we will create two new IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
|
||||
|
||||
Lemur uses to STS to talk to different accounts. For managing one account this isn't necessary but we will still use it so that we can easily add new accounts.
|
||||
|
||||
LemurInstanceProfile is the IAM role you will launch your instance with. It actually has almost no rights. In fact it should really only be able to use STS to assume role to the Lemur role.
|
||||
|
||||
Here is are example polices for the LemurInstanceProfile:
|
||||
Here are example policies for the LemurInstanceProfile:
|
||||
|
||||
SES-SendEmail
|
||||
|
||||
@ -276,7 +391,12 @@ STS-AssumeRole
|
||||
|
||||
|
||||
|
||||
Next we will create the the Lemur IAM role. Lemur
|
||||
Next we will create the the Lemur IAM role.
|
||||
|
||||
.. note::
|
||||
|
||||
The default IAM role that Lemur assumes into is called `Lemur`, if you need to change this ensure you set `LEMUR_INSTANCE_PROFILE` to your role name in the configuration.
|
||||
|
||||
|
||||
Here is an example policy for Lemur:
|
||||
|
||||
@ -328,7 +448,8 @@ IAM-ServerCertificate
|
||||
|
||||
|
||||
Setting up STS access
|
||||
---------------------
|
||||
"""""""""""""""""""""
|
||||
|
||||
Once we have setup our accounts we need to ensure that we create a trust relationship so that LemurInstanceProfile can assume the Lemur role.
|
||||
|
||||
In the AWS console select the Lemur IAM role and select the Trust Relationships tab and click Edit Trust Relationship
|
||||
@ -355,7 +476,7 @@ Below is an example policy:
|
||||
|
||||
|
||||
Adding N+1 accounts
|
||||
-------------------
|
||||
"""""""""""""""""""
|
||||
|
||||
To add another account we go to the new account and create a new Lemur IAM role with the same policy as above.
|
||||
|
||||
@ -383,7 +504,7 @@ An example policy:
|
||||
}
|
||||
|
||||
Setting up SES
|
||||
--------------
|
||||
""""""""""""""
|
||||
|
||||
Lemur has built in support for sending it's certificate notifications via Amazon's simple email service (SES). To force
|
||||
Lemur to use SES ensure you are the running as the IAM role defined above and that you have followed the steps outlined
|
||||
@ -393,23 +514,11 @@ The configuration::
|
||||
|
||||
LEMUR_MAIL = 'lemur.example.com'
|
||||
|
||||
Will be sender of all notifications, so ensure that it is verified with AWS.
|
||||
Will be the sender of all notifications, so ensure that it is verified with AWS.
|
||||
|
||||
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
|
||||
settings.
|
||||
|
||||
Upgrading Lemur
|
||||
===============
|
||||
|
||||
Lemur provides an easy way to upgrade between versions. Simply download the newest
|
||||
version of Lemur from pypi and then apply any schema cahnges with the following command.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur db upgrade
|
||||
|
||||
.. note:: Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
|
||||
|
||||
.. _CommandLineInterface:
|
||||
|
||||
Command Line Interface
|
||||
@ -475,69 +584,144 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
|
||||
lemur db upgrade
|
||||
|
||||
|
||||
.. data:: create_user
|
||||
|
||||
Creates new users within Lemur.
|
||||
|
||||
::
|
||||
|
||||
lemur create_user -u jim -e jim@example.com
|
||||
|
||||
|
||||
.. data:: create_role
|
||||
|
||||
Creates new roles within Lemur.
|
||||
|
||||
::
|
||||
|
||||
lemur create_role -n example -d "a new role"
|
||||
|
||||
|
||||
.. data:: check_revoked
|
||||
|
||||
Traverses every certificate that Lemur is aware of and attempts to understand it's validity.
|
||||
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 it's 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
|
||||
------------
|
||||
|
||||
Lemur includes several sub-commands for interacting with Lemur such as creating new users, creating new roles and even
|
||||
issuing certificates.
|
||||
|
||||
The best way to discover these commands is by using the built in help pages
|
||||
|
||||
::
|
||||
|
||||
lemur --help
|
||||
|
||||
|
||||
and to get help on sub-commands
|
||||
|
||||
::
|
||||
|
||||
lemur certificates --help
|
||||
|
||||
|
||||
|
||||
Upgrading Lemur
|
||||
===============
|
||||
|
||||
To upgrade Lemur to the newest release you will need to ensure you have the lastest code and have run any needed
|
||||
database migrations.
|
||||
|
||||
To get the latest code from github run
|
||||
|
||||
::
|
||||
|
||||
cd <lemur-source-directory>
|
||||
git pull -t <version>
|
||||
python setup.py develop
|
||||
|
||||
|
||||
.. note::
|
||||
It's important to grab the latest release by specifying the release tag. This tags denote stable versions of Lemur.
|
||||
If you want to try the bleeding edge version of Lemur you can by using the master branch.
|
||||
|
||||
|
||||
After you have the latest version of the Lemur code base you must run any needed database migrations. To run migrations
|
||||
|
||||
::
|
||||
|
||||
cd <lemur-source-directory>/lemur
|
||||
lemur db upgrade
|
||||
|
||||
|
||||
This will ensure that any needed tables or columns are created or destroyed.
|
||||
|
||||
.. note::
|
||||
Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
|
||||
|
||||
.. note::
|
||||
By default Alembic looks for the `migrations` folder in the current working directory.The migrations folder is
|
||||
located under `<LEMUR_HOME>/lemur/migrations` if you are running the lemur command from any location besides
|
||||
`<LEMUR_HOME>/lemur` you will need to pass the `-d` flag to specify the absolute file path to the `migrations` folder.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
||||
There are several interfaces currently available to extend Lemur. These are a work in
|
||||
progress and the API is not frozen.
|
||||
|
||||
Bundled Plugins
|
||||
---------------
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
|
||||
The following extensions are available and maintained by members of the Lemur community:
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
|
||||
|
||||
|
||||
Identity and Access Management
|
||||
==============================
|
||||
|
||||
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
|
||||
user is first created in Lemur the can be assigned one or more roles. These roles are typically dynamically created
|
||||
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
|
||||
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
|
||||
meaning.
|
||||
|
||||
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
|
||||
as ViewPrivateKeyPermission are compositions of these three main Permissions.
|
||||
|
||||
Lets take a look at how these permissions used:
|
||||
Lets take a look at how these permissions are used:
|
||||
|
||||
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
|
||||
that the `Authority` is associated with it Lemur allows that user to user/view/update that `Authority`.
|
||||
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
|
||||
|
||||
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
|
||||
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
|
||||
private key.
|
||||
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
|
||||
private key information.
|
||||
|
||||
These permissions are applied to the user upon login and refreshed on every request.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
|
||||
|
@ -1,2 +1 @@
|
||||
Change Log
|
||||
==========
|
||||
.. include:: ../CHANGELOG.rst
|
23
docs/conf.py
@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# security_monkey documentation build configuration file, created by
|
||||
# lemur documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Jun 7 18:43:48 2014.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
@ -11,7 +11,6 @@
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@ -54,10 +53,12 @@ copyright = u'2015, Netflix Inc.'
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1.1'
|
||||
base_dir = os.path.join(os.path.dirname(__file__), os.pardir)
|
||||
about = {}
|
||||
with open(os.path.join(base_dir, "lemur", "__about__.py")) as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
version = release = about["__version__"]
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -100,9 +101,13 @@ pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
@ -144,6 +144,17 @@ If you've made changes and need to compile them by hand for any reason, you can
|
||||
|
||||
The minified and processed files should be committed alongside the unprocessed changes.
|
||||
|
||||
It's also important to note that Lemur's frontend and API are not tied together. The API does not serve any of the static assets, we rely on nginx or some other file server to server all of the static assets.
|
||||
During development that means we need an additional server to serve those static files for the GUI.
|
||||
|
||||
This is accomplished with a Gulp task:
|
||||
|
||||
::
|
||||
|
||||
./node_modules/.bin/gulp serve
|
||||
|
||||
The gulp task compiles all the JS/CSS/HTML files and opens the Lemur welcome page in your default browsers. Additionally any changes to made to the JS/CSS/HTML with be reloaded in your browsers.
|
||||
|
||||
Developing with Flask
|
||||
----------------------
|
||||
|
||||
@ -180,19 +191,92 @@ You can see a list of open pull requests (pending changes) by visiting https://g
|
||||
|
||||
Pull requests should be against **master** and pass all TravisCI checks
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
||||
Writing a Plugin
|
||||
================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
plugins/index
|
||||
|
||||
|
||||
REST API
|
||||
========
|
||||
|
||||
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
|
||||
UI. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
.. automodule:: lemur.auth.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.destinations.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Notifications
|
||||
-------------
|
||||
|
||||
.. automodule:: lemur.notifications.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.users.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.roles.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.certificates.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Authorities
|
||||
-----------
|
||||
|
||||
.. automodule:: lemur.authorities.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
.. automodule:: lemur.domains.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Internals
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
internals/lemur
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
lemur_cloudca Package
|
||||
=====================
|
||||
|
||||
:mod:`lemur_cloudca` Package
|
||||
----------------------------
|
||||
|
||||
.. automodule:: lemur.plugins.lemur_cloudca
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`plugin` Module
|
||||
--------------------
|
||||
|
||||
.. automodule:: lemur.plugins.lemur_cloudca.plugin
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -27,6 +27,5 @@ Subpackages
|
||||
lemur.plugins.base
|
||||
lemur.plugins.bases
|
||||
lemur.plugins.lemur_aws
|
||||
lemur.plugins.lemur_cloudca
|
||||
lemur.plugins.lemur_email
|
||||
lemur.plugins.lemur_verisign
|
||||
|
@ -96,5 +96,4 @@ Subpackages
|
||||
lemur.notifications
|
||||
lemur.plugins
|
||||
lemur.roles
|
||||
lemur.status
|
||||
lemur.users
|
||||
|
@ -1,11 +0,0 @@
|
||||
status Package
|
||||
==============
|
||||
|
||||
:mod:`views` Module
|
||||
-------------------
|
||||
|
||||
.. automodule:: lemur.status.views
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -1,6 +1,3 @@
|
||||
Writing a Plugin
|
||||
================
|
||||
|
||||
Several interfaces exist for extending Lemur:
|
||||
|
||||
* Issuer (lemur.plugins.base.issuer)
|
||||
@ -8,7 +5,7 @@ Several interfaces exist for extending Lemur:
|
||||
* Source (lemur.plugins.base.source)
|
||||
* Notification (lemur.plugins.base.notification)
|
||||
|
||||
Each interface has its own function that will need to be defined in order for
|
||||
Each interface has its own functions that will need to be defined in order for
|
||||
your plugin to work correctly. See :ref:`Plugin Interfaces <PluginInterfaces>` for details.
|
||||
|
||||
|
||||
@ -91,7 +88,7 @@ Issuer
|
||||
Issuer plugins are used when you have an external service that creates certificates or authorities.
|
||||
In the simple case the third party only issues certificates (Verisign, DigiCert, etc.).
|
||||
|
||||
If you have a third party or internal service that creates authorities (CloudCA, EJBCA, etc.), Lemur has you covered,
|
||||
If you have a third party or internal service that creates authorities (EJBCA, etc.), Lemur has you covered,
|
||||
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
||||
|
||||
|
||||
@ -214,8 +211,8 @@ certificate Lemur does not know about and adding the certificate to it's invento
|
||||
|
||||
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
|
||||
|
||||
.. warning::
|
||||
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarentee that ]
|
||||
.. warning::
|
||||
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
|
||||
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron
|
||||
job if you need a higher resolution sync job.
|
||||
|
||||
@ -226,10 +223,31 @@ The `SourcePlugin` object requires implementation of one function::
|
||||
# request.get("some source of certificates")
|
||||
|
||||
|
||||
.. Note::
|
||||
.. note::
|
||||
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
|
||||
|
||||
|
||||
Export
|
||||
------
|
||||
|
||||
Formats, formats and more formats. That's the current PKI landscape. See the always relevant `xkcd <https://xkcd.com/927/>`_.
|
||||
Thankfully Lemur supports the ability to output your certificates into whatever format you want. This integration comes by the way
|
||||
of Export plugins. Support is still new and evolving, the goal of these plugins is to return raw data in a new format that
|
||||
can then be used by any number of applications. Included in Lemur is the `JavaExportPlugin` which currently supports generating
|
||||
a Java Key Store (JKS) file for use in Java based applications.
|
||||
|
||||
|
||||
The `ExportPlugin` object requires the implementation of one function::
|
||||
|
||||
def export(self, body, chain, key, options, **kwargs):
|
||||
# sys.call('openssl hokuspocus')
|
||||
# return "extension", passphrase, raw
|
||||
|
||||
|
||||
.. note::
|
||||
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to these calls.
|
||||
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
|
@ -1,66 +0,0 @@
|
||||
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
|
||||
UI. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
.. automodule:: lemur.auth.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.destinations.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Notifications
|
||||
-------------
|
||||
|
||||
.. automodule:: lemur.notifications.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.users.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
.. automodule:: lemur.roles.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Certificates
|
||||
------------
|
||||
|
||||
.. automodule:: lemur.certificates.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Authorities
|
||||
-----------
|
||||
|
||||
.. automodule:: lemur.authorities.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
.. automodule:: lemur.domains.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
53
docs/doing-a-release.rst
Normal file
@ -0,0 +1,53 @@
|
||||
Doing a release
|
||||
===============
|
||||
|
||||
Doing a release of ``lemur`` requires a few steps.
|
||||
|
||||
Bumping the version number
|
||||
--------------------------
|
||||
|
||||
The next step in doing a release is bumping the version number in the
|
||||
software.
|
||||
|
||||
* Update the version number in ``lemur/__about__.py``.
|
||||
* Set the release date in the :doc:`/changelog`.
|
||||
* Do a commit indicating this.
|
||||
* Send a pull request with this.
|
||||
* Wait for it to be merged.
|
||||
|
||||
Performing the release
|
||||
----------------------
|
||||
|
||||
The commit that merged the version number bump is now the official release
|
||||
commit for this release. You will need to have ``gpg`` installed and a ``gpg``
|
||||
key in order to do a release. Once this has happened:
|
||||
|
||||
* Run ``invoke release {version}``.
|
||||
|
||||
The release should now be available on PyPI and a tag should be available in
|
||||
the repository.
|
||||
|
||||
Verifying the release
|
||||
---------------------
|
||||
|
||||
You should verify that ``pip install lemur`` works correctly:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import lemur
|
||||
>>> lemur.__version__
|
||||
'...'
|
||||
|
||||
Verify that this is the version you just released.
|
||||
|
||||
Post-release tasks
|
||||
------------------
|
||||
|
||||
* Update the version number to the next major (e.g. ``0.5.dev1``) in
|
||||
``lemur/__about__.py`` and
|
||||
* Add new :doc:`/changelog` entry with next version and note that it is under
|
||||
active development
|
||||
* Send a pull request with these items
|
||||
* Check for any outstanding code undergoing a deprecation cycle by looking in
|
||||
``lemur.utils`` for ``DeprecatedIn**`` definitions. If any exist open
|
||||
a ticket to increment them for the next release.
|
25
docs/faq.rst
@ -4,8 +4,8 @@ Frequently Asked Questions
|
||||
Common Problems
|
||||
---------------
|
||||
|
||||
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
|
||||
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See
|
||||
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'*
|
||||
You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
|
||||
:doc:`administration/index` for more information.
|
||||
|
||||
|
||||
@ -14,6 +14,27 @@ I am seeing Lemur's javascript load in my browser but not the CSS.
|
||||
:doc:`production/index` for example configurations.
|
||||
|
||||
|
||||
After installing Lemur I am unable to login
|
||||
Ensure that you are trying to login with the credentials you entered during `lemur init`. These are separate
|
||||
from the postgres database credentials.
|
||||
|
||||
|
||||
Running 'lemur db upgrade' seems stuck.
|
||||
Most likely, the upgrade is stuck because an existing query on the database is holding onto a lock that the
|
||||
migration needs.
|
||||
|
||||
To resolve, login to your lemur database and run:
|
||||
|
||||
SELECT * FROM pg_locks l INNER JOIN pg_stat_activity s ON (l.pid = s.pid) WHERE waiting AND NOT granted;
|
||||
|
||||
This will give you a list of queries that are currently waiting to be executed. From there attempt to idenity the PID
|
||||
of the query blocking the migration. Once found execute:
|
||||
|
||||
select pg_terminate_backend(<blocking-pid>);
|
||||
|
||||
See `<http://stackoverflow.com/questions/22896496/alembic-migration-stuck-with-postgresql>`_ for more.
|
||||
|
||||
|
||||
How do I
|
||||
--------
|
||||
|
||||
|
@ -1,261 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx-autopackage-script
|
||||
|
||||
This script parses a directory tree looking for python modules and packages and
|
||||
creates ReST files appropriately to create code documentation with Sphinx.
|
||||
It also creates a modules index (named modules.<suffix>).
|
||||
"""
|
||||
|
||||
# Copyright 2008 Société des arts technologiques (SAT), http://www.sat.qc.ca/
|
||||
# Copyright 2010 Thomas Waldmann <tw AT waldmann-edv DOT de>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import optparse
|
||||
|
||||
|
||||
# automodule options
|
||||
OPTIONS = ['members',
|
||||
'undoc-members',
|
||||
# 'inherited-members', # disabled because there's a bug in sphinx
|
||||
'show-inheritance',
|
||||
]
|
||||
|
||||
INIT = '__init__.py'
|
||||
|
||||
def makename(package, module):
|
||||
"""Join package and module with a dot."""
|
||||
# Both package and module can be None/empty.
|
||||
if package:
|
||||
name = package
|
||||
if module:
|
||||
name += '.' + module
|
||||
else:
|
||||
name = module
|
||||
return name
|
||||
|
||||
def write_file(name, text, opts):
|
||||
"""Write the output file for module/package <name>."""
|
||||
if opts.dryrun:
|
||||
return
|
||||
fname = os.path.join(opts.destdir, "%s.%s" % (name, opts.suffix))
|
||||
if not opts.force and os.path.isfile(fname):
|
||||
print 'File %s already exists, skipping.' % fname
|
||||
else:
|
||||
print 'Creating file %s.' % fname
|
||||
f = open(fname, 'w')
|
||||
f.write(text)
|
||||
f.close()
|
||||
|
||||
def format_heading(level, text):
|
||||
"""Create a heading of <level> [1, 2 or 3 supported]."""
|
||||
underlining = ['=', '-', '~', ][level-1] * len(text)
|
||||
return '%s\n%s\n\n' % (text, underlining)
|
||||
|
||||
def format_directive(module, package=None):
|
||||
"""Create the automodule directive and add the options."""
|
||||
directive = '.. automodule:: %s\n' % makename(package, module)
|
||||
for option in OPTIONS:
|
||||
directive += ' :%s:\n' % option
|
||||
return directive
|
||||
|
||||
def create_module_file(package, module, opts):
|
||||
"""Build the text of the file and write the file."""
|
||||
text = format_heading(1, '%s Module' % module)
|
||||
text += format_heading(2, ':mod:`%s` Module' % module)
|
||||
text += format_directive(module, package)
|
||||
write_file(makename(package, module), text, opts)
|
||||
|
||||
def create_package_file(root, master_package, subroot, py_files, opts, subs):
|
||||
"""Build the text of the file and write the file."""
|
||||
package = os.path.split(root)[-1]
|
||||
text = format_heading(1, '%s Package' % package)
|
||||
# add each package's module
|
||||
for py_file in py_files:
|
||||
if shall_skip(os.path.join(root, py_file)):
|
||||
continue
|
||||
is_package = py_file == INIT
|
||||
py_file = os.path.splitext(py_file)[0]
|
||||
py_path = makename(subroot, py_file)
|
||||
if is_package:
|
||||
heading = ':mod:`%s` Package' % package
|
||||
else:
|
||||
heading = ':mod:`%s` Module' % py_file
|
||||
text += format_heading(2, heading)
|
||||
text += format_directive(is_package and subroot or py_path, master_package)
|
||||
text += '\n'
|
||||
|
||||
# build a list of directories that are packages (they contain an INIT file)
|
||||
subs = [sub for sub in subs if os.path.isfile(os.path.join(root, sub, INIT))]
|
||||
# if there are some package directories, add a TOC for theses subpackages
|
||||
if subs:
|
||||
text += format_heading(2, 'Subpackages')
|
||||
text += '.. toctree::\n\n'
|
||||
for sub in subs:
|
||||
text += ' %s.%s\n' % (makename(master_package, subroot), sub)
|
||||
text += '\n'
|
||||
|
||||
write_file(makename(master_package, subroot), text, opts)
|
||||
|
||||
def create_modules_toc_file(master_package, modules, opts, name='modules'):
|
||||
"""
|
||||
Create the module's index.
|
||||
"""
|
||||
text = format_heading(1, '%s Modules' % opts.header)
|
||||
text += '.. toctree::\n'
|
||||
text += ' :maxdepth: %s\n\n' % opts.maxdepth
|
||||
|
||||
modules.sort()
|
||||
prev_module = ''
|
||||
for module in modules:
|
||||
# look if the module is a subpackage and, if yes, ignore it
|
||||
if module.startswith(prev_module + '.'):
|
||||
continue
|
||||
prev_module = module
|
||||
text += ' %s\n' % module
|
||||
|
||||
write_file(name, text, opts)
|
||||
|
||||
def shall_skip(module):
|
||||
"""
|
||||
Check if we want to skip this module.
|
||||
"""
|
||||
# skip it, if there is nothing (or just \n or \r\n) in the file
|
||||
return os.path.getsize(module) < 3
|
||||
|
||||
def recurse_tree(path, excludes, opts):
|
||||
"""
|
||||
Look for every file in the directory tree and create the corresponding
|
||||
ReST files.
|
||||
"""
|
||||
# use absolute path for root, as relative paths like '../../foo' cause
|
||||
# 'if "/." in root ...' to filter out *all* modules otherwise
|
||||
path = os.path.abspath(path)
|
||||
# check if the base directory is a package and get is name
|
||||
if INIT in os.listdir(path):
|
||||
package_name = path.split(os.path.sep)[-1]
|
||||
else:
|
||||
package_name = None
|
||||
|
||||
toc = []
|
||||
tree = os.walk(path, False)
|
||||
for root, subs, files in tree:
|
||||
# keep only the Python script files
|
||||
py_files = sorted([f for f in files if os.path.splitext(f)[1] == '.py'])
|
||||
if INIT in py_files:
|
||||
py_files.remove(INIT)
|
||||
py_files.insert(0, INIT)
|
||||
# remove hidden ('.') and private ('_') directories
|
||||
subs = sorted([sub for sub in subs if sub[0] not in ['.', '_']])
|
||||
# check if there are valid files to process
|
||||
# TODO: could add check for windows hidden files
|
||||
if "/." in root or "/_" in root \
|
||||
or not py_files \
|
||||
or is_excluded(root, excludes):
|
||||
continue
|
||||
if INIT in py_files:
|
||||
# we are in package ...
|
||||
if (# ... with subpackage(s)
|
||||
subs
|
||||
or
|
||||
# ... with some module(s)
|
||||
len(py_files) > 1
|
||||
or
|
||||
# ... with a not-to-be-skipped INIT file
|
||||
not shall_skip(os.path.join(root, INIT))
|
||||
):
|
||||
subroot = root[len(path):].lstrip(os.path.sep).replace(os.path.sep, '.')
|
||||
create_package_file(root, package_name, subroot, py_files, opts, subs)
|
||||
toc.append(makename(package_name, subroot))
|
||||
elif root == path:
|
||||
# if we are at the root level, we don't require it to be a package
|
||||
for py_file in py_files:
|
||||
if not shall_skip(os.path.join(path, py_file)):
|
||||
module = os.path.splitext(py_file)[0]
|
||||
create_module_file(package_name, module, opts)
|
||||
toc.append(makename(package_name, module))
|
||||
|
||||
# create the module's index
|
||||
if not opts.notoc:
|
||||
create_modules_toc_file(package_name, toc, opts)
|
||||
|
||||
def normalize_excludes(rootpath, excludes):
|
||||
"""
|
||||
Normalize the excluded directory list:
|
||||
* must be either an absolute path or start with rootpath,
|
||||
* otherwise it is joined with rootpath
|
||||
* with trailing slash
|
||||
"""
|
||||
sep = os.path.sep
|
||||
f_excludes = []
|
||||
for exclude in excludes:
|
||||
if not os.path.isabs(exclude) and not exclude.startswith(rootpath):
|
||||
exclude = os.path.join(rootpath, exclude)
|
||||
if not exclude.endswith(sep):
|
||||
exclude += sep
|
||||
f_excludes.append(exclude)
|
||||
return f_excludes
|
||||
|
||||
def is_excluded(root, excludes):
|
||||
"""
|
||||
Check if the directory is in the exclude list.
|
||||
|
||||
Note: by having trailing slashes, we avoid common prefix issues, like
|
||||
e.g. an exlude "foo" also accidentally excluding "foobar".
|
||||
"""
|
||||
sep = os.path.sep
|
||||
if not root.endswith(sep):
|
||||
root += sep
|
||||
for exclude in excludes:
|
||||
if root.startswith(exclude):
|
||||
return True
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""
|
||||
Parse and check the command line arguments.
|
||||
"""
|
||||
parser = optparse.OptionParser(usage="""usage: %prog [options] <package path> [exclude paths, ...]
|
||||
|
||||
Note: By default this script will not overwrite already created files.""")
|
||||
parser.add_option("-n", "--doc-header", action="store", dest="header", help="Documentation Header (default=Project)", default="Project")
|
||||
parser.add_option("-d", "--dest-dir", action="store", dest="destdir", help="Output destination directory", default="")
|
||||
parser.add_option("-s", "--suffix", action="store", dest="suffix", help="module suffix (default=txt)", default="txt")
|
||||
parser.add_option("-m", "--maxdepth", action="store", dest="maxdepth", help="Maximum depth of submodules to show in the TOC (default=4)", type="int", default=4)
|
||||
parser.add_option("-r", "--dry-run", action="store_true", dest="dryrun", help="Run the script without creating the files")
|
||||
parser.add_option("-f", "--force", action="store_true", dest="force", help="Overwrite all the files")
|
||||
parser.add_option("-t", "--no-toc", action="store_true", dest="notoc", help="Don't create the table of content file")
|
||||
(opts, args) = parser.parse_args()
|
||||
if not args:
|
||||
parser.error("package path is required.")
|
||||
else:
|
||||
rootpath, excludes = args[0], args[1:]
|
||||
if os.path.isdir(rootpath):
|
||||
# check if the output destination is a valid directory
|
||||
if opts.destdir and os.path.isdir(opts.destdir):
|
||||
excludes = normalize_excludes(rootpath, excludes)
|
||||
recurse_tree(rootpath, excludes, opts)
|
||||
else:
|
||||
print '%s is not a valid output destination directory.' % opts.destdir
|
||||
else:
|
||||
print '%s is not a valid directory.' % rootpath
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
BIN
docs/guide/certificate_extensions.png
Normal file
After Width: | Height: | Size: 125 KiB |
BIN
docs/guide/create.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
docs/guide/create_authority.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
docs/guide/create_authority_options.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/guide/create_certificate.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
docs/guide/create_role.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/guide/create_user.png
Normal file
After Width: | Height: | Size: 39 KiB |
@ -1,14 +1,103 @@
|
||||
Creating Users
|
||||
==============
|
||||
User Guide
|
||||
==========
|
||||
|
||||
These guides are quick tutorials on how to perform basic tasks in Lemur.
|
||||
|
||||
|
||||
Creating Roles
|
||||
==============
|
||||
Create a New Authority
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Before Lemur can issue certificates you must configure the authority you wish use. Lemur itself does
|
||||
not issue certificates, it relies on external CAs and the plugins associated with those CAs to create the certificate
|
||||
that Lemur can then manage.
|
||||
|
||||
|
||||
Creating Authorities
|
||||
====================
|
||||
.. figure:: create.png
|
||||
|
||||
In the authority table select "Create"
|
||||
|
||||
.. figure:: create_authority.png
|
||||
|
||||
Enter a authority name and short description about the authority. Enter an owner,
|
||||
and certificate common name. Depending on the authority and the authority/issuer plugin
|
||||
these values may or may not be used.
|
||||
|
||||
.. figure:: create_authority_options.png
|
||||
|
||||
Again how many of these values get used largely depends on the underlying plugin. It
|
||||
is important to make sure you select the right plugin that you wish to use.
|
||||
|
||||
|
||||
Create a New Certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: create.png
|
||||
|
||||
In the certificate table select "Create"
|
||||
|
||||
.. figure:: create_certificate.png
|
||||
|
||||
Enter an owner, short description and the authority you wish to issue this certificate.
|
||||
Enter a common name into the certificate, if no validity range is selected two years is
|
||||
the default.
|
||||
|
||||
You can add notification options and upload the created certificate to a destination, both
|
||||
of these are editable features and can be changed after the certificate has been created.
|
||||
|
||||
.. figure:: certificate_extensions.png
|
||||
|
||||
These options are typically for advanced users, the one exception is the `Subject Alternate Names` or SAN.
|
||||
For certificates that need to include more than one domains, the first domain is the Common Name and all
|
||||
other domains are added here as DNSName entries.
|
||||
|
||||
|
||||
Import an Existing Certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: upload_certificate.png
|
||||
|
||||
Enter a owner, short description and public certificate. If there are intermediates and private keys
|
||||
Lemur will track them just as it does if the certificate were created through Lemur. Lemur generates
|
||||
a certificate name but you can override that by passing a value to the `Custom Name` field.
|
||||
|
||||
You can add notification options and upload the created certificate to a destination, both
|
||||
of these are editable features and can be changed after the certificate has been created.
|
||||
|
||||
|
||||
Create a New User
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. figure:: settings.png
|
||||
|
||||
From the settings dropdown select "Users"
|
||||
|
||||
.. figure:: create.png
|
||||
|
||||
In the user table select "Create"
|
||||
|
||||
.. figure:: create_user.png
|
||||
|
||||
Enter the username, email and password for the user. You can also assign any
|
||||
roles that the user will need when they login. While there is no deletion
|
||||
(we want to track creators forever) you can mark a user as 'Inactive' that will
|
||||
not allow them to login to Lemur.
|
||||
|
||||
|
||||
Create a New Role
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: settings.png
|
||||
|
||||
From the settings dropdown select "Roles"
|
||||
|
||||
.. figure:: create.png
|
||||
|
||||
In the role table select "Create"
|
||||
|
||||
.. figure:: create_role.png
|
||||
|
||||
Enter a role name and short description about the role. You can optionally store
|
||||
a user/password on the role. This is useful if your authority require specific roles.
|
||||
You can then accurately map those roles onto Lemur users. Also optional you can assign
|
||||
users to your new role.
|
||||
|
||||
|
||||
Creating Certificates
|
||||
=====================
|
||||
|
BIN
docs/guide/settings.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/guide/upload_certificate.png
Normal file
After Width: | Height: | Size: 73 KiB |
@ -1,8 +1,8 @@
|
||||
Lemur
|
||||
=====
|
||||
|
||||
Lemur is a SSL management service. It attempts to help track and create certificates. By removing common issues with
|
||||
CSR creation it gives normal developers 'sane' SSL defaults and helps security teams push SSL usage throughout an organization.
|
||||
Lemur is a TLS management service. It attempts to help track and create certificates. By removing common issues with
|
||||
CSR creation it gives normal developers 'sane' TLS defaults and helps security teams push TLS usage throughout an organization.
|
||||
|
||||
Installation
|
||||
------------
|
||||
@ -27,8 +27,7 @@ Administration
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
administration/index
|
||||
plugins/index
|
||||
administration
|
||||
|
||||
Developers
|
||||
----------
|
||||
@ -38,14 +37,21 @@ Developers
|
||||
|
||||
developer/index
|
||||
|
||||
|
||||
REST API
|
||||
Security
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
developer/rest
|
||||
security
|
||||
|
||||
Doing a Release
|
||||
---------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
doing-a-release
|
||||
|
||||
FAQ
|
||||
----
|
||||
|
@ -1,20 +0,0 @@
|
||||
Plugins
|
||||
=======
|
||||
|
||||
There are several interfaces currently available to extend Lemur. These are a work in
|
||||
progress and the API is not frozen.
|
||||
|
||||
Bundled Plugins
|
||||
---------------
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
|
||||
The following extensions are available and maintained by members of the Lemur community:
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
|
@ -6,21 +6,22 @@ There are several steps needed to make Lemur production ready. Here we focus on
|
||||
Basics
|
||||
======
|
||||
|
||||
Because of the sensitivity of the information stored and maintain by Lemur it is important that you follow standard host hardening practices:
|
||||
Because of the sensitivity of the information stored and maintained by Lemur it is important that you follow standard host hardening practices:
|
||||
|
||||
- Run Lemur with a limited user
|
||||
- Disabled any unneeded service
|
||||
- Disabled any unneeded services
|
||||
- Enable remote logging
|
||||
- Restrict access to host
|
||||
|
||||
.. _CredentialManagement:
|
||||
|
||||
Credential Management
|
||||
---------------------
|
||||
|
||||
Lemur often contains credentials such as mutual SSL keys that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
|
||||
Lemur often contains credentials such as mutual TLS keys or API tokens that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
|
||||
to automatically encrypt these keys such that your keys not be in clear text.
|
||||
|
||||
The keys are located within lemur/keys and broken down by environment
|
||||
The keys are located within lemur/keys and broken down by environment.
|
||||
|
||||
To utilize this ability use the following commands:
|
||||
|
||||
@ -30,7 +31,7 @@ and
|
||||
|
||||
``lemur unlock``
|
||||
|
||||
If you choose to use this feature ensure that the KEY are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
|
||||
If you choose to use this feature ensure that the keys are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
|
||||
|
||||
Entropy
|
||||
-------
|
||||
@ -56,8 +57,8 @@ For additional information about OpenSSL entropy issues:
|
||||
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
|
||||
|
||||
|
||||
SSL
|
||||
====
|
||||
TLS/SSL
|
||||
=======
|
||||
|
||||
Nginx
|
||||
-----
|
||||
@ -71,7 +72,7 @@ Nginx is a very popular choice to serve a Python project:
|
||||
Nginx doesn't run any Python process, it only serves requests from outside to
|
||||
the Python server.
|
||||
|
||||
Therefor there are two steps:
|
||||
Therefore there are two steps:
|
||||
|
||||
- Run the Python process.
|
||||
- Run Nginx.
|
||||
@ -89,7 +90,7 @@ You must create a Nginx configuration file for Lemur. On GNU/Linux, they usually
|
||||
go into /etc/nginx/conf.d/. Name it lemur.conf.
|
||||
|
||||
`proxy_pass` just passes the external request to the Python process.
|
||||
The port much match the one used by the 0bin process of course.
|
||||
The port must match the one used by the Lemur process of course.
|
||||
|
||||
You can make some adjustments to get a better user experience::
|
||||
|
||||
@ -109,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;
|
||||
@ -127,10 +128,10 @@ You can make some adjustments to get a better user experience::
|
||||
|
||||
}
|
||||
|
||||
This makes Nginx serve the favicon and static files which is is much better at than python.
|
||||
This makes Nginx serve the favicon and static files which it is much better at than python.
|
||||
|
||||
It is highly recommended that you deploy SSL when deploying Lemur. This may be obvious given Lemur's purpose but the
|
||||
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates SSL::
|
||||
It is highly recommended that you deploy TLS when deploying Lemur. This may be obvious given Lemur's purpose but the
|
||||
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates TLS::
|
||||
|
||||
server_tokens off;
|
||||
add_header X-Frame-Options DENY;
|
||||
@ -175,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;
|
||||
@ -218,7 +219,7 @@ An example apache config::
|
||||
...
|
||||
</VirtualHost>
|
||||
|
||||
Also included in the configurations above are several best practices when it comes to deploying SSL. Things like enabling
|
||||
Also included in the configurations above are several best practices when it comes to deploying TLS. Things like enabling
|
||||
HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment.
|
||||
|
||||
.. note::
|
||||
@ -256,13 +257,12 @@ Create a configuration file named supervisor.ini::
|
||||
nodaemon=false
|
||||
minfds=1024
|
||||
minprocs=200
|
||||
user=lemur
|
||||
|
||||
[program:lemur]
|
||||
command=python /path/to/lemur/manage.py manage.py start
|
||||
|
||||
directory=/path/to/lemur/
|
||||
environment=PYTHONPATH='/path/to/lemur/'
|
||||
environment=PYTHONPATH='/path/to/lemur/',LEMUR_CONF='/home/lemur/.lemur/lemur.conf.py'
|
||||
user=lemur
|
||||
autostart=true
|
||||
autorestart=true
|
||||
@ -270,7 +270,7 @@ Create a configuration file named supervisor.ini::
|
||||
The 4 first entries are just boiler plate to get you started, you can copy
|
||||
them verbatim.
|
||||
|
||||
The last one define one (you can have many) process supervisor should manage.
|
||||
The last one defines one (you can have many) process supervisor should manage.
|
||||
|
||||
It means it will run the command::
|
||||
|
||||
@ -292,6 +292,28 @@ Then you can manage the process by running::
|
||||
|
||||
supervisorctl -c /path/to/supervisor.ini
|
||||
|
||||
It will start a shell from were you can start/stop/restart the service
|
||||
It will start a shell from which you can start/stop/restart the service.
|
||||
|
||||
You can read all errors that might occurs from /tmp/lemur.log.
|
||||
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
|
@ -1,95 +1,93 @@
|
||||
Quickstart
|
||||
**********
|
||||
|
||||
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service.
|
||||
This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
|
||||
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service. This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
|
||||
|
||||
Pressed for time? See the Lemur docker file on `Github <https://github.com/Netflix/lemur-docker>`_.
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
Some basic prerequisites which you'll need in order to run Lemur:
|
||||
|
||||
* A UNIX-based operating system. We test on Ubuntu, develop on OS X
|
||||
* A UNIX-based operating system (we test on Ubuntu, develop on OS X)
|
||||
* Python 2.7
|
||||
* PostgreSQL
|
||||
* Ngnix
|
||||
* Nginx
|
||||
|
||||
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and TLS (ELB), are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be be as generic as possible and are not intended to document every step of launching Lemur into a given environment.
|
||||
|
||||
|
||||
Installing Build Dependencies
|
||||
-----------------------------
|
||||
|
||||
If installing Lemur on a bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo apt-get update
|
||||
$ 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).
|
||||
|
||||
Now, install Python ``virtualenv`` package:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo pip install -U virtualenv
|
||||
|
||||
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and SSL (ELB),
|
||||
are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be
|
||||
be as generic as possible and are not intended to document every step of launching Lemur into a given environment.
|
||||
|
||||
Setting up an Environment
|
||||
-------------------------
|
||||
|
||||
The first thing you'll need is the Python ``virtualenv`` package. You probably already
|
||||
have this, but if not, you can install it with::
|
||||
|
||||
pip install -U virtualenv
|
||||
|
||||
Once that's done, choose a location for the environment, and create it with the ``virtualenv``
|
||||
command. For our guide, we're going to choose ``/www/lemur/``::
|
||||
|
||||
virtualenv /www/lemur/
|
||||
|
||||
Finally, activate your virtualenv::
|
||||
|
||||
source /www/lemur/bin/activate
|
||||
|
||||
.. note:: Activating the environment adjusts your PATH, so that things like pip now
|
||||
install into the virtualenv by default.
|
||||
|
||||
|
||||
Installing build dependencies
|
||||
-----------------------------
|
||||
|
||||
If installing Lemur on truely bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's
|
||||
dependencies::
|
||||
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install nodejs-legacy python-pip libpq-dev python-dev build-essential libssl-dev libffi-dev nginx git supervisor
|
||||
|
||||
And optionally if your database is going to be on the same host as the webserver::
|
||||
|
||||
$ sudo apt-get install postgres
|
||||
|
||||
|
||||
Installing Lemur
|
||||
----------------
|
||||
|
||||
Once you've got the environment setup, you can install Lemur and all its dependencies with
|
||||
the same command you used to grab virtualenv::
|
||||
|
||||
pip install -U lemur
|
||||
|
||||
Once everything is installed, you should be able to execute the Lemur CLI, via ``lemur``, and get something
|
||||
like the following:
|
||||
In this guide, Lemur will be installed in ``/www``, so you need to create that structure first:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur
|
||||
usage: lemur [--config=/path/to/settings.py] [command] [options]
|
||||
$ sudo mkdir /www
|
||||
$ cd /www
|
||||
|
||||
Clone Lemur inside the just created directory and give yourself write permission (we assume ``lemur`` is the user):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo git clone https://github.com/Netflix/lemur
|
||||
$ sudo chown -R lemur lemur/
|
||||
|
||||
Create the virtual environment, activate it and enter the Lemur's directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ virtualenv lemur
|
||||
$ source /www/lemur/bin/activate
|
||||
$ cd lemur
|
||||
|
||||
.. note:: Activating the environment adjusts your PATH, so that things like pip now install into the virtualenv by default.
|
||||
|
||||
|
||||
Installing from Source
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you're installing the Lemur source (e.g. from git), you'll also need to install **npm**.
|
||||
Once your system is prepared, ensure that you are in the virtualenv:
|
||||
|
||||
Once your system is prepared, symlink your source into the virtualenv:
|
||||
.. code-block:: bash
|
||||
|
||||
$ which python
|
||||
|
||||
And then run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ make develop
|
||||
|
||||
.. Note:: This command will install npm dependencies as well as compile static assets.
|
||||
.. note:: This command will install npm dependencies as well as compile static assets.
|
||||
|
||||
|
||||
Creating a configuration
|
||||
------------------------
|
||||
|
||||
Before we run Lemur we must create a valid configuration file for it.
|
||||
|
||||
The Lemur cli comes with a simple command to get you up and running quickly.
|
||||
Before we run Lemur, we must create a valid configuration file for it. The Lemur command line interface comes with a simple command to get you up and running quickly.
|
||||
|
||||
Simply run:
|
||||
|
||||
@ -97,89 +95,82 @@ Simply run:
|
||||
|
||||
$ lemur create_config
|
||||
|
||||
.. Note:: This command will create a default configuration under `~/.lemur/lemur.conf.py` you
|
||||
can specify this location by passing the `config_path` parameter to the `create_config` command.
|
||||
.. note:: This command will create a default configuration under ``~/.lemur/lemur.conf.py`` you can specify this location by passing the ``config_path`` parameter to the ``create_config`` command.
|
||||
|
||||
You can specify ``-c`` or ``--config`` to any Lemur command to specify the current environment you are working in. Lemur will also look under the environmental variable ``LEMUR_CONF`` should that be easier to setup in your environment.
|
||||
|
||||
You can specify `-c` or `--config` to any Lemur command to specify the current environment
|
||||
you are working in. Lemur will also look under the environmental variable `LEMUR_CONF` should
|
||||
that be easier to setup in your environment.
|
||||
|
||||
Update your configuration
|
||||
-------------------------
|
||||
|
||||
Once created you will need to update the configuration file with information about your environment,
|
||||
such as which database to talk to, where keys are stores etc..
|
||||
Once created, you will need to update the configuration file with information about your environment, such as which database to talk to, where keys are stored etc.
|
||||
|
||||
.. note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
|
||||
``postgresql://userame:password@<database-fqdn>:<database-port>/<database-name>``
|
||||
|
||||
.. Note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
|
||||
postgresql://userame:password@databasefqdn:databaseport/databasename
|
||||
|
||||
Setup Postgres
|
||||
--------------
|
||||
|
||||
For production a dedicated database is recommended, for this guide we will assume postgres has been installed and is on
|
||||
the same machine that Lemur is installed on.
|
||||
For production, a dedicated database is recommended, for this guide we will assume postgres has been installed and is on the same machine that Lemur is installed on.
|
||||
|
||||
First, set a password for the postgres user. For this guide, we will use **lemur** as an example but you should use the database password generated for by Lemur::
|
||||
First, set a password for the postgres user. For this guide, we will use ``lemur`` as an example but you should use the database password generated by Lemur:
|
||||
|
||||
$ sudo -u postgres psql postgres
|
||||
# \password postgres
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
.. code-block:: bash
|
||||
|
||||
Type CTRL-D to exit psql once you have changed the password.
|
||||
$ sudo -u postgres -i
|
||||
# \password postgres
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
|
||||
Next, we will create our a new database::
|
||||
Once successful, type CTRL-D to exit the Postgres shell.
|
||||
|
||||
$ sudo -u postgres createdb lemur
|
||||
Next, we will create our new database:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo -u postgres createdb lemur
|
||||
|
||||
.. _InitializingLemur:
|
||||
|
||||
.. 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
|
||||
------------------
|
||||
|
||||
Lemur provides a helpful command that will initialize your database for you. It creates a default user (lemur) that is
|
||||
used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when
|
||||
Lemur has discovered certificates from a third party source. This is also a default user that can be used to
|
||||
administer Lemur.
|
||||
Lemur provides a helpful command that will initialize your database for you. It creates a default user (``lemur``) that is used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when Lemur has discovered certificates from a third party source. This is also a default user that can be used to administer Lemur.
|
||||
|
||||
In addition to create a new User, Lemur also creates a few default email notifications. These notifications are based
|
||||
on a few configuration options such as `LEMUR_SECURITY_TEAM_EMAIL` they basically garentee that every cerificate within
|
||||
Lemur will send one expiration notification to the security team.
|
||||
In addition to creating a new user, Lemur also creates a few default email notifications. These notifications are based on a few configuration options such as ``LEMUR_SECURITY_TEAM_EMAIL``. They basically guarantee that every certificate within Lemur will send one expiration notification to the security team.
|
||||
|
||||
Additional notifications can be created through the UI or API.
|
||||
See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
|
||||
Additional notifications can be created through the UI or API. See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
|
||||
|
||||
**Make note of the password used as this will be used during first login to the Lemur UI**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur db init
|
||||
**Make note of the password used as this will be used during first login to the Lemur UI.**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cd /www/lemur/lemur
|
||||
$ lemur init
|
||||
|
||||
.. note:: It is recommended that once the 'lemur' user is created that you create individual users for every day access.
|
||||
There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account
|
||||
for them or be enrolled automatically through SSO. This can be done through the CLI or UI.
|
||||
See :ref:`Creating Users <CreatingUsers>` and :ref:`Command Line Interface <CommandLineInterface>` for details
|
||||
|
||||
.. 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.
|
||||
|
||||
|
||||
Setup a Reverse Proxy
|
||||
---------------------
|
||||
|
||||
By default, Lemur runs on port 5000. Even if you change this, under normal conditions you won't be able to bind to
|
||||
port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we recommend
|
||||
you setup a simple web proxy.
|
||||
By default, Lemur runs on port 8000. Even if you change this, under normal conditions you won't be able to bind to port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we need setup a simple web proxy. There are many different web servers you can use for this, we like and recommend Nginx.
|
||||
|
||||
|
||||
Proxying with Nginx
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You'll use the builtin HttpProxyModule within Nginx to handle proxying
|
||||
You'll use the builtin ``HttpProxyModule`` within Nginx to handle proxying. Edit the ``/etc/nginx/sites-available/default`` file according to the lines below
|
||||
|
||||
::
|
||||
|
||||
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;
|
||||
@ -194,16 +185,22 @@ You'll use the builtin HttpProxyModule within Nginx to handle proxying
|
||||
index index.html;
|
||||
}
|
||||
|
||||
See :doc:`../production/index` for more details on using Nginx.
|
||||
.. note:: See :doc:`../production/index` for more details on using Nginx.
|
||||
|
||||
After making these changes, restart Nginx service to apply them:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo service nginx restart
|
||||
|
||||
|
||||
Starting the Web Service
|
||||
------------------------
|
||||
|
||||
Lemur provides a built-in webserver (powered by gunicorn and eventlet) to get you off the ground quickly.
|
||||
Lemur provides a built-in web server (powered by gunicorn and eventlet) to get you off the ground quickly.
|
||||
|
||||
To start the webserver, you simply use ``lemur start``. If you opted to use an alternative configuration path
|
||||
you can pass that via the --config option.
|
||||
To start the web server, you simply use ``lemur start``. If you opted to use an alternative configuration path
|
||||
you can pass that via the ``--config`` option.
|
||||
|
||||
.. note::
|
||||
You can login with the default user created during :ref:`Initializing Lemur <InitializingLemur>` or any other
|
||||
@ -211,23 +208,23 @@ you can pass that via the --config option.
|
||||
|
||||
::
|
||||
|
||||
# Lemur's server runs on port 5000 by default. Make sure your client reflects
|
||||
# Lemur's server runs on port 8000 by default. Make sure your client reflects
|
||||
# the correct host and port!
|
||||
lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:5000
|
||||
lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:8000
|
||||
|
||||
You should now be able to test the web service by visiting ``http://localhost:8000/``.
|
||||
|
||||
You should now be able to test the web service by visiting `http://localhost:5000/`.
|
||||
|
||||
Running Lemur as a Service
|
||||
---------------------------
|
||||
--------------------------
|
||||
|
||||
We recommend using whatever software you are most familiar with for managing Lemur processes. One option is `Supervisor <http://supervisord.org/>`_.
|
||||
|
||||
We recommend using whatever software you are most familiar with for managing Lemur processes. One option is
|
||||
`Supervisor <http://supervisord.org/>`_.
|
||||
|
||||
Configure ``supervisord``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's bin/
|
||||
folder and you're good to go.
|
||||
Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's ``bin/`` folder and you're good to go.
|
||||
|
||||
::
|
||||
|
||||
@ -242,42 +239,43 @@ folder and you're good to go.
|
||||
|
||||
See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor.
|
||||
|
||||
|
||||
Syncing
|
||||
-------
|
||||
|
||||
Lemur uses periodic sync tasks to make sure it is up-to-date with it's environment. As always things can change outside
|
||||
of Lemur, but we do our best to reconcile those changes.
|
||||
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
|
||||
--------------------
|
||||
|
||||
If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The
|
||||
``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the
|
||||
power and flexibility that goes with it.
|
||||
If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The ``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the power and flexibility that goes with it.
|
||||
|
||||
Some of the features which you'll likely find useful are listed below.
|
||||
|
||||
Some of those which you'll likely find useful are:
|
||||
|
||||
lock
|
||||
~~~~
|
||||
|
||||
Encrypts sensitive key material - This is most useful for storing encrypted secrets in source code.
|
||||
Encrypts sensitive key material - this is most useful for storing encrypted secrets in source code.
|
||||
|
||||
|
||||
unlock
|
||||
~~~~~~
|
||||
|
||||
Decrypts sensitive key material - Used to decrypt the secrets stored in source during deployment.
|
||||
Decrypts sensitive key material - used to decrypt the secrets stored in source during deployment.
|
||||
|
||||
|
||||
What's Next?
|
||||
------------
|
||||
|
||||
The above gets you going, but for production there are several different security considerations to take into account,
|
||||
remember Lemur is handling sensitive data and security is imperative.
|
||||
Get familiar with how Lemur works by reviewing the :doc:`../guide/index`. When you're ready see :doc:`../production/index` for more details on how to configure Lemur for production.
|
||||
|
||||
See :doc:`../production/index` for more details on how to configure Lemur for production.
|
||||
The above just gets you going, but for production there are several different security considerations to take into account. Remember, Lemur is handling sensitive data and security is imperative.
|
||||
|
@ -2,4 +2,28 @@ Jinja2>=2.3
|
||||
Pygments>=1.2
|
||||
Sphinx>=1.3
|
||||
docutils>=0.7
|
||||
markupsafe
|
||||
markupsafe
|
||||
sphinxcontrib-httpdomain
|
||||
Flask==0.10.1
|
||||
Flask-RESTful==0.3.3
|
||||
Flask-SQLAlchemy==2.1
|
||||
Flask-Script==2.0.5
|
||||
Flask-Migrate==1.7.0
|
||||
Flask-Bcrypt==0.7.1
|
||||
Flask-Principal==0.4.0
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy-Utils==0.31.4
|
||||
BeautifulSoup4
|
||||
requests==2.9.1
|
||||
psycopg2==2.6.1
|
||||
arrow==0.7.0
|
||||
boto==2.38.0 # we might make this optional
|
||||
six==1.10.0
|
||||
gunicorn==19.4.4
|
||||
pycrypto==2.6.1
|
||||
cryptography==1.1.2
|
||||
pyopenssl==0.15.1
|
||||
pyjwt==1.4.0
|
||||
xmltodict==0.9.2
|
||||
lockfile==0.12.2
|
||||
future==0.15.2
|
||||
|
66
docs/security.rst
Normal file
@ -0,0 +1,66 @@
|
||||
Security
|
||||
========
|
||||
|
||||
We take the security of ``lemur`` seriously. The following are a set of
|
||||
policies we have adopted to ensure that security issues are addressed in a
|
||||
timely fashion.
|
||||
|
||||
Reporting a security issue
|
||||
--------------------------
|
||||
|
||||
We ask that you do not report security issues to our normal GitHub issue
|
||||
tracker.
|
||||
|
||||
If you believe you've identified a security issue with ``lemur``, please
|
||||
report it to ``cloudsecurity@netflix.com``.
|
||||
|
||||
Once you've submitted an issue via email, you should receive an acknowledgment
|
||||
within 48 hours, and depending on the action to be taken, you may receive
|
||||
further follow-up emails.
|
||||
|
||||
Supported Versions
|
||||
------------------
|
||||
|
||||
At any given time, we will provide security support for the `master`_ branch
|
||||
as well as the 2 most recent releases.
|
||||
|
||||
Disclosure Process
|
||||
------------------
|
||||
|
||||
Our process for taking a security issue from private discussion to public
|
||||
disclosure involves multiple steps.
|
||||
|
||||
Approximately one week before full public disclosure, we will send advance
|
||||
notification of the issue to a list of people and organizations, primarily
|
||||
composed of operating-system vendors and other distributors of
|
||||
``lemur``. This notification will consist of an email message
|
||||
containing:
|
||||
|
||||
* A full description of the issue and the affected versions of
|
||||
``lemur``.
|
||||
* The steps we will be taking to remedy the issue.
|
||||
* The patches, if any, that will be applied to ``lemur``.
|
||||
* The date on which the ``lemur`` team will apply these patches, issue
|
||||
new releases, and publicly disclose the issue.
|
||||
|
||||
Simultaneously, the reporter of the issue will receive notification of the date
|
||||
on which we plan to make the issue public.
|
||||
|
||||
On the day of disclosure, we will take the following steps:
|
||||
|
||||
* Apply the relevant patches to the ``lemur`` repository. The commit
|
||||
messages for these patches will indicate that they are for security issues,
|
||||
but will not describe the issue in any detail; instead, they will warn of
|
||||
upcoming disclosure.
|
||||
* Issue the relevant releases.
|
||||
|
||||
If a reported issue is believed to be particularly time-sensitive – due to a
|
||||
known exploit in the wild, for example – the time between advance notification
|
||||
and public disclosure may be shortened considerably.
|
||||
|
||||
The list of people and organizations who receives advanced notification of
|
||||
security issues is not, and will not, be made public. This list generally
|
||||
consists of high profile downstream distributors and is entirely at the
|
||||
discretion of the ``lemur`` team.
|
||||
|
||||
.. _`master`: https://github.com/Netflix/lemur
|
@ -48,8 +48,8 @@ gulp.task('test', function (done) {
|
||||
|
||||
gulp.task('dev:fonts', function () {
|
||||
var fileList = [
|
||||
'lemur/static/app/vendor/bower_components/bootstrap/dist/fonts/*',
|
||||
'lemur/static/app/vendor/bower_components/fontawesome/fonts/*'
|
||||
'bower_components/bootstrap/dist/fonts/*',
|
||||
'bower_components/fontawesome/fonts/*'
|
||||
];
|
||||
|
||||
return gulp.src(fileList)
|
||||
@ -57,7 +57,7 @@ gulp.task('dev:fonts', function () {
|
||||
});
|
||||
|
||||
gulp.task('dev:styles', function () {
|
||||
var baseContent = '@import "lemur/static/app/vendor/bower_components/bootstrap/less/bootstrap.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/variables.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/bootswatch.less";@import "lemur/static/app/vendor/bower_components/bootstrap/less/utilities.less";';
|
||||
var baseContent = '@import "bower_components/bootstrap/less/bootstrap.less";@import "bower_components/bootswatch/$theme$/variables.less";@import "bower_components/bootswatch/$theme$/bootswatch.less";@import "bower_components/bootstrap/less/utilities.less";';
|
||||
var isBootswatchFile = function (file) {
|
||||
|
||||
var suffix = 'bootswatch.less';
|
||||
@ -72,16 +72,17 @@ gulp.task('dev:styles', function () {
|
||||
};
|
||||
|
||||
var fileList = [
|
||||
'lemur/static/app/styles/lemur.css',
|
||||
'lemur/static/app/vendor/bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'lemur/static/app/vendor/bower_components/fontawesome/css/font-awesome.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-spinkit/src/angular-spinkit.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-chart.js/dist/angular-chart.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-loading-bar/src/loading-bar.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-ui-switch/angular-ui-switch.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-wizard/dist/angular-wizard.css',
|
||||
'lemur/static/app/vendor/bower_components/ng-table/ng-table.css',
|
||||
'lemur/static/app/vendor/bower_components/angularjs-toaster/toaster.css'
|
||||
'bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'bower_components/fontawesome/css/font-awesome.css',
|
||||
'bower_components/angular-spinkit/src/angular-spinkit.css',
|
||||
'bower_components/angular-chart.js/dist/angular-chart.css',
|
||||
'bower_components/angular-loading-bar/src/loading-bar.css',
|
||||
'bower_components/angular-ui-switch/angular-ui-switch.css',
|
||||
'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'
|
||||
];
|
||||
|
||||
return gulp.src(fileList)
|
||||
@ -238,8 +239,8 @@ gulp.task('build:images', function () {
|
||||
|
||||
gulp.task('package:strip', function () {
|
||||
return gulp.src(['lemur/static/dist/scripts/main*'])
|
||||
.pipe(replace('http:\/\/localhost:5000', ''))
|
||||
.pipe(replace('http:\/\/localhost:3000', ''))
|
||||
.pipe(replace('http:\/\/localhost:8000', ''))
|
||||
.pipe(useref())
|
||||
.pipe(revReplace())
|
||||
.pipe(gulp.dest('lemur/static/dist/scripts'))
|
||||
|
@ -27,7 +27,10 @@ function browserSyncInit(baseDir, files, browser) {
|
||||
browserSync.instance = browserSync.init(files, {
|
||||
startPath: '/index.html',
|
||||
server: {
|
||||
baseDir: baseDir
|
||||
baseDir: baseDir,
|
||||
routes: {
|
||||
'/bower_components': './bower_components'
|
||||
}
|
||||
},
|
||||
browser: browser,
|
||||
ghostMode: false
|
||||
|
18
lemur/__about__.py
Normal file
@ -0,0 +1,18 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__all__ = [
|
||||
"__title__", "__summary__", "__uri__", "__version__", "__author__",
|
||||
"__email__", "__license__", "__copyright__",
|
||||
]
|
||||
|
||||
__title__ = "lemur"
|
||||
__summary__ = ("Certificate management and orchestration service")
|
||||
__uri__ = "https://github.com/Netflix/lemur"
|
||||
|
||||
__version__ = "0.3.0"
|
||||
|
||||
__author__ = "The Lemur developers"
|
||||
__email__ = "security@netflix.com"
|
||||
|
||||
__license__ = "Apache License, Version 2.0"
|
||||
__copyright__ = "Copyright 2015 {0}".format(__author__)
|
@ -8,7 +8,10 @@
|
||||
|
||||
|
||||
"""
|
||||
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
|
||||
@ -17,11 +20,21 @@ from lemur.domains.views import mod as domains_bp
|
||||
from lemur.destinations.views import mod as destinations_bp
|
||||
from lemur.authorities.views import mod as authorities_bp
|
||||
from lemur.certificates.views import mod as certificates_bp
|
||||
from lemur.status.views import mod as status_bp
|
||||
from lemur.defaults.views import mod as defaults_bp
|
||||
from lemur.plugins.views import mod as plugins_bp
|
||||
from lemur.notifications.views import mod as notifications_bp
|
||||
from lemur.sources.views import mod as sources_bp
|
||||
|
||||
from lemur.__about__ import (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
__uri__, __version__
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"__title__", "__summary__", "__uri__", "__version__", "__author__",
|
||||
"__email__", "__license__", "__copyright__",
|
||||
]
|
||||
|
||||
LEMUR_BLUEPRINTS = (
|
||||
users_bp,
|
||||
@ -31,7 +44,7 @@ LEMUR_BLUEPRINTS = (
|
||||
destinations_bp,
|
||||
authorities_bp,
|
||||
certificates_bp,
|
||||
status_bp,
|
||||
defaults_bp,
|
||||
plugins_bp,
|
||||
notifications_bp,
|
||||
sources_bp
|
||||
@ -54,12 +67,21 @@ def configure_hook(app):
|
||||
from lemur.decorators import crossdomain
|
||||
if app.config.get('CORS'):
|
||||
@app.after_request
|
||||
@crossdomain(origin="http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
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
|
||||
|
@ -16,24 +16,36 @@ operator_permission = Permission(RoleNeed('operator'))
|
||||
admin_permission = Permission(RoleNeed('admin'))
|
||||
|
||||
CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'certificateView')
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||
|
||||
CertificateOwner = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'certificateView')
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'role')
|
||||
|
||||
|
||||
class SensitiveDomainPermission(Permission):
|
||||
def __init__(self):
|
||||
super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(str(certificate_id))
|
||||
o_need = CertificateOwnerNeed(str(role_id))
|
||||
super(ViewKeyPermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(ViewKeyPermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
|
||||
|
||||
class UpdateCertificatePermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(str(certificate_id))
|
||||
o_need = CertificateOwnerNeed(str(role_id))
|
||||
super(UpdateCertificatePermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
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'])
|
||||
@ -42,7 +54,7 @@ ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
|
||||
|
||||
class ViewRoleCredentialsPermission(Permission):
|
||||
def __init__(self, role_id):
|
||||
need = ViewRoleCredentialsNeed(str(role_id))
|
||||
need = ViewRoleCredentialsNeed(role_id)
|
||||
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin'))
|
||||
|
||||
|
||||
|
@ -8,11 +8,12 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from builtins import bytes
|
||||
import jwt
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
from builtins import str
|
||||
|
||||
from functools import wraps
|
||||
from datetime import datetime, timedelta
|
||||
@ -29,24 +30,21 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
|
||||
|
||||
from lemur.users import service as user_service
|
||||
from lemur.auth.permissions import CertificateOwnerNeed, CertificateCreatorNeed, \
|
||||
from lemur.auth.permissions import CertificateCreatorNeed, \
|
||||
AuthorityCreatorNeed, ViewRoleCredentialsNeed
|
||||
|
||||
|
||||
def base64url_decode(data):
|
||||
if isinstance(data, str):
|
||||
data = str(data)
|
||||
|
||||
rem = len(data) % 4
|
||||
|
||||
if rem > 0:
|
||||
data += b'=' * (4 - rem)
|
||||
data += '=' * (4 - rem)
|
||||
|
||||
return base64.urlsafe_b64decode(data)
|
||||
return base64.urlsafe_b64decode(bytes(data.encode('latin-1')))
|
||||
|
||||
|
||||
def base64url_encode(data):
|
||||
return base64.urlsafe_b64encode(data).replace(b'=', b'')
|
||||
return base64.urlsafe_b64encode(data).replace('=', '')
|
||||
|
||||
|
||||
def get_rsa_public_key(n, e):
|
||||
@ -141,9 +139,11 @@ def fetch_token_header(token):
|
||||
|
||||
try:
|
||||
return json.loads(base64url_decode(header_segment))
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
except binascii.Error:
|
||||
except binascii.Error as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
|
||||
|
||||
@ -165,8 +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(CertificateOwnerNeed(role.id))
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.id))
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.name))
|
||||
identity.provides.add(RoleNeed(role.name))
|
||||
|
||||
# apply ownership for authorities
|
||||
|
@ -9,11 +9,12 @@ import jwt
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from flask import g, Blueprint, current_app
|
||||
from flask import Blueprint, current_app
|
||||
|
||||
from flask.ext.restful import reqparse, Resource, Api
|
||||
from flask.ext.principal import Identity, identity_changed
|
||||
|
||||
from lemur.extensions import metrics
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
from lemur.users import service as user_service
|
||||
@ -35,7 +36,7 @@ class Login(Resource):
|
||||
|
||||
Authorization:Bearer <token>
|
||||
|
||||
Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting
|
||||
Tokens have a set expiration date. You can inspect the token expiration by base64 decoding the token and inspecting
|
||||
it's contents.
|
||||
|
||||
.. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \
|
||||
@ -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):
|
||||
"""
|
||||
@ -125,7 +125,7 @@ class Ping(Resource):
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
# take the information we have received from Meechum to create a new request
|
||||
# take the information we have received from the provider to create a new request
|
||||
params = {
|
||||
'client_id': args['clientId'],
|
||||
'grant_type': 'authorization_code',
|
||||
@ -138,7 +138,7 @@ class Ping(Resource):
|
||||
access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL')
|
||||
user_api_url = current_app.config.get('PING_USER_API_URL')
|
||||
|
||||
# the secret and cliendId will be given to you when you signup for meechum
|
||||
# the secret and cliendId will be given to you when you signup for the provider
|
||||
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
|
||||
headers = {'Authorization': 'Basic {0}'.format(basic)}
|
||||
|
||||
@ -179,14 +179,11 @@ 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 = []
|
||||
|
||||
# Legacy edge case - 'admin' has some special privileges associated with it
|
||||
if 'secops@netflix.com' in profile['googleGroups']:
|
||||
roles.append(role_service.get_by_name('admin'))
|
||||
|
||||
for group in profile['googleGroups']:
|
||||
role = role_service.get_by_name(group)
|
||||
if not role:
|
||||
@ -196,10 +193,12 @@ class Ping(Resource):
|
||||
# if we get an sso user create them an account
|
||||
# we still pick a random password in case sso is down
|
||||
if not user:
|
||||
# every user is an operator (tied to the verisignCA)
|
||||
v = role_service.get_by_name('verisign')
|
||||
if v:
|
||||
roles.append(v)
|
||||
|
||||
# every user is an operator (tied to a default role)
|
||||
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
|
||||
v = role_service.get_by_name(current_app.config.get('LEMUR_DEFAULT_ROLE'))
|
||||
if v:
|
||||
roles.append(v)
|
||||
|
||||
user = user_service.create(
|
||||
profile['email'],
|
||||
@ -222,7 +221,7 @@ class Ping(Resource):
|
||||
profile['email'],
|
||||
profile['email'],
|
||||
True,
|
||||
profile.get('thumbnailPhotoUrl'), # Encase profile isn't google+ enabled
|
||||
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
|
||||
roles
|
||||
)
|
||||
|
||||
@ -232,5 +231,78 @@ class Ping(Resource):
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
class Google(Resource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Google, self).__init__()
|
||||
|
||||
def post(self):
|
||||
access_token_url = 'https://accounts.google.com/o/oauth2/token'
|
||||
people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect'
|
||||
|
||||
self.reqparse.add_argument('clientId', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('redirectUri', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('code', type=str, required=True, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
# Step 1. Exchange authorization code for access token
|
||||
payload = {
|
||||
'client_id': args['clientId'],
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': args['redirectUri'],
|
||||
'code': args['code'],
|
||||
'client_secret': current_app.config.get('GOOGLE_SECRET')
|
||||
}
|
||||
|
||||
r = requests.post(access_token_url, data=payload)
|
||||
token = r.json()
|
||||
|
||||
# Step 2. Retrieve information about the current user
|
||||
headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])}
|
||||
|
||||
r = requests.get(people_api_url, headers=headers)
|
||||
profile = r.json()
|
||||
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
|
||||
if user:
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
class Providers(Resource):
|
||||
def get(self):
|
||||
active_providers = []
|
||||
|
||||
for provider in current_app.config.get("ACTIVE_PROVIDERS"):
|
||||
provider = provider.lower()
|
||||
|
||||
if provider == "google":
|
||||
active_providers.append({
|
||||
'name': 'google',
|
||||
'clientId': current_app.config.get("GOOGLE_CLIENT_ID"),
|
||||
'url': api.url_for(Google)
|
||||
})
|
||||
|
||||
elif provider == "ping":
|
||||
active_providers.append({
|
||||
'name': current_app.config.get("PING_NAME"),
|
||||
'url': current_app.config.get('PING_REDIRECT_URI'),
|
||||
'redirectUri': current_app.config.get("PING_REDIRECT_URI"),
|
||||
'clientId': current_app.config.get("PING_CLIENT_ID"),
|
||||
'responseType': 'code',
|
||||
'scope': ['openid', 'email', 'profile', 'address'],
|
||||
'scopeDelimiter': ' ',
|
||||
'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"),
|
||||
'requiredUrlParams': ['scope'],
|
||||
'type': '2.0'
|
||||
})
|
||||
|
||||
return active_providers
|
||||
|
||||
|
||||
api.add_resource(Login, '/auth/login', endpoint='login')
|
||||
api.add_resource(Ping, '/auth/ping', endpoint='ping')
|
||||
api.add_resource(Google, '/auth/google', endpoint='google')
|
||||
api.add_resource(Providers, '/auth/providers', endpoint='providers')
|
||||
|
@ -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 cert_get_cn, cert_get_not_after, cert_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 = cert_get_cn(cert)
|
||||
self.not_before = cert_get_not_before(cert)
|
||||
self.not_after = cert_get_not_after(cert)
|
||||
self.roles = roles
|
||||
self.description = description
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
return blob
|
||||
def __init__(self, **kwargs):
|
||||
self.owner = kwargs['owner']
|
||||
self.roles = kwargs.get('roles', [])
|
||||
self.name = kwargs.get('name')
|
||||
self.description = kwargs.get('description')
|
||||
self.authority_certificate = kwargs['authority_certificate']
|
||||
self.plugin_name = kwargs['plugin']['slug']
|
||||
|
112
lemur/authorities/schemas.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""
|
||||
.. module: lemur.authorities.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from marshmallow import fields, validates_schema
|
||||
from marshmallow import validate
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.common import validators
|
||||
|
||||
|
||||
class AuthorityInputSchema(LemurInputSchema):
|
||||
name = fields.String(required=True)
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
common_name = fields.String(required=True, validate=validators.sensitive_domain)
|
||||
|
||||
validity_start = fields.Date()
|
||||
validity_end = fields.Date()
|
||||
validity_years = fields.Integer()
|
||||
|
||||
# certificate body fields
|
||||
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
||||
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
|
||||
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
|
||||
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
|
||||
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
|
||||
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
||||
# signing related options
|
||||
type = fields.String(validate=validate.OneOf(['root', 'subca']), missing='root')
|
||||
parent = fields.Nested(AssociatedAuthoritySchema)
|
||||
signing_algorithm = fields.String(validate=validate.OneOf(['sha256WithRSA', 'sha1WithRSA']), missing='sha256WithRSA')
|
||||
key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048')
|
||||
key_name = fields.String()
|
||||
sensitivity = fields.String(validate=validate.OneOf(['medium', 'high']), missing='medium')
|
||||
serial_number = fields.Integer()
|
||||
first_serial = fields.Integer(missing=1)
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
roles = fields.Nested(AssociatedRoleSchema(many=True))
|
||||
|
||||
@validates_schema
|
||||
def validate_dates(self, data):
|
||||
validators.dates(data)
|
||||
|
||||
@validates_schema
|
||||
def validate_subca(self, data):
|
||||
if data['type'] == 'subca':
|
||||
if not data.get('parent'):
|
||||
raise ValidationError("If generating a subca parent 'authority' must be specified.")
|
||||
|
||||
|
||||
class AuthorityUpdateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
roles = fields.Nested(AssociatedRoleSchema(many=True))
|
||||
|
||||
|
||||
class RootAuthorityCertificateOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
status = fields.Boolean()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
|
||||
|
||||
class AuthorityOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
owner = fields.Email()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
active = fields.Boolean()
|
||||
options = fields.Dict()
|
||||
roles = fields.List(fields.Nested(AssociatedRoleSchema))
|
||||
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
|
||||
|
||||
|
||||
class AuthorityNestedOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
owner = fields.Email()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
active = fields.Boolean()
|
||||
|
||||
|
||||
authority_update_schema = AuthorityUpdateSchema()
|
||||
authority_input_schema = AuthorityInputSchema()
|
||||
authority_output_schema = AuthorityOutputSchema()
|
||||
authorities_output_schema = AuthorityOutputSchema(many=True)
|
@ -9,69 +9,61 @@
|
||||
|
||||
"""
|
||||
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, active=None, roles=None):
|
||||
def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
"""
|
||||
Update a an authority with new values.
|
||||
|
||||
:param authority_id:
|
||||
:param roles: roles that are allowed to use this authority
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
authority = get(authority_id)
|
||||
|
||||
if roles:
|
||||
authority = database.update_list(authority, 'roles', Role, roles)
|
||||
authority.roles = roles
|
||||
|
||||
if active:
|
||||
authority.active = active
|
||||
|
||||
authority.description = description
|
||||
authority.owner = owner
|
||||
return database.update(authority)
|
||||
|
||||
|
||||
def create(kwargs):
|
||||
def mint(**kwargs):
|
||||
"""
|
||||
Create a new authority.
|
||||
Creates the authority based on the plugin provided.
|
||||
"""
|
||||
issuer = kwargs['plugin']['plugin_object']
|
||||
body, chain, roles = issuer.create_authority(kwargs)
|
||||
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title)
|
||||
return body, chain, roles
|
||||
|
||||
:rtype : Authority
|
||||
|
||||
def create_authority_roles(roles, owner, plugin_title):
|
||||
"""
|
||||
Creates all of the necessary authority roles.
|
||||
:param roles:
|
||||
:return:
|
||||
"""
|
||||
|
||||
issuer = plugins.get(kwargs.get('pluginName'))
|
||||
|
||||
kwargs['creator'] = g.current_user.email
|
||||
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
|
||||
|
||||
cert = Certificate(cert_body, chain=intermediate)
|
||||
cert.owner = kwargs['ownerEmail']
|
||||
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':
|
||||
@ -79,21 +71,51 @@ def create(kwargs):
|
||||
|
||||
role_objs.append(role)
|
||||
|
||||
authority = Authority(
|
||||
kwargs.get('caName'),
|
||||
kwargs['ownerEmail'],
|
||||
kwargs['pluginName'],
|
||||
cert_body,
|
||||
description=kwargs['caDescription'],
|
||||
chain=intermediate,
|
||||
roles=role_objs
|
||||
)
|
||||
# create an role for the owner and assign it
|
||||
owner_role = role_service.get_by_name(owner)
|
||||
if not owner_role:
|
||||
owner_role = role_service.create(
|
||||
owner,
|
||||
description="Auto generated role based on owner: {0}".format(owner)
|
||||
)
|
||||
|
||||
database.update(cert)
|
||||
role_objs.append(owner_role)
|
||||
return role_objs
|
||||
|
||||
|
||||
def create(**kwargs):
|
||||
"""
|
||||
Creates a new authority.
|
||||
"""
|
||||
kwargs['creator'] = g.user.email
|
||||
body, chain, roles = mint(**kwargs)
|
||||
|
||||
kwargs['body'] = body
|
||||
kwargs['chain'] = chain
|
||||
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
if kwargs['type'] == 'subca':
|
||||
description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
|
||||
authority is {1}.".format(kwargs.get('name'), kwargs.get('parent'))
|
||||
else:
|
||||
description = "This is the ROOT certificate for the {0} certificate authority.".format(
|
||||
kwargs.get('name')
|
||||
)
|
||||
|
||||
kwargs['description'] = description
|
||||
|
||||
cert = upload(**kwargs)
|
||||
kwargs['authority_certificate'] = cert
|
||||
|
||||
authority = Authority(**kwargs)
|
||||
authority = database.create(authority)
|
||||
g.user.authorities.append(authority)
|
||||
|
||||
g.current_user.authorities.append(authority)
|
||||
|
||||
metrics.send('authority_created', 'counter', 1, metric_tags=dict(owner=authority.owner))
|
||||
return authority
|
||||
|
||||
|
||||
@ -112,7 +134,6 @@ def get(authority_id):
|
||||
"""
|
||||
Retrieves an authority given it's ID
|
||||
|
||||
:rtype : Authority
|
||||
:param authority_id:
|
||||
:return:
|
||||
"""
|
||||
@ -124,7 +145,6 @@ def get_by_name(authority_name):
|
||||
Retrieves an authority given it's name.
|
||||
|
||||
:param authority_name:
|
||||
:rtype : Authority
|
||||
:return:
|
||||
"""
|
||||
return database.get(Authority, authority_name, field='name')
|
||||
@ -138,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):
|
||||
@ -155,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:
|
||||
@ -172,13 +183,8 @@ def render(args):
|
||||
if not g.current_user.is_admin:
|
||||
authority_ids = []
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
authority_ids.append(role.authority.id)
|
||||
for authority in role.authorities:
|
||||
authority_ids.append(authority.id)
|
||||
query = query.filter(Authority.id.in_(authority_ids))
|
||||
|
||||
query = database.find_all(query, Authority, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Authority, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Authority, args)
|
||||
|
@ -5,31 +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,
|
||||
'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)
|
||||
@ -41,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
|
||||
@ -65,28 +53,52 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair. format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -97,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
|
||||
|
||||
@ -112,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**:
|
||||
|
||||
@ -147,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):
|
||||
@ -205,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
|
||||
@ -229,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
|
||||
|
||||
@ -263,9 +294,42 @@ class Authorities(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"roles": [],
|
||||
"active": false
|
||||
}
|
||||
"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**:
|
||||
|
||||
@ -276,56 +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, location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json')
|
||||
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, active=args['active'], roles=args['roles'])
|
||||
return service.update(
|
||||
authority_id,
|
||||
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
|
||||
@ -349,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
|
||||
@ -367,10 +475,35 @@ class CertificateAuthority(AuthenticatedResource):
|
||||
"""
|
||||
cert = certificate_service.get(certificate_id)
|
||||
if not cert:
|
||||
return dict(message="Certificate not found"), 404
|
||||
return dict(message="Certificate not found."), 404
|
||||
|
||||
return cert.authority
|
||||
|
||||
|
||||
class AuthorityVisualizations(AuthenticatedResource):
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
{"name": "flare",
|
||||
"children": [
|
||||
{
|
||||
"name": "analytics",
|
||||
"children": [
|
||||
{
|
||||
"name": "cluster",
|
||||
"children": [
|
||||
{"name": "AgglomerativeCluster", "size": 3938},
|
||||
{"name": "CommunityStructure", "size": 3812},
|
||||
{"name": "HierarchicalCluster", "size": 6714},
|
||||
{"name": "MergeEdge", "size": 743}
|
||||
]
|
||||
}
|
||||
}
|
||||
]}
|
||||
"""
|
||||
authority = service.get(authority_id)
|
||||
return dict(name=authority.name, children=[{"name": c.name} for c in authority.certificates])
|
||||
|
||||
api.add_resource(AuthoritiesList, '/authorities', endpoint='authorities')
|
||||
api.add_resource(Authorities, '/authorities/<int:authority_id>', endpoint='authority')
|
||||
api.add_resource(AuthorityVisualizations, '/authorities/<int:authority_id>/visualize', endpoint='authority_visualizations')
|
||||
api.add_resource(CertificateAuthority, '/certificates/<int:certificate_id>/authority', endpoint='certificateAuthority')
|
||||
|
@ -6,247 +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 sqlalchemy_utils import EncryptedType
|
||||
|
||||
from lemur.utils import get_key
|
||||
from lemur.database import db
|
||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||
certificate_destination_associations, certificate_notification_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
|
||||
|
||||
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
|
||||
def get_or_increase_name(name):
|
||||
count = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).count()
|
||||
|
||||
if count >= 1:
|
||||
return name + '-' + str(count)
|
||||
|
||||
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.
|
||||
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
if san:
|
||||
t = SAN_NAMING_TEMPLATE
|
||||
else:
|
||||
t = DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
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 cert_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 cert_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 cert_get_serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
|
||||
|
||||
def cert_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(cert_get_domains(cert)) > 1:
|
||||
return True
|
||||
|
||||
|
||||
def cert_is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
domains = cert_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 cert_get_bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
:param cert:
|
||||
:return: Integer
|
||||
"""
|
||||
return cert.public_key().key_size
|
||||
|
||||
|
||||
def cert_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 cert_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 cert_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(EncryptedType(String, get_key))
|
||||
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')
|
||||
|
||||
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.bits = cert_get_bitstrength(cert)
|
||||
self.issuer = cert_get_issuer(cert)
|
||||
self.serial = cert_get_serial(cert)
|
||||
self.cn = cert_get_cn(cert)
|
||||
self.san = cert_is_san(cert)
|
||||
self.not_before = cert_get_not_before(cert)
|
||||
self.not_after = cert_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 cert_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
|
||||
@ -276,11 +131,47 @@ class Certificate(db.Model):
|
||||
"""
|
||||
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
@event.listens_for(Certificate.destinations, 'append')
|
||||
def update_destinations(target, value, initiator):
|
||||
"""
|
||||
Attempt to upload the new certificate to the new destination
|
||||
|
||||
:param target:
|
||||
:param value:
|
||||
:param 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')
|
||||
def update_replacement(target, value, initiator):
|
||||
"""
|
||||
When a certificate is marked as 'replaced' it is then marked as in-active
|
||||
|
||||
:param target:
|
||||
:param value:
|
||||
:param initiator:
|
||||
:return:
|
||||
"""
|
||||
value.active = False
|
||||
|
||||
|
||||
@event.listens_for(Certificate, 'before_update')
|
||||
def protect_active(mapper, connection, target):
|
||||
"""
|
||||
When a certificate has a replacement do not allow it to be marked as 'active'
|
||||
|
||||
:param connection:
|
||||
:param mapper:
|
||||
:param target:
|
||||
:return:
|
||||
"""
|
||||
if target.active:
|
||||
if target.replaced:
|
||||
raise Exception("Cannot mark certificate as active, certificate has been marked as replaced.")
|
||||
|
155
lemur/certificates/schemas.py
Normal file
@ -0,0 +1,155 @@
|
||||
"""
|
||||
.. module: lemur.certificates.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
from marshmallow import fields, validates_schema, post_load
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
||||
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema
|
||||
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.common import validators
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
|
||||
class CertificateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
|
||||
@post_load
|
||||
def default_notifications(self, data):
|
||||
if not data['notifications']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
return data
|
||||
|
||||
|
||||
class CertificateInputSchema(CertificateSchema):
|
||||
name = fields.String()
|
||||
common_name = fields.String(required=True, validate=validators.sensitive_domain)
|
||||
authority = fields.Nested(AssociatedAuthoritySchema, required=True)
|
||||
|
||||
validity_start = fields.DateTime()
|
||||
validity_end = fields.DateTime()
|
||||
validity_years = fields.Integer()
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
csr = fields.String(validate=validators.csr)
|
||||
|
||||
# certificate body fields
|
||||
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
||||
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
|
||||
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
|
||||
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
|
||||
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
@validates_schema
|
||||
def validate_dates(self, data):
|
||||
validators.dates(data)
|
||||
|
||||
|
||||
class CertificateEditInputSchema(CertificateSchema):
|
||||
active = fields.Boolean()
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
|
||||
class CertificateNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
status = fields.Boolean()
|
||||
creator = fields.Nested(UserNestedOutputSchema)
|
||||
issuer = fields.Nested(AuthorityNestedOutputSchema)
|
||||
|
||||
|
||||
class CertificateOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
deleted = fields.Boolean(default=False)
|
||||
description = fields.String()
|
||||
issuer = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
san = fields.Boolean()
|
||||
serial = fields.String()
|
||||
signing_algorithm = fields.String()
|
||||
status = fields.Boolean()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
domains = fields.Nested(DomainNestedOutputSchema, many=True)
|
||||
destinations = fields.Nested(DestinationNestedOutputSchema, many=True)
|
||||
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||
endpoints = fields.List(fields.Dict(), missing=[])
|
||||
|
||||
|
||||
class CertificateUploadInputSchema(CertificateSchema):
|
||||
name = fields.String()
|
||||
active = fields.Boolean(missing=True)
|
||||
|
||||
private_key = fields.String(validate=validators.private_key)
|
||||
body = fields.String(required=True, validate=validators.public_certificate)
|
||||
chain = fields.String(validate=validators.public_certificate) # TODO this could be multiple certificates
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
@validates_schema
|
||||
def keys(self, data):
|
||||
if data.get('destinations'):
|
||||
if not data.get('private_key'):
|
||||
raise ValidationError('Destinations require private key.')
|
||||
|
||||
|
||||
class CertificateExportInputSchema(LemurInputSchema):
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
||||
|
||||
certificate_input_schema = CertificateInputSchema()
|
||||
certificate_output_schema = CertificateOutputSchema()
|
||||
certificates_output_schema = CertificateOutputSchema(many=True)
|
||||
certificate_upload_input_schema = CertificateUploadInputSchema()
|
||||
certificate_export_input_schema = CertificateExportInputSchema()
|
||||
certificate_edit_input_schema = CertificateEditInputSchema()
|
@ -11,14 +11,17 @@ 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
|
||||
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.notifications.models import Notification
|
||||
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
|
||||
@ -76,48 +79,74 @@ def find_duplicates(cert_body):
|
||||
return Certificate.query.filter_by(body=cert_body).all()
|
||||
|
||||
|
||||
def update(cert_id, owner, description, active, destinations, notifications):
|
||||
def export(cert, export_plugin):
|
||||
"""
|
||||
Updates a certificate.
|
||||
Exports a certificate to the requested format. This format
|
||||
may be a binary format.
|
||||
|
||||
:param export_plugin:
|
||||
:param cert:
|
||||
: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, roles):
|
||||
"""
|
||||
Updates a certificate
|
||||
:param cert_id:
|
||||
:param owner:
|
||||
:param description:
|
||||
:param active:
|
||||
:param destinations:
|
||||
:param notifications:
|
||||
:param replaces:
|
||||
:return:
|
||||
"""
|
||||
cert = get(cert_id)
|
||||
cert.owner = owner
|
||||
cert.active = active
|
||||
cert.description = description
|
||||
|
||||
database.update_list(cert, 'notifications', Notification, notifications)
|
||||
database.update_list(cert, 'destinations', Destination, destinations)
|
||||
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)
|
||||
|
||||
csr, private_key = create_csr(issuer_options)
|
||||
# allow the CSR to be specified by the user
|
||||
if not kwargs.get('csr'):
|
||||
csr, private_key = create_csr(**kwargs)
|
||||
else:
|
||||
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):
|
||||
@ -134,61 +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'))
|
||||
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)
|
||||
|
||||
cert.description = kwargs.get('description')
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
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'))
|
||||
|
||||
# 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
|
||||
|
||||
@ -197,33 +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, 'notifications', Notification, kwargs.get('notifications'))
|
||||
|
||||
# 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)
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
return cert
|
||||
|
||||
|
||||
@ -247,6 +240,7 @@ def render(args):
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
|
||||
if 'issuer' in terms:
|
||||
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||
sub_query = database.session_query(Authority.id)\
|
||||
@ -261,10 +255,17 @@ def render(args):
|
||||
)
|
||||
return database.sort_and_page(query, Certificate, args)
|
||||
|
||||
if 'destination' in terms:
|
||||
elif 'destination' in terms:
|
||||
query = query.filter(Certificate.destinations.any(Destination.id == terms[1]))
|
||||
elif 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
query = query.filter(Certificate.active == terms[1])
|
||||
elif 'cn' in terms:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Certificate.cn.ilike('%{0}%'.format(terms[1])),
|
||||
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = database.filter(query, Certificate, terms)
|
||||
|
||||
@ -291,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
|
||||
@ -307,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']),
|
||||
@ -319,17 +320,18 @@ def create_csr(csr_config):
|
||||
x509.BasicConstraints(ca=False, path_length=None), critical=True,
|
||||
)
|
||||
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
if csr_config.get('extensions'):
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'sub_alt_names':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['name_type'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName(general_names), critical=True
|
||||
)
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName(general_names), critical=True
|
||||
)
|
||||
|
||||
# TODO support more CSR options, none of the authority plugins currently support these options
|
||||
# builder.add_extension(
|
||||
@ -379,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()
|
||||
@ -389,7 +391,7 @@ def create_csr(csr_config):
|
||||
encoding=serialization.Encoding.PEM
|
||||
)
|
||||
|
||||
return csr, pem
|
||||
return csr, private_key
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
@ -420,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]
|
||||
|
@ -5,14 +5,14 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import requests
|
||||
import subprocess
|
||||
from OpenSSL import crypto
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from flask import current_app
|
||||
from lemur.utils import mktempfile
|
||||
|
||||
|
||||
def ocsp_verify(cert_path, issuer_chain_path):
|
||||
@ -53,27 +53,18 @@ def crl_verify(cert_path):
|
||||
:return: True if certificate is valid, False otherwise
|
||||
:raise Exception: If certificate does not have CRL
|
||||
"""
|
||||
s = "(http(s)?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}/\S*?$)"
|
||||
regex = re.compile(s, re.MULTILINE)
|
||||
with open(cert_path, 'rt') as c:
|
||||
cert = x509.load_pem_x509_certificate(c.read(), default_backend())
|
||||
|
||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_path, 'rt').read())
|
||||
for x in range(x509.get_extension_count()):
|
||||
ext = x509.get_extension(x)
|
||||
if ext.get_short_name() == 'crlDistributionPoints':
|
||||
r = regex.search(ext.get_data())
|
||||
points = r.groups()
|
||||
break
|
||||
else:
|
||||
raise Exception("Certificate does not have a CRL distribution point")
|
||||
|
||||
for point in points:
|
||||
if point:
|
||||
response = requests.get(point)
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content)
|
||||
revoked = crl.get_revoked()
|
||||
for r in revoked:
|
||||
if x509.get_serial_number() == r.get_serial():
|
||||
return
|
||||
distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value
|
||||
for p in distribution_points:
|
||||
point = p.full_name[0].value
|
||||
response = requests.get(point)
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) # TODO this should be switched to cryptography when support exists
|
||||
revoked = crl.get_revoked()
|
||||
for r in revoked:
|
||||
if cert.serial == r.get_serial():
|
||||
return
|
||||
return True
|
||||
|
||||
|
||||
@ -99,22 +90,6 @@ def verify(cert_path, issuer_chain_path):
|
||||
raise Exception("Failed to verify")
|
||||
|
||||
|
||||
def make_tmp_file(string):
|
||||
"""
|
||||
Creates a temporary file for a given string
|
||||
|
||||
:param string:
|
||||
:return: Full file path to created file
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
m.update(string)
|
||||
hexdigest = m.hexdigest()
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), hexdigest)
|
||||
with open(path, 'w') as f:
|
||||
f.write(string)
|
||||
return path
|
||||
|
||||
|
||||
def verify_string(cert_string, issuer_string):
|
||||
"""
|
||||
Verify a certificate given only it's string value
|
||||
@ -123,13 +98,11 @@ def verify_string(cert_string, issuer_string):
|
||||
:param issuer_string:
|
||||
:return: True if valid, False otherwise
|
||||
"""
|
||||
cert_path = make_tmp_file(cert_string)
|
||||
issuer_path = make_tmp_file(issuer_string)
|
||||
status = verify(cert_path, issuer_path)
|
||||
remove_tmp_file(cert_path)
|
||||
remove_tmp_file(issuer_path)
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.write(cert_string)
|
||||
with mktempfile() as issuer_tmp:
|
||||
with open(issuer_tmp, 'w') as f:
|
||||
f.write(issuer_string)
|
||||
status = verify(cert_tmp, issuer_tmp)
|
||||
return status
|
||||
|
||||
|
||||
def remove_tmp_file(file_path):
|
||||
os.remove(file_path)
|
||||
|
@ -5,106 +5,37 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
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.certificates import service
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
|
||||
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, CertificatePermission
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \
|
||||
certificate_upload_input_schema, certificates_output_schema, certificate_export_input_schema, certificate_edit_input_schema
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
|
||||
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,
|
||||
'status': fields.String,
|
||||
'body': fields.String
|
||||
}
|
||||
|
||||
|
||||
def valid_authority(authority_options):
|
||||
"""
|
||||
Defends against invalid authorities
|
||||
|
||||
:param authority_options:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
name = authority_options['name']
|
||||
authority = Authority.query.filter(Authority.name == name).one()
|
||||
|
||||
if not authority:
|
||||
raise ValueError("Unable to find authority specified")
|
||||
|
||||
if not authority.active:
|
||||
raise ValueError("Selected authority [{0}] is not currently active".format(name))
|
||||
|
||||
return authority
|
||||
|
||||
|
||||
def pem_str(value, name):
|
||||
"""
|
||||
Used to validate that the given string is a PEM formatted string
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_certificate(bytes(value), default_backend())
|
||||
except Exception:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name))
|
||||
return value
|
||||
|
||||
|
||||
def private_key_str(value, name):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
serialization.load_pem_private_key(bytes(value), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name))
|
||||
return value
|
||||
|
||||
|
||||
class CertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /certificates
|
||||
@ -128,37 +59,65 @@ class CertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
@ -172,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
|
||||
|
||||
@ -187,47 +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",
|
||||
"authority": {
|
||||
"body": "-----BEGIN...",
|
||||
"name": "timetest2",
|
||||
"chain": "",
|
||||
"notBefore": "2015-06-05T15:20:59",
|
||||
"active": true,
|
||||
"id": 50,
|
||||
"notAfter": "2015-06-17T15:21:08",
|
||||
"description": "dsfdsf"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -238,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
|
||||
@ -266,53 +214,36 @@ 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('owner', type=str, 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')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('country', type=str, location='json')
|
||||
self.reqparse.add_argument('state', type=str, location='json')
|
||||
self.reqparse.add_argument('location', type=str, location='json')
|
||||
self.reqparse.add_argument('organization', type=str, location='json')
|
||||
self.reqparse.add_argument('organizationalUnit', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('commonName', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = args['authority']
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
role = role_service.get_by_name(data['authority'].owner)
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
roles = [x.name for x in data['authority'].roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority.id, roles)
|
||||
authority_permission = AuthorityPermission(data['authority'].id, roles)
|
||||
|
||||
if permission.can():
|
||||
return service.create(**args)
|
||||
if authority_permission.can():
|
||||
return service.create(**data)
|
||||
|
||||
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
||||
return dict(message="You are not authorized to use {0}".format(data['authority'].name)), 403
|
||||
|
||||
|
||||
class CertificatesUpload(AuthenticatedResource):
|
||||
""" Defines the 'certificates' upload endpoint """
|
||||
|
||||
def __init__(self):
|
||||
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
|
||||
|
||||
@ -332,7 +263,9 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"intermediateCert": "---Begin Public...",
|
||||
"privateKey": "---Begin Private..."
|
||||
"destinations": [],
|
||||
"notifications": []
|
||||
"notifications": [],
|
||||
"replacements": [],
|
||||
"name": "cert1"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -344,22 +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",
|
||||
"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
|
||||
@ -370,26 +332,19 @@ 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('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', 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):
|
||||
""" Defines the 'certificates' stats endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesStats, self).__init__()
|
||||
@ -446,7 +401,7 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = ViewKeyPermission(certificate_id, hasattr(role, 'id'))
|
||||
permission = ViewKeyPermission(certificate_id, getattr(role, 'name', None))
|
||||
|
||||
if permission.can():
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
@ -462,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
|
||||
@ -486,32 +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",
|
||||
"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
|
||||
|
||||
@ -529,7 +514,8 @@ class Certificates(AuthenticatedResource):
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false
|
||||
"notifications": [],
|
||||
"destinations": []
|
||||
"destinations": [],
|
||||
"replacements": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -541,47 +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=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, hasattr(role, 'id'))
|
||||
|
||||
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']
|
||||
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
|
||||
@ -589,11 +600,12 @@ class Certificates(AuthenticatedResource):
|
||||
|
||||
class NotificationCertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
|
||||
def __init__(self):
|
||||
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
|
||||
@ -617,37 +629,65 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
@ -662,9 +702,192 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
args['notification_id'] = notification_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class CertificatesReplacementsList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesReplacementsList, self).__init__()
|
||||
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/replacements
|
||||
|
||||
One certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/1/replacements HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"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
|
||||
|
||||
|
||||
class CertificateExport(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateExport, self).__init__()
|
||||
|
||||
@validate_schema(certificate_export_input_schema, None)
|
||||
def post(self, certificate_id, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates/1/export
|
||||
|
||||
Export a certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /certificates/1/export HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"export": {
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"available": ["Java Key Store (JKS)"],
|
||||
"required": true,
|
||||
"type": "select",
|
||||
"name": "type",
|
||||
"helpMessage": "Choose the format you wish to export",
|
||||
"value": "Java Key Store (JKS)"
|
||||
}, {
|
||||
"required": false,
|
||||
"type": "str",
|
||||
"name": "passphrase",
|
||||
"validation": "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,}$",
|
||||
"helpMessage": "If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8."
|
||||
}, {
|
||||
"required": false,
|
||||
"type": "str",
|
||||
"name": "alias",
|
||||
"helpMessage": "Enter the alias you wish to use for the keystore."
|
||||
}],
|
||||
"version": "unknown",
|
||||
"description": "Attempts to generate a JKS keystore or truststore",
|
||||
"title": "Java",
|
||||
"author": "Kevin Glisson",
|
||||
"type": "export",
|
||||
"slug": "java-export"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"data": "base64encodedstring",
|
||||
"passphrase": "UAWOHW#&@_%!tnwmxh832025",
|
||||
"extension": "jks"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
cert = service.get(certificate_id)
|
||||
|
||||
permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
|
||||
|
||||
options = data['plugin']['plugin_options']
|
||||
plugin = data['plugin']['plugin_object']
|
||||
|
||||
if plugin.requires_key:
|
||||
if permission.can():
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||
else:
|
||||
return dict(message='You are not authorized to export this certificate'), 403
|
||||
else:
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||
|
||||
# we take a hit in message size when b64 encoding
|
||||
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data))
|
||||
|
||||
|
||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
||||
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
||||
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
||||
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates')
|
||||
api.add_resource(CertificateExport, '/certificates/<int:certificate_id>/export', endpoint='exportCertificate')
|
||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
|
||||
endpoint='notificationCertificates')
|
||||
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',
|
||||
endpoint='replacements')
|
||||
|
175
lemur/common/defaults.py
Normal file
@ -0,0 +1,175 @@
|
||||
import sys
|
||||
from flask import current_app
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
|
||||
def parse_certificate(body):
|
||||
if sys.version_info >= (3, 0):
|
||||
return x509.load_pem_x509_certificate(body, default_backend())
|
||||
else:
|
||||
return x509.load_pem_x509_certificate(bytes(body), default_backend())
|
||||
|
||||
|
||||
def certificate_name(common_name, issuer, not_before, not_after, san):
|
||||
"""
|
||||
Create a name for our certificate. A naming standard
|
||||
is based on a series of templates. The name includes
|
||||
useful information such as Common Name, Validation dates,
|
||||
and Issuer.
|
||||
|
||||
:param san:
|
||||
:param common_name:
|
||||
:param not_after:
|
||||
:param issuer:
|
||||
:param not_before:
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
if san:
|
||||
t = SAN_NAMING_TEMPLATE
|
||||
else:
|
||||
t = DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
temp = t.format(
|
||||
subject=common_name,
|
||||
issuer=issuer,
|
||||
not_before=not_before.strftime('%Y%m%d'),
|
||||
not_after=not_after.strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
disallowed_chars = disallowed_chars.replace("-", "")
|
||||
disallowed_chars = disallowed_chars.replace(".", "")
|
||||
temp = temp.replace('*', "WILDCARD")
|
||||
|
||||
for c in disallowed_chars:
|
||||
temp = temp.replace(c, "")
|
||||
|
||||
# white space is silly too
|
||||
return temp.replace(" ", "-")
|
||||
|
||||
|
||||
def signing_algorithm(cert):
|
||||
return cert.signature_hash_algorithm.name
|
||||
|
||||
|
||||
def common_name(cert):
|
||||
"""
|
||||
Attempts to get a sane common name from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Common name or None
|
||||
"""
|
||||
return cert.subject.get_attributes_for_oid(
|
||||
x509.OID_COMMON_NAME
|
||||
)[0].value.strip()
|
||||
|
||||
|
||||
def domains(cert):
|
||||
"""
|
||||
Attempts to get an domains listed in a certificate.
|
||||
If 'subjectAltName' extension is not available we simply
|
||||
return the common name.
|
||||
|
||||
:param cert:
|
||||
:return: List of domains
|
||||
"""
|
||||
domains = []
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
|
||||
entries = ext.value.get_values_for_type(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
|
||||
|
||||
def san(cert):
|
||||
"""
|
||||
Determines if a given certificate is a SAN certificate.
|
||||
SAN certificates are simply certificates that cover multiple domains.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
if len(domains(cert)) > 1:
|
||||
return True
|
||||
|
||||
|
||||
def is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
d = domains(cert)
|
||||
if len(d) == 1 and d[0][0:1] == "*":
|
||||
return True
|
||||
|
||||
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
|
||||
return True
|
||||
|
||||
|
||||
def bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
:param cert:
|
||||
:return: Integer
|
||||
"""
|
||||
return cert.public_key().key_size
|
||||
|
||||
|
||||
def issuer(cert):
|
||||
"""
|
||||
Gets a sane issuer from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Issuer
|
||||
"""
|
||||
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
try:
|
||||
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
|
||||
for c in delchars:
|
||||
issuer = issuer.replace(c, "")
|
||||
return issuer
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to get issuer! {0}".format(e))
|
||||
|
||||
|
||||
def not_before(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_before' field.
|
||||
This field denotes the first date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_before
|
||||
|
||||
|
||||
def not_after(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_after' field.
|
||||
This field denotes the last date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_after
|
@ -22,7 +22,8 @@ class InstanceManager(object):
|
||||
|
||||
def add(self, class_path):
|
||||
self.cache = None
|
||||
self.class_list.append(class_path)
|
||||
if class_path not in self.class_list:
|
||||
self.class_list.append(class_path)
|
||||
|
||||
def remove(self, class_path):
|
||||
self.cache = None
|
||||
|
153
lemur/common/schema.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""
|
||||
.. module: lemur.common.schema
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import request, current_app
|
||||
|
||||
from sqlalchemy.orm.collections import InstrumentedList
|
||||
|
||||
from marshmallow import Schema, post_dump, pre_load, pre_dump
|
||||
from inflection import camelize, underscore
|
||||
|
||||
|
||||
class LemurSchema(Schema):
|
||||
"""
|
||||
Base schema from which all grouper schema's inherit
|
||||
"""
|
||||
__envelope__ = True
|
||||
|
||||
def under(self, data, many=None):
|
||||
items = []
|
||||
if many:
|
||||
for i in data:
|
||||
items.append(
|
||||
{underscore(key): value for key, value in i.items()}
|
||||
)
|
||||
return items
|
||||
return {
|
||||
underscore(key): value
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
def camel(self, data, many=None):
|
||||
items = []
|
||||
if many:
|
||||
for i in data:
|
||||
items.append(
|
||||
{camelize(key, uppercase_first_letter=False): value for key, value in i.items()}
|
||||
)
|
||||
return items
|
||||
return {
|
||||
camelize(key, uppercase_first_letter=False): value
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
def wrap_with_envelope(self, data, many):
|
||||
if many:
|
||||
if 'total' in self.context.keys():
|
||||
return dict(total=self.context['total'], items=data)
|
||||
return data
|
||||
|
||||
|
||||
class LemurInputSchema(LemurSchema):
|
||||
@pre_load(pass_many=True)
|
||||
def preprocess(self, data, many):
|
||||
return self.under(data, many=many)
|
||||
|
||||
|
||||
class LemurOutputSchema(LemurSchema):
|
||||
@pre_load(pass_many=True)
|
||||
def preprocess(self, data, many):
|
||||
if many:
|
||||
data = self.unwrap_envelope(data, many)
|
||||
return self.under(data, many=many)
|
||||
|
||||
@pre_dump(pass_many=True)
|
||||
def unwrap_envelope(self, data, many):
|
||||
if many:
|
||||
if data:
|
||||
if isinstance(data, InstrumentedList) or isinstance(data, list):
|
||||
self.context['total'] = len(data)
|
||||
return data
|
||||
else:
|
||||
self.context['total'] = data['total']
|
||||
else:
|
||||
self.context['total'] = 0
|
||||
data = {'items': []}
|
||||
|
||||
return data['items']
|
||||
|
||||
return data
|
||||
|
||||
@post_dump(pass_many=True)
|
||||
def post_process(self, data, many):
|
||||
if data:
|
||||
data = self.camel(data, many=many)
|
||||
if self.__envelope__:
|
||||
return self.wrap_with_envelope(data, many=many)
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
def format_errors(messages):
|
||||
errors = {}
|
||||
for k, v in messages.items():
|
||||
key = camelize(k, uppercase_first_letter=False)
|
||||
if isinstance(v, dict):
|
||||
errors[key] = format_errors(v)
|
||||
elif isinstance(v, list):
|
||||
errors[key] = v[0]
|
||||
return errors
|
||||
|
||||
|
||||
def wrap_errors(messages):
|
||||
errors = dict(message='Validation Error.')
|
||||
if messages.get('_schema'):
|
||||
errors['reasons'] = {'Schema': {'rule': messages['_schema']}}
|
||||
else:
|
||||
errors['reasons'] = format_errors(messages)
|
||||
return errors
|
||||
|
||||
|
||||
def validate_schema(input_schema, output_schema):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if input_schema:
|
||||
if request.get_json():
|
||||
request_data = request.get_json()
|
||||
else:
|
||||
request_data = request.args
|
||||
|
||||
data, errors = input_schema.load(request_data)
|
||||
|
||||
if errors:
|
||||
return wrap_errors(errors), 400
|
||||
|
||||
kwargs['data'] = data
|
||||
|
||||
try:
|
||||
resp = f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return dict(message=e.message), 500
|
||||
|
||||
if isinstance(resp, tuple):
|
||||
return resp[0], resp[1]
|
||||
|
||||
if not resp:
|
||||
return dict(message="No data found"), 404
|
||||
|
||||
if output_schema:
|
||||
data = output_schema.dump(resp)
|
||||
return data.data, 200
|
||||
return resp, 200
|
||||
|
||||
return decorated_function
|
||||
return decorator
|
@ -63,9 +63,9 @@ class marshal_items(object):
|
||||
if hasattr(e, 'data'):
|
||||
return {'message': e.data['message']}, 400
|
||||
else:
|
||||
return {'message': 'unknown'}, 400
|
||||
return {'message': {'exception': 'unknown'}}, 400
|
||||
else:
|
||||
return {'message': str(e)}, 400
|
||||
return {'message': {'exception': str(e)}}, 400
|
||||
return wrapper
|
||||
|
||||
|
||||
|
120
lemur/common/validators.py
Normal file
@ -0,0 +1,120 @@
|
||||
|
||||
import arrow
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from lemur.domains import service as domain_service
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
|
||||
|
||||
def public_certificate(body):
|
||||
"""
|
||||
Determines if specified string is valid public certificate.
|
||||
|
||||
:param body:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_certificate(bytes(body), default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('Public certificate presented is not valid.')
|
||||
|
||||
|
||||
def private_key(key):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param key:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
serialization.load_pem_private_key(bytes(key), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('Private key presented is not valid.')
|
||||
|
||||
|
||||
def sensitive_domain(domain):
|
||||
"""
|
||||
Determines if domain has been marked as sensitive.
|
||||
:param domain:
|
||||
:return:
|
||||
"""
|
||||
domains = domain_service.get_by_name(domain)
|
||||
for domain in domains:
|
||||
# we only care about non-admins
|
||||
if not SensitiveDomainPermission().can():
|
||||
if domain.sensitive:
|
||||
raise ValidationError(
|
||||
'Domain {0} has been marked as sensitive, contact and administrator \
|
||||
to issue the certificate.'.format(domain))
|
||||
|
||||
|
||||
def oid_type(oid_type):
|
||||
"""
|
||||
Determines if the specified oid type is valid.
|
||||
:param oid_type:
|
||||
:return:
|
||||
"""
|
||||
valid_types = ['b64asn1', 'string', 'ia5string']
|
||||
if oid_type.lower() not in [o_type.lower() for o_type in valid_types]:
|
||||
raise ValidationError('Invalid Oid Type: {0} choose from {1}'.format(oid_type, ",".join(valid_types)))
|
||||
|
||||
|
||||
def sub_alt_type(alt_type):
|
||||
"""
|
||||
Determines if the specified subject alternate type is valid.
|
||||
:param alt_type:
|
||||
:return:
|
||||
"""
|
||||
valid_types = ['DNSName', 'IPAddress', 'uniFormResourceIdentifier', 'directoryName', 'rfc822Name', 'registrationID',
|
||||
'otherName', 'x400Address', 'EDIPartyName']
|
||||
if alt_type.lower() not in [a_type.lower() for a_type in valid_types]:
|
||||
raise ValidationError('Invalid SubAltName Type: {0} choose from {1}'.format(type, ",".join(valid_types)))
|
||||
|
||||
|
||||
def csr(data):
|
||||
"""
|
||||
Determines if the CSR is valid.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_csr(bytes(data), default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('CSR presented is not valid.')
|
||||
|
||||
|
||||
def dates(data):
|
||||
if not data.get('validity_start') and data.get('validity_end'):
|
||||
raise ValidationError('If validity start is specified so must validity end.')
|
||||
|
||||
if not data.get('validity_end') and data.get('validity_start'):
|
||||
raise ValidationError('If validity end is specified so must validity start.')
|
||||
|
||||
if data.get('validity_end') and data.get('validity_years'):
|
||||
raise ValidationError('Cannot specify both validity end and validity years.')
|
||||
|
||||
if data.get('validity_start') and data.get('validity_end'):
|
||||
if not data['validity_start'] < data['validity_end']:
|
||||
raise ValidationError('Validity start must be before validity end.')
|
||||
|
||||
if data.get('authority'):
|
||||
if data.get('validity_start').replace(hour=0, minute=0, second=0, tzinfo=None) < data['authority'].authority_certificate.not_before.replace(hour=0, minute=0, second=0):
|
||||
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
|
||||
|
||||
if data.get('validity_end').replace(hour=0, minute=0, second=0, tzinfo=None) > data['authority'].authority_certificate.not_after.replace(hour=0, minute=0, second=0):
|
||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
||||
|
||||
if data.get('validity_years'):
|
||||
now = arrow.utcnow()
|
||||
end = now.replace(years=+data['validity_years'])
|
||||
|
||||
if data.get('authority'):
|
||||
if now.naive < data['authority'].authority_certificate.not_before:
|
||||
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
|
||||
|
||||
if end.naive > data['authority'].authority_certificate.not_after:
|
||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
@ -9,10 +9,11 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.sql import and_, or_
|
||||
from sqlalchemy.orm import make_transient
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
|
||||
from lemur.extensions import db
|
||||
from lemur.exceptions import AttrNotFound, DuplicateError
|
||||
@ -126,8 +127,7 @@ def get(model, value, field="id"):
|
||||
query = session_query(model)
|
||||
try:
|
||||
return query.filter(getattr(model, field) == value).one()
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
except NoResultFound as e:
|
||||
return
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -256,6 +253,18 @@ def update_list(model, model_attr, item_model, items):
|
||||
return model
|
||||
|
||||
|
||||
def clone(model):
|
||||
"""
|
||||
Clones the given model and removes it's primary key
|
||||
:param model:
|
||||
:return:
|
||||
"""
|
||||
db.session.expunge(model)
|
||||
make_transient(model)
|
||||
model.id = None
|
||||
return model
|
||||
|
||||
|
||||
def sort_and_page(query, model, args):
|
||||
"""
|
||||
Helper that allows us to combine sorting and paging
|
||||
@ -275,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)
|
||||
|
63
lemur/defaults/views.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
.. module: lemur.status.views
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app, Blueprint
|
||||
from flask.ext.restful import Api
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
|
||||
mod = Blueprint('default', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class LemurDefaults(AuthenticatedResource):
|
||||
""" Defines the 'defaults' endpoint """
|
||||
def __init__(self):
|
||||
super(LemurDefaults)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /defaults
|
||||
|
||||
Returns defaults needed to generate CSRs
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /defaults HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return dict(
|
||||
country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
|
||||
state=current_app.config.get('LEMUR_DEFAULT_STATE'),
|
||||
location=current_app.config.get('LEMUR_DEFAULT_LOCATION'),
|
||||
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT')
|
||||
)
|
||||
|
||||
api.add_resource(LemurDefaults, '/defaults', endpoint='default')
|
42
lemur/destinations/schemas.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
.. module: lemur.destinations.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
||||
from marshmallow import fields, post_dump
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema
|
||||
|
||||
|
||||
class DestinationInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String(required=True)
|
||||
description = fields.String(required=True)
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginInputSchema, required=True)
|
||||
|
||||
|
||||
class DestinationOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
options = fields.List(fields.Dict())
|
||||
|
||||
@post_dump
|
||||
def fill_object(self, data):
|
||||
data['plugin']['pluginOptions'] = data['options']
|
||||
return data
|
||||
|
||||
|
||||
class DestinationNestedOutputSchema(DestinationOutputSchema):
|
||||
__envelope__ = False
|
||||
|
||||
|
||||
destination_input_schema = DestinationInputSchema()
|
||||
destinations_output_schema = DestinationOutputSchema(many=True)
|
||||
destination_output_schema = DestinationOutputSchema()
|
@ -8,6 +8,7 @@
|
||||
from sqlalchemy import func
|
||||
|
||||
from lemur import database
|
||||
from lemur.models import certificate_destination_associations
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
@ -85,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)
|
||||
|
||||
@ -102,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):
|
||||
@ -117,10 +109,9 @@ def stats(**kwargs):
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
attr = getattr(Destination, kwargs.get('metric'))
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
items = database.db.session.query(Destination.label, func.count(certificate_destination_associations.c.certificate_id))\
|
||||
.join(certificate_destination_associations)\
|
||||
.group_by(Destination.label).all()
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
|
@ -7,34 +7,28 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.destinations import service
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.destinations.schemas import destinations_output_schema, destination_input_schema, destination_output_schema
|
||||
|
||||
|
||||
mod = Blueprint('destinations', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'description': fields.String,
|
||||
'destinationOptions': fields.Raw(attribute='options'),
|
||||
'pluginName': fields.String(attribute='plugin_name'),
|
||||
'label': fields.String,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class DestinationsList(AuthenticatedResource):
|
||||
""" Defines the 'destinations' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(DestinationsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destinations_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /destinations
|
||||
@ -58,32 +52,40 @@ class DestinationsList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
}
|
||||
],
|
||||
"items": [{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -92,8 +94,8 @@ class DestinationsList(AuthenticatedResource):
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(destination_input_schema, destination_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /destinations
|
||||
|
||||
@ -108,20 +110,30 @@ class DestinationsList(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -133,20 +145,30 @@ class DestinationsList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:arg label: human readable account label
|
||||
@ -154,12 +176,7 @@ class DestinationsList(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.create(data['label'], data['plugin']['slug'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
|
||||
class Destinations(AuthenticatedResource):
|
||||
@ -167,7 +184,7 @@ class Destinations(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Destinations, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destination_output_schema)
|
||||
def get(self, destination_id):
|
||||
"""
|
||||
.. http:get:: /destinations/1
|
||||
@ -191,20 +208,30 @@ class Destinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -213,8 +240,8 @@ class Destinations(AuthenticatedResource):
|
||||
return service.get(destination_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, destination_id):
|
||||
@validate_schema(destination_input_schema, destination_output_schema)
|
||||
def put(self, destination_id, data=None):
|
||||
"""
|
||||
.. http:put:: /destinations/1
|
||||
|
||||
@ -228,23 +255,35 @@ class Destinations(AuthenticatedResource):
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -254,20 +293,30 @@ class Destinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:arg accountNumber: aws account number
|
||||
@ -276,12 +325,7 @@ class Destinations(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(destination_id, args['label'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.update(destination_id, data['label'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, destination_id):
|
||||
@ -294,7 +338,7 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateDestinations, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destination_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/destinations
|
||||
@ -318,32 +362,40 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
}
|
||||
],
|
||||
"items": [{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
@ -7,7 +7,7 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
@ -16,11 +16,4 @@ class Domain(db.Model):
|
||||
__tablename__ = 'domains'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(256))
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
blob['certificates'] = [x.id for x in self.certificate]
|
||||
return blob
|
||||
sensitive = Column(Boolean, default=False)
|
||||
|
35
lemur/domains/schemas.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
.. module: lemur.domains.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import AssociatedCertificateSchema
|
||||
|
||||
# from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
|
||||
|
||||
class DomainInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String(required=True)
|
||||
sensitive = fields.Boolean()
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class DomainOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
sensitive = fields.Boolean()
|
||||
# certificates = fields.Nested(CertificateNestedOutputSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class DomainNestedOutputSchema(DomainOutputSchema):
|
||||
__envelope__ = False
|
||||
|
||||
|
||||
domain_input_schema = DomainInputSchema()
|
||||
domain_output_schema = DomainOutputSchema()
|
||||
domains_output_schema = DomainOutputSchema(many=True)
|
@ -32,6 +32,43 @@ def get_all():
|
||||
return database.find_all(query, Domain, {}).all()
|
||||
|
||||
|
||||
def get_by_name(name):
|
||||
"""
|
||||
Fetches domain by it's name
|
||||
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
return database.get_all(Domain, name, field="name").all()
|
||||
|
||||
|
||||
def create(name, sensitive):
|
||||
"""
|
||||
Create a new domain
|
||||
|
||||
:param name:
|
||||
:param sensitive:
|
||||
:return:
|
||||
"""
|
||||
domain = Domain(name=name, sensitive=sensitive)
|
||||
return database.create(domain)
|
||||
|
||||
|
||||
def update(domain_id, name, sensitive):
|
||||
"""
|
||||
Update an existing domain
|
||||
|
||||
:param domain_id:
|
||||
:param name:
|
||||
:param sensitive:
|
||||
:return:
|
||||
"""
|
||||
domain = get(domain_id)
|
||||
domain.name = name
|
||||
domain.sensitive = sensitive
|
||||
database.update(domain)
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper to parse REST Api requests
|
||||
@ -40,11 +77,6 @@ def render(args):
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Domain).join(Certificate, Domain.certificate)
|
||||
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -55,9 +87,4 @@ def render(args):
|
||||
if certificate_id:
|
||||
query = query.filter(Certificate.id == certificate_id)
|
||||
|
||||
query = database.find_all(query, Domain, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Domain, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Domain, args)
|
||||
|
@ -8,17 +8,16 @@
|
||||
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.domains import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
FIELDS = {
|
||||
'id': fields.Integer,
|
||||
'name': fields.String
|
||||
}
|
||||
from lemur.domains.schemas import domain_input_schema, domain_output_schema, domains_output_schema
|
||||
|
||||
mod = Blueprint('domains', __name__)
|
||||
api = Api(mod)
|
||||
@ -29,7 +28,7 @@ class DomainsList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(DomainsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domains_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /domains
|
||||
@ -57,10 +56,12 @@ class DomainsList(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
"sensitive": false
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
@ -68,9 +69,9 @@ class DomainsList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -79,13 +80,58 @@ class DomainsList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@validate_schema(domain_input_schema, domain_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /domains
|
||||
|
||||
The current domain list
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.create(data['name'], data['sensitive'])
|
||||
|
||||
|
||||
class Domains(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Domains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domain_output_schema)
|
||||
def get(self, domain_id):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
@ -111,6 +157,7 @@ class Domains(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -119,13 +166,56 @@ class Domains(AuthenticatedResource):
|
||||
"""
|
||||
return service.get(domain_id)
|
||||
|
||||
@validate_schema(domain_input_schema, domain_output_schema)
|
||||
def put(self, domain_id, data=None):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
|
||||
update one domain
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /domains HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
if SensitiveDomainPermission().can():
|
||||
return service.update(domain_id, data['name'], data['sensitive'])
|
||||
|
||||
return dict(message='You are not authorized to modify this domain'), 403
|
||||
|
||||
|
||||
class CertificateDomains(AuthenticatedResource):
|
||||
""" Defines the 'domains' endpoint """
|
||||
def __init__(self):
|
||||
super(CertificateDomains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domains_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/domains
|
||||
@ -153,10 +243,12 @@ class CertificateDomains(AuthenticatedResource):
|
||||
{
|
||||
"id": 1,
|
||||
"name": "www.example.com",
|
||||
"sensitive": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "www.example2.com",
|
||||
"sensitive": false
|
||||
}
|
||||
]
|
||||
"total": 2
|
||||
@ -164,9 +256,9 @@ class CertificateDomains(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
@ -3,17 +3,20 @@
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
|
||||
from flask.ext.migrate import Migrate
|
||||
from flask_migrate import Migrate
|
||||
migrate = Migrate()
|
||||
|
||||
from flask.ext.bcrypt import Bcrypt
|
||||
from flask_bcrypt import Bcrypt
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
from flask.ext.principal import Principal
|
||||
from flask_principal import Principal
|
||||
principal = Principal()
|
||||
|
||||
from flask_mail import Mail
|
||||
smtp_mail = Mail()
|
||||
|
||||
from lemur.metrics import Metrics
|
||||
metrics = Metrics()
|
||||
|
@ -19,7 +19,7 @@ from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask
|
||||
from lemur.common.health import mod as health
|
||||
from lemur.extensions import db, migrate, principal, smtp_mail
|
||||
from lemur.extensions import db, migrate, principal, smtp_mail, metrics
|
||||
|
||||
|
||||
DEFAULT_BLUEPRINTS = (
|
||||
@ -112,6 +112,7 @@ def configure_extensions(app):
|
||||
migrate.init_app(app, db)
|
||||
principal.init_app(app)
|
||||
smtp_mail.init_app(app)
|
||||
metrics.init_app(app)
|
||||
|
||||
|
||||
def configure_blueprints(app, blueprints):
|
||||
|
472
lemur/manage.py
@ -1,7 +1,12 @@
|
||||
from __future__ import unicode_literals # at top of module
|
||||
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import arrow
|
||||
import requests
|
||||
import json
|
||||
from gunicorn.config import make_settings
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
@ -20,8 +25,12 @@ from lemur.certificates import service as cert_service
|
||||
from lemur.sources import service as source_service
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.certificates.service import get_name_from_arn
|
||||
from lemur.certificates.verify import verify_string
|
||||
from lemur.sources.service import sync
|
||||
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
from lemur.sources.service import sync as source_sync
|
||||
|
||||
from lemur import create_app
|
||||
|
||||
@ -68,7 +77,7 @@ SECRET_KEY = '{flask_secret_key}'
|
||||
|
||||
# You should consider storing these separately from your config
|
||||
LEMUR_TOKEN_SECRET = '{secret_token}'
|
||||
LEMUR_ENCRYPTION_KEY = '{encryption_key}'
|
||||
LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
@ -77,7 +86,17 @@ LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
||||
LEMUR_EMAIL = ''
|
||||
LEMUR_SECURITY_TEAM_EMAIL = []
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
|
||||
# Certificate Defaults
|
||||
|
||||
LEMUR_DEFAULT_COUNTRY = ''
|
||||
LEMUR_DEFAULT_STATE = ''
|
||||
LEMUR_DEFAULT_LOCATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
|
||||
|
||||
# Authentication Providers
|
||||
ACTIVE_PROVIDERS = []
|
||||
|
||||
# Logging
|
||||
|
||||
@ -100,13 +119,6 @@ SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
|
||||
# These will be dependent on which 3rd party that Lemur is
|
||||
# configured to use.
|
||||
|
||||
# CLOUDCA_URL = ''
|
||||
# CLOUDCA_PEM_PATH = ''
|
||||
# CLOUDCA_BUNDLE = ''
|
||||
|
||||
# number of years to issue if not specified
|
||||
# CLOUDCA_DEFAULT_VALIDITY = 2
|
||||
|
||||
# VERISIGN_URL = ''
|
||||
# VERISIGN_PEM_PATH = ''
|
||||
# VERISIGN_FIRST_NAME = ''
|
||||
@ -136,12 +148,15 @@ def check_revoked():
|
||||
as `unknown`.
|
||||
"""
|
||||
for cert in cert_service.get_all_certs():
|
||||
if cert.chain:
|
||||
status = verify_string(cert.body, cert.chain)
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
try:
|
||||
if cert.chain:
|
||||
status = verify_string(cert.body, cert.chain)
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
|
||||
cert.status = 'valid' if status else "invalid"
|
||||
cert.status = 'valid' if status else 'invalid'
|
||||
except Exception as e:
|
||||
cert.status = 'unknown'
|
||||
database.update(cert)
|
||||
|
||||
|
||||
@ -163,7 +178,9 @@ def generate_settings():
|
||||
settings file.
|
||||
"""
|
||||
output = CONFIG_TEMPLATE.format(
|
||||
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
# we use Fernet.generate_key to make sure that the key length is
|
||||
# compatible with Fernet
|
||||
encryption_key=Fernet.generate_key(),
|
||||
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
)
|
||||
@ -171,19 +188,18 @@ def generate_settings():
|
||||
return output
|
||||
|
||||
|
||||
@manager.option('-s', '--sources', dest='labels', default='', required=False)
|
||||
@manager.option('-l', '--list', dest='view', default=False, required=False)
|
||||
def sync_sources(labels, view):
|
||||
@manager.option('-s', '--sources', dest='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
|
||||
information it discovers.
|
||||
"""
|
||||
if view:
|
||||
if not labels:
|
||||
sys.stdout.write("Active\tLabel\tDescription\n")
|
||||
for source in source_service.get_all():
|
||||
sys.stdout.write(
|
||||
"[{active}]\t{label}\t{description}!\n".format(
|
||||
"{active}\t{label}\t{description}!\n".format(
|
||||
label=source.label,
|
||||
description=source.description,
|
||||
active=source.active
|
||||
@ -198,13 +214,14 @@ def sync_sources(labels, view):
|
||||
try:
|
||||
sync_lock.acquire(timeout=10) # wait up to 10 seconds
|
||||
|
||||
if labels:
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
else:
|
||||
sys.stdout.write("[+] Starting to sync ALL sources!\n")
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
|
||||
if labels[0] == 'all':
|
||||
source_sync()
|
||||
else:
|
||||
source_sync(labels=labels)
|
||||
|
||||
sync(labels=labels)
|
||||
sys.stdout.write(
|
||||
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
||||
time=(time.time() - start_time)
|
||||
@ -248,18 +265,23 @@ class InitializeApp(Command):
|
||||
Additionally a Lemur user will be created as a default user
|
||||
and be used when certificates are discovered by Lemur.
|
||||
"""
|
||||
def run(self):
|
||||
option_list = (
|
||||
Option('-p', '--password', dest='password'),
|
||||
)
|
||||
|
||||
def run(self, password):
|
||||
create()
|
||||
user = user_service.get_by_username("lemur")
|
||||
|
||||
if not user:
|
||||
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
if not password:
|
||||
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
||||
password = prompt_pass("Password")
|
||||
password1 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
if password != password1:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
role = role_service.get_by_name('admin')
|
||||
|
||||
@ -270,16 +292,16 @@ class InitializeApp(Command):
|
||||
role = role_service.create('admin', description='this is the lemur administrator role')
|
||||
sys.stdout.write("[+] Created 'admin' role\n")
|
||||
|
||||
user_service.create("lemur", password1, 'lemur@nobody', True, None, [role])
|
||||
user_service.create("lemur", password, 'lemur@nobody', True, None, [role])
|
||||
sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n")
|
||||
|
||||
else:
|
||||
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
|
||||
|
||||
sys.stdout.write("[+] Creating expiration email notifications!\n")
|
||||
sys.stdout.write("[!] Using {recipients} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n")
|
||||
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
|
||||
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
|
||||
sys.stdout.write(
|
||||
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
|
||||
num=len(intervals),
|
||||
@ -295,13 +317,13 @@ 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),
|
||||
Option('-e', '--email', dest='email', required=True),
|
||||
Option('-a', '--active', dest='active', default=True),
|
||||
Option('-r', '--roles', dest='roles', default=[])
|
||||
Option('-r', '--roles', dest='roles', action='append', default=[])
|
||||
)
|
||||
|
||||
def run(self, username, email, active, roles):
|
||||
@ -311,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):
|
||||
@ -369,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
|
||||
@ -480,6 +530,337 @@ def unlock(path=None):
|
||||
sys.stdout.write("[+] Keys have been unencrypted!\n")
|
||||
|
||||
|
||||
def unicode_(data):
|
||||
import sys
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
return data.decode('UTF-8')
|
||||
return data
|
||||
|
||||
|
||||
class RotateELBs(Command):
|
||||
"""
|
||||
Rotates existing certificates to a new one on an ELB
|
||||
"""
|
||||
option_list = (
|
||||
Option('-e', '--elb-list', dest='elb_list', required=True),
|
||||
Option('-p', '--chain-path', dest='chain_path'),
|
||||
Option('-c', '--cert-name', dest='cert_name'),
|
||||
Option('-a', '--cert-prefix', dest='cert_prefix'),
|
||||
Option('-d', '--description', dest='description')
|
||||
)
|
||||
|
||||
def run(self, elb_list, chain_path, cert_name, cert_prefix, description):
|
||||
|
||||
for e in open(elb_list, 'r').readlines():
|
||||
elb_name, account_id, region, from_port, to_port, protocol = e.strip().split(',')
|
||||
|
||||
if cert_name:
|
||||
arn = "arn:aws:iam::{0}:server-certificate/{1}".format(account_id, cert_name)
|
||||
|
||||
else:
|
||||
# if no cert name is provided we need to discover it
|
||||
listeners = elb.get_listeners(account_id, region, elb_name)
|
||||
|
||||
# get the listener we care about
|
||||
for listener in listeners:
|
||||
if listener[0] == int(from_port) and listener[1] == int(to_port):
|
||||
arn = listener[4]
|
||||
name = get_name_from_arn(arn)
|
||||
certificate = cert_service.get_by_name(name)
|
||||
break
|
||||
else:
|
||||
sys.stdout.write("[-] Could not find ELB {0}".format(elb_name))
|
||||
continue
|
||||
|
||||
if not certificate:
|
||||
sys.stdout.write("[-] Could not find certificate {0} in Lemur".format(name))
|
||||
continue
|
||||
|
||||
dests = []
|
||||
for d in certificate.destinations:
|
||||
dests.append({'id': d.id})
|
||||
|
||||
nots = []
|
||||
for n in certificate.notifications:
|
||||
nots.append({'id': n.id})
|
||||
|
||||
new_certificate = database.clone(certificate)
|
||||
|
||||
if cert_prefix:
|
||||
new_certificate.name = "{0}-{1}".format(cert_prefix, new_certificate.name)
|
||||
|
||||
new_certificate.chain = open(chain_path, 'r').read()
|
||||
new_certificate.description = "{0} - {1}".format(new_certificate.description, description)
|
||||
|
||||
new_certificate = database.create(new_certificate)
|
||||
database.update_list(new_certificate, 'destinations', Destination, dests)
|
||||
database.update_list(new_certificate, 'notifications', Notification, nots)
|
||||
database.update(new_certificate)
|
||||
|
||||
arn = new_certificate.get_arn(account_id)
|
||||
|
||||
elb.update_listeners(account_id, region, elb_name, [(from_port, to_port, protocol, arn)], [from_port])
|
||||
|
||||
sys.stdout.write("[+] Updated {0} to use {1}\n".format(elb_name, new_certificate.name))
|
||||
|
||||
|
||||
class ProvisionELB(Command):
|
||||
"""
|
||||
Creates and provisions a certificate on an ELB based on command line arguments
|
||||
"""
|
||||
option_list = (
|
||||
Option('-d', '--dns', dest='dns', action='append', required=True, type=unicode_),
|
||||
Option('-e', '--elb', dest='elb_name', required=True, type=unicode_),
|
||||
Option('-o', '--owner', dest='owner', type=unicode_),
|
||||
Option('-a', '--authority', dest='authority', required=True, type=unicode_),
|
||||
Option('-s', '--description', dest='description', default=u'Command line provisioned keypair', type=unicode_),
|
||||
Option('-t', '--destination', dest='destinations', action='append', type=unicode_, required=True),
|
||||
Option('-n', '--notification', dest='notifications', action='append', type=unicode_, default=[]),
|
||||
Option('-r', '--region', dest='region', default=u'us-east-1', type=unicode_),
|
||||
Option('-p', '--dport', '--port', dest='dport', default=7002),
|
||||
Option('--src-port', '--source-port', '--sport', dest='sport', default=443),
|
||||
Option('--dry-run', dest='dryrun', action='store_true')
|
||||
)
|
||||
|
||||
def configure_user(self, owner):
|
||||
from flask import g
|
||||
import lemur.users.service
|
||||
|
||||
# grab the user
|
||||
g.user = lemur.users.service.get_by_username(owner)
|
||||
# get the first user by default
|
||||
if not g.user:
|
||||
g.user = lemur.users.service.get_all()[0]
|
||||
|
||||
return g.user.username
|
||||
|
||||
def build_cert_options(self, destinations, notifications, description, owner, dns, authority):
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from lemur.certificates.views import valid_authority
|
||||
import sys
|
||||
|
||||
# convert argument lists to arrays, or empty sets
|
||||
destinations = self.get_destinations(destinations)
|
||||
if not destinations:
|
||||
sys.stderr.write("Valid destinations provided\n")
|
||||
sys.exit(1)
|
||||
|
||||
# get the primary CN
|
||||
common_name = dns[0]
|
||||
|
||||
# If there are more than one fqdn, add them as alternate names
|
||||
extensions = {}
|
||||
if len(dns) > 1:
|
||||
extensions['subAltNames'] = {'names': map(lambda x: {'nameType': 'DNSName', 'value': x}, dns)}
|
||||
|
||||
try:
|
||||
authority = valid_authority({"name": authority})
|
||||
except NoResultFound:
|
||||
sys.stderr.write("Invalid authority specified: '{}'\naborting\n".format(authority))
|
||||
sys.exit(1)
|
||||
|
||||
options = {
|
||||
# Convert from the Destination model to the JSON input expected further in the code
|
||||
'destinations': map(lambda x: {'id': x.id, 'label': x.label}, destinations),
|
||||
'description': description,
|
||||
'notifications': notifications,
|
||||
'commonName': common_name,
|
||||
'extensions': extensions,
|
||||
'authority': authority,
|
||||
'owner': owner,
|
||||
# defaults:
|
||||
'organization': current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
'organizationalUnit': current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
|
||||
'country': current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
|
||||
'state': current_app.config.get('LEMUR_DEFAULT_STATE'),
|
||||
'location': current_app.config.get('LEMUR_DEFAULT_LOCATION')
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
def get_destinations(self, destination_names):
|
||||
from lemur.destinations import service
|
||||
|
||||
destinations = []
|
||||
|
||||
for destination_name in destination_names:
|
||||
destination = service.get_by_label(destination_name)
|
||||
|
||||
if not destination:
|
||||
sys.stderr.write("Invalid destination specified: '{}'\nAborting...\n".format(destination_name))
|
||||
sys.exit(1)
|
||||
|
||||
destinations.append(service.get_by_label(destination_name))
|
||||
|
||||
return destinations
|
||||
|
||||
def check_duplicate_listener(self, elb_name, region, account, sport, dport):
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
listeners = elb.get_listeners(account, region, elb_name)
|
||||
for listener in listeners:
|
||||
if listener[0] == sport and listener[1] == dport:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_destination_account(self, destinations):
|
||||
for destination in self.get_destinations(destinations):
|
||||
if destination.plugin_name == 'aws-destination':
|
||||
|
||||
account_number = destination.plugin.get_option('accountNumber', destination.options)
|
||||
return account_number
|
||||
|
||||
sys.stderr.write("No destination AWS account provided, failing\n")
|
||||
sys.exit(1)
|
||||
|
||||
def run(self, dns, elb_name, owner, authority, description, notifications, destinations, region, dport, sport,
|
||||
dryrun):
|
||||
from lemur.certificates import service
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
from boto.exception import BotoServerError
|
||||
|
||||
# configure the owner if we can find it, or go for default, and put it in the global
|
||||
owner = self.configure_user(owner)
|
||||
|
||||
# make a config blob from the command line arguments
|
||||
cert_options = self.build_cert_options(
|
||||
destinations=destinations,
|
||||
notifications=notifications,
|
||||
description=description,
|
||||
owner=owner,
|
||||
dns=dns,
|
||||
authority=authority)
|
||||
|
||||
aws_account = self.get_destination_account(destinations)
|
||||
|
||||
if dryrun:
|
||||
import json
|
||||
|
||||
cert_options['authority'] = cert_options['authority'].name
|
||||
sys.stdout.write('Will create certificate using options: {}\n'
|
||||
.format(json.dumps(cert_options, sort_keys=True, indent=2)))
|
||||
sys.stdout.write('Will create listener {}->{} HTTPS using the new certificate to elb {}\n'
|
||||
.format(sport, dport, elb_name))
|
||||
sys.exit(0)
|
||||
|
||||
if self.check_duplicate_listener(elb_name, region, aws_account, sport, dport):
|
||||
sys.stderr.write("ELB {} already has a listener {}->{}\nAborting...\n".format(elb_name, sport, dport))
|
||||
sys.exit(1)
|
||||
|
||||
# create the certificate
|
||||
try:
|
||||
sys.stdout.write('Creating certificate for {}\n'.format(cert_options['commonName']))
|
||||
cert = service.create(**cert_options)
|
||||
except Exception as e:
|
||||
if e.message == 'Duplicate certificate: a certificate with the same common name exists already':
|
||||
sys.stderr.write("Certificate already exists named: {}\n".format(dns[0]))
|
||||
sys.exit(1)
|
||||
raise e
|
||||
|
||||
cert_arn = cert.get_arn(aws_account)
|
||||
sys.stderr.write('cert arn: {}\n'.format(cert_arn))
|
||||
|
||||
sys.stderr.write('Configuring elb {} from port {} to port {} in region {} with cert {}\n'
|
||||
.format(elb_name, sport, dport, region, cert_arn))
|
||||
|
||||
delay = 1
|
||||
done = False
|
||||
retries = 5
|
||||
while not done and retries > 0:
|
||||
try:
|
||||
elb.create_new_listeners(aws_account, region, elb_name, [(sport, dport, 'HTTPS', cert_arn)])
|
||||
except BotoServerError as bse:
|
||||
# if the server returns ad error, the certificate
|
||||
if bse.error_code == 'CertificateNotFound':
|
||||
sys.stderr.write('Certificate not available yet in the AWS account, waiting {}, {} retries left\n'
|
||||
.format(delay, retries))
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
retries -= 1
|
||||
elif bse.error_code == 'DuplicateListener':
|
||||
sys.stderr.write('ELB {} already has a listener {}->{}'.format(elb_name, sport, dport))
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise bse
|
||||
else:
|
||||
done = True
|
||||
|
||||
|
||||
@manager.command
|
||||
def publish_verisign_units():
|
||||
"""
|
||||
Simple function that queries verisign for API units and posts the mertics to
|
||||
Atlas API for other teams to consume.
|
||||
:return:
|
||||
"""
|
||||
from lemur.plugins import plugins
|
||||
v = plugins.get('verisign-issuer')
|
||||
units = v.get_available_units()
|
||||
|
||||
metrics = {}
|
||||
for item in units:
|
||||
if item['@type'] in metrics.keys():
|
||||
metrics[item['@type']] += int(item['@remaining'])
|
||||
else:
|
||||
metrics.update({item['@type']: int(item['@remaining'])})
|
||||
|
||||
for name, value in metrics.items():
|
||||
metric = [
|
||||
{
|
||||
"timestamp": 1321351651,
|
||||
"type": "GAUGE",
|
||||
"name": "Symantec {0} Unit Count".format(name),
|
||||
"tags": {},
|
||||
"value": value
|
||||
}
|
||||
]
|
||||
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
class Rolling(Command):
|
||||
"""
|
||||
Rotates existing certificates to a new one on an ELB
|
||||
"""
|
||||
option_list = (
|
||||
Option('-w', '--window', dest='window', default=24),
|
||||
)
|
||||
|
||||
def run(self, window):
|
||||
"""
|
||||
Simple function that queries verisign for API units and posts the mertics to
|
||||
Atlas API for other teams to consume.
|
||||
:return:
|
||||
"""
|
||||
end = arrow.utcnow()
|
||||
start = end.replace(hours=-window)
|
||||
items = Certificate.query.filter(Certificate.not_before <= end.format('YYYY-MM-DD')) \
|
||||
.filter(Certificate.not_before >= start.format('YYYY-MM-DD')).all()
|
||||
|
||||
metrics = {}
|
||||
for i in items:
|
||||
name = "{0},{1}".format(i.owner, i.issuer)
|
||||
if metrics.get(name):
|
||||
metrics[name] += 1
|
||||
else:
|
||||
metrics[name] = 1
|
||||
|
||||
for name, value in metrics.iteritems():
|
||||
owner, issuer = name.split(",")
|
||||
metric = [
|
||||
{
|
||||
"timestamp": 1321351651,
|
||||
"type": "GAUGE",
|
||||
"name": "Issued Certificates",
|
||||
"tags": {"owner": owner, "issuer": issuer, "window": window},
|
||||
"value": value
|
||||
}
|
||||
]
|
||||
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
def main():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1'))
|
||||
@ -488,9 +869,12 @@ def main():
|
||||
manager.add_command("db", MigrateCommand)
|
||||
manager.add_command("init", InitializeApp())
|
||||
manager.add_command("create_user", CreateUser())
|
||||
manager.add_command("reset_password", ResetPassword())
|
||||
manager.add_command("create_role", CreateRole())
|
||||
manager.add_command("provision_elb", ProvisionELB())
|
||||
manager.add_command("rotate_elbs", RotateELBs())
|
||||
manager.add_command("rolling", Rolling())
|
||||
manager.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
32
lemur/metrics.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
.. module: lemur.metrics
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
class Metrics(object):
|
||||
"""
|
||||
:param app: The Flask application object. Defaults to None.
|
||||
"""
|
||||
_providers = []
|
||||
|
||||
def __init__(self, app=None):
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initializes the application with the extension.
|
||||
|
||||
:param app: The Flask application object.
|
||||
"""
|
||||
self._providers = app.config.get('METRIC_PROVIDERS', [])
|
||||
|
||||
def send(self, metric_name, metric_type, metric_value, *args, **kwargs):
|
||||
for provider in self._providers:
|
||||
current_app.logger.debug(
|
||||
"Sending metric '{metric}' to the {provider} provider.".format(metric=metric_name, provider=provider))
|
||||
p = plugins.get(provider)
|
||||
p.submit(metric_name, metric_type, metric_value, *args, **kwargs)
|
@ -1,42 +0,0 @@
|
||||
"""Adding in models for certificate sources
|
||||
|
||||
Revision ID: 1ff763f5b80b
|
||||
Revises: 4dc5ddd111b8
|
||||
Create Date: 2015-08-01 15:24:20.412725
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1ff763f5b80b'
|
||||
down_revision = '4dc5ddd111b8'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
import sqlalchemy_utils
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('sources',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=32), nullable=True),
|
||||
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('plugin_name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('certificate_source_associations',
|
||||
sa.Column('source_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['source_id'], ['destinations.id'], ondelete='cascade')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_source_associations')
|
||||
op.drop_table('sources')
|
||||
### end Alembic commands ###
|
131
lemur/migrations/versions/3307381f3b88_.py
Normal file
@ -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 ###
|
31
lemur/migrations/versions/33de094da890_.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Adding the ability to specify certificate replacements
|
||||
|
||||
Revision ID: 33de094da890
|
||||
Revises: ed422fc58ba
|
||||
Create Date: 2015-11-30 15:40:19.827272
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '33de094da890'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('certificate_replacement_associations',
|
||||
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_replacement_associations')
|
||||
### end Alembic commands ###
|
@ -1,49 +0,0 @@
|
||||
"""Refactors Accounts to Destinations
|
||||
|
||||
Revision ID: 3b718f59b8ce
|
||||
Revises: None
|
||||
Create Date: 2015-07-09 17:44:55.626221
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3b718f59b8ce'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_account_associations')
|
||||
op.drop_table('accounts')
|
||||
op.add_column('destinations', sa.Column('plugin_name', sa.String(length=32), nullable=True))
|
||||
op.drop_index('ix_elbs_account_id', table_name='elbs')
|
||||
op.drop_constraint(u'elbs_account_id_fkey', 'elbs', type_='foreignkey')
|
||||
op.drop_column('elbs', 'account_id')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('elbs', sa.Column('account_id', sa.BIGINT(), autoincrement=False, nullable=True))
|
||||
op.create_foreign_key(u'elbs_account_id_fkey', 'elbs', 'accounts', ['account_id'], ['id'])
|
||||
op.create_index('ix_elbs_account_id', 'elbs', ['account_id'], unique=False)
|
||||
op.drop_column('destinations', 'plugin_name')
|
||||
op.create_table('accounts',
|
||||
sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('accounts_id_seq'::regclass)"), nullable=False),
|
||||
sa.Column('account_number', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('label', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
|
||||
sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.PrimaryKeyConstraint('id', name=u'accounts_pkey'),
|
||||
sa.UniqueConstraint('account_number', name=u'accounts_account_number_key'),
|
||||
postgresql_ignore_search_path=False
|
||||
)
|
||||
op.create_table('certificate_account_associations',
|
||||
sa.Column('account_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('certificate_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['account_id'], [u'accounts.id'], name=u'certificate_account_associations_account_id_fkey', ondelete=u'CASCADE'),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], [u'certificates.id'], name=u'certificate_account_associations_certificate_id_fkey', ondelete=u'CASCADE')
|
||||
)
|
||||
### end Alembic commands ###
|
63
lemur/migrations/versions/412b22cb656a_.py
Normal file
@ -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 ###
|
26
lemur/migrations/versions/4c50b903d1ae_.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Adding ability to mark domains as 'sensitive'
|
||||
|
||||
Revision ID: 4c50b903d1ae
|
||||
Revises: 33de094da890
|
||||
Create Date: 2015-12-30 10:19:30.057791
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4c50b903d1ae'
|
||||
down_revision = '33de094da890'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('domains', sa.Column('sensitive', sa.Boolean(), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('domains', 'sensitive')
|
||||
### end Alembic commands ###
|
@ -1,41 +0,0 @@
|
||||
"""Adding notifications
|
||||
|
||||
Revision ID: 4c8915e461b3
|
||||
Revises: 3b718f59b8ce
|
||||
Create Date: 2015-07-24 14:34:57.316273
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4c8915e461b3'
|
||||
down_revision = '3b718f59b8ce'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
import sqlalchemy_utils
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('notifications',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=128), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=True),
|
||||
sa.Column('plugin_name', sa.String(length=32), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.drop_column(u'certificates', 'challenge')
|
||||
op.drop_column(u'certificates', 'csr_config')
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(u'certificates', sa.Column('csr_config', sa.TEXT(), autoincrement=False, nullable=True))
|
||||
op.add_column(u'certificates', sa.Column('challenge', postgresql.BYTEA(), autoincrement=False, nullable=True))
|
||||
op.drop_table('notifications')
|
||||
### end Alembic commands ###
|
@ -1,31 +0,0 @@
|
||||
"""Creating a one-to-many relationship for notifications
|
||||
|
||||
Revision ID: 4dc5ddd111b8
|
||||
Revises: 4c8915e461b3
|
||||
Create Date: 2015-07-24 15:02:04.398262
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4dc5ddd111b8'
|
||||
down_revision = '4c8915e461b3'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('certificate_notification_associations',
|
||||
sa.Column('notification_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('certificate_notification_associations')
|
||||
### end Alembic commands ###
|
@ -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,13 +35,44 @@ 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')),
|
||||
Column('certificate_id', Integer,
|
||||
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')),
|
||||
Column('certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.certificate_id, certificate_replacement_associations.c.certificate_id)
|
||||
|
||||
roles_authorities = db.Table('roles_authorities',
|
||||
Column('authority_id', Integer, ForeignKey('authorities.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
|
||||
|
||||
roles_certificates = db.Table('roles_certificates',
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id)
|
||||
|
||||
|
||||
roles_users = db.Table('roles_users',
|
||||
Column('user_id', Integer, ForeignKey('users.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id)
|
||||
|
49
lemur/notifications/schemas.py
Normal file
@ -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)
|
@ -9,7 +9,6 @@
|
||||
|
||||
"""
|
||||
import ssl
|
||||
import socket
|
||||
|
||||
import arrow
|
||||
|
||||
@ -37,10 +36,16 @@ def _get_message_data(cert):
|
||||
:param cert:
|
||||
:return:
|
||||
"""
|
||||
cert_dict = cert.as_dict()
|
||||
cert_dict['creator'] = cert.user.email
|
||||
cert_dict['domains'] = [x .name for x in cert.domains]
|
||||
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert) if cert.name != x]))
|
||||
cert_dict = {}
|
||||
|
||||
if cert.user:
|
||||
cert_dict['creator'] = cert.user.email
|
||||
|
||||
cert_dict['not_after'] = cert.not_after
|
||||
cert_dict['owner'] = cert.owner
|
||||
cert_dict['name'] = cert.name
|
||||
cert_dict['body'] = cert.body
|
||||
|
||||
return cert_dict
|
||||
|
||||
|
||||
@ -56,13 +61,18 @@ def _deduplicate(messages):
|
||||
|
||||
for m, r, o in roll_ups:
|
||||
if r == targets:
|
||||
m.append(data)
|
||||
current_app.logger.info(
|
||||
"Sending expiration alert about {0} to {1}".format(
|
||||
data['name'], ",".join(targets)))
|
||||
for cert in m:
|
||||
if cert['body'] == data['body']:
|
||||
break
|
||||
else:
|
||||
m.append(data)
|
||||
current_app.logger.info(
|
||||
"Sending expiration alert about {0} to {1}".format(
|
||||
data['name'], ",".join(targets)))
|
||||
break
|
||||
else:
|
||||
roll_ups.append(([data], targets, options))
|
||||
|
||||
return roll_ups
|
||||
|
||||
|
||||
@ -106,8 +116,9 @@ def _get_domain_certificate(name):
|
||||
try:
|
||||
pub_key = ssl.get_server_certificate((name, 443))
|
||||
return cert_service.find_duplicates(pub_key.strip())
|
||||
except socket.gaierror as e:
|
||||
except Exception as e:
|
||||
current_app.logger.info(str(e))
|
||||
return []
|
||||
|
||||
|
||||
def _find_superseded(cert):
|
||||
@ -178,6 +189,9 @@ def create_default_expiration_notifications(name, recipients):
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
if not recipients:
|
||||
return []
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'unit',
|
||||
@ -198,7 +212,7 @@ def create_default_expiration_notifications(name, recipients):
|
||||
},
|
||||
]
|
||||
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [30, 15, 2])
|
||||
|
||||
notifications = []
|
||||
for i in intervals:
|
||||
@ -259,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)
|
||||
|
||||
@ -305,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)
|
||||
|
||||
@ -327,9 +337,4 @@ def render(args):
|
||||
else:
|
||||
query = database.filter(query, Notification, terms)
|
||||
|
||||
query = database.find_all(query, Notification, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Notification, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Notification, args)
|
||||
|
@ -7,34 +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,
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@ -61,7 +54,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
"items": [
|
||||
{
|
||||
"description": "An example",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -103,9 +96,9 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -114,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
|
||||
|
||||
@ -131,7 +124,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -178,7 +171,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -221,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']
|
||||
)
|
||||
|
||||
|
||||
@ -241,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
|
||||
@ -266,7 +253,7 @@ class Notifications(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -308,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
|
||||
|
||||
@ -345,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):
|
||||
@ -371,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
|
||||
@ -398,7 +378,7 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
"items": [
|
||||
{
|
||||
"description": "An example",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -440,17 +420,13 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair. format is k=v;
|
||||
:query limit: limit number. default is 10
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('active', type=bool, location='args')
|
||||
args = parser.parse_args()
|
||||
args['certificate_id'] = certificate_id
|
||||
return service.render(args)
|
||||
return service.render({'certificate_id': certificate_id})
|
||||
|
||||
|
||||
api.add_resource(NotificationsList, '/notifications', endpoint='notifications')
|
||||
|
@ -108,10 +108,11 @@ class IPlugin(local):
|
||||
"""
|
||||
return self.resource_links
|
||||
|
||||
def get_option(self, name, options):
|
||||
@staticmethod
|
||||
def get_option(name, options):
|
||||
for o in options:
|
||||
if o.get(name):
|
||||
return o['value']
|
||||
if o.get('name') == name:
|
||||
return o.get('value', o.get('default'))
|
||||
|
||||
|
||||
class Plugin(IPlugin):
|
||||
|
@ -2,3 +2,4 @@ from .destination import DestinationPlugin # noqa
|
||||
from .issuer import IssuerPlugin # noqa
|
||||
from .source import SourcePlugin # noqa
|
||||
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa
|
||||
from .export import ExportPlugin # noqa
|
||||
|
21
lemur/plugins/bases/export.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""
|
||||
.. module: lemur.bases.export
|
||||
: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 ExportPlugin(Plugin):
|
||||
"""
|
||||
This is the base class from which all supported
|
||||
exporters will inherit from.
|
||||
"""
|
||||
type = 'export'
|
||||
requires_key = True
|
||||
|
||||
def export(self):
|
||||
raise NotImplemented
|
16
lemur/plugins/bases/metric.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
.. module: lemur.bases.metric
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from lemur.plugins.base import Plugin
|
||||
|
||||
|
||||
class MetricPlugin(Plugin):
|
||||
type = 'metric'
|
||||
|
||||
def submit(self, *args, **kwargs):
|
||||
raise NotImplemented
|
@ -45,7 +45,7 @@ class ExpirationNotificationPlugin(NotificationPlugin):
|
||||
]
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
def plugin_options(self):
|
||||
return list(self.default_options) + self.additional_options
|
||||
|
||||
def send(self):
|
||||
|
107
lemur/plugins/lemur_atlas/plugin.py
Normal file
@ -0,0 +1,107 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_atlas.plugin
|
||||
:platform: Unix
|
||||
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
from datetime import datetime
|
||||
|
||||
from flask import current_app
|
||||
from lemur.plugins import lemur_atlas as atlas
|
||||
from lemur.plugins.bases.metric import MetricPlugin
|
||||
|
||||
|
||||
def millis_since_epoch():
|
||||
"""
|
||||
current time since epoch in milliseconds
|
||||
"""
|
||||
epoch = datetime.utcfromtimestamp(0)
|
||||
delta = datetime.now() - epoch
|
||||
return int(delta.total_seconds() * 1000.0)
|
||||
|
||||
|
||||
class AtlasMetricPlugin(MetricPlugin):
|
||||
title = 'Atlas'
|
||||
slug = 'atlas-metric'
|
||||
description = 'Adds support for sending key metrics to Atlas'
|
||||
version = atlas.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'sidecar_host',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'help_message': 'If no host is provided localhost is assumed',
|
||||
'default': 'localhost'
|
||||
},
|
||||
{
|
||||
'name': 'sidecar_port',
|
||||
'type': 'int',
|
||||
'required': False,
|
||||
'default': 8078
|
||||
}
|
||||
]
|
||||
|
||||
metric_data = {}
|
||||
sidecar_host = None
|
||||
sidecar_port = None
|
||||
|
||||
def submit(self, metric_name, metric_type, metric_value, metric_tags=None, options=None):
|
||||
if not options:
|
||||
options = self.options
|
||||
|
||||
# TODO marshmallow schema?
|
||||
valid_types = ['COUNTER', 'GAUGE', 'TIMER']
|
||||
if metric_type.upper() not in valid_types:
|
||||
raise Exception(
|
||||
"Invalid Metric Type for Atlas: '{metric}' choose from: {options}".format(
|
||||
metric=metric_type, options=','.join(valid_types)
|
||||
)
|
||||
)
|
||||
|
||||
if metric_tags:
|
||||
if not isinstance(metric_tags, dict):
|
||||
raise Exception(
|
||||
"Invalid Metric Tags for Atlas: Tags must be in dict format"
|
||||
)
|
||||
|
||||
if metric_value == "NaN" or isinstance(metric_value, int) or isinstance(metric_value, float):
|
||||
self.metric_data['value'] = metric_value
|
||||
else:
|
||||
raise Exception(
|
||||
"Invalid Metric Value for Atlas: Metric must be a number"
|
||||
)
|
||||
|
||||
self.metric_data['type'] = metric_type.upper()
|
||||
self.metric_data['name'] = str(metric_name)
|
||||
self.metric_data['tags'] = metric_tags
|
||||
self.metric_data['timestamp'] = millis_since_epoch()
|
||||
|
||||
self.sidecar_host = self.get_option('sidecar_host', options)
|
||||
self.sidecar_port = self.get_option('sidecar_port', options)
|
||||
|
||||
try:
|
||||
res = requests.post(
|
||||
'http://{host}:{port}/metrics'.format(
|
||||
host=self.sidecar_host,
|
||||
port=self.sidecar_port),
|
||||
data=json.dumps([self.metric_data])
|
||||
)
|
||||
|
||||
if res.status_code != 200:
|
||||
current_app.logger.warning("Failed to publish altas metric. {0}".format(res.content))
|
||||
|
||||
except ConnectionError:
|
||||
current_app.logger.warning(
|
||||
"AtlasMetrics: could not connect to sidecar at {host}:{port}".format(
|
||||
host=self.sidecar_host, port=self.sidecar_port
|
||||
)
|
||||
)
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
.. module: elb
|
||||
.. module: lemur.plugins.lemur_aws.elb
|
||||
:synopsis: Module contains some often used and helpful classes that
|
||||
are used to deal with ELBs
|
||||
|
||||
@ -28,7 +28,6 @@ def is_valid(listener_tuple):
|
||||
|
||||
:param listener_tuple:
|
||||
"""
|
||||
|
||||
current_app.logger.debug(listener_tuple)
|
||||
lb_port, i_port, lb_protocol, arn = listener_tuple
|
||||
current_app.logger.debug(lb_protocol)
|
||||
@ -138,3 +137,19 @@ def delete_listeners(account_number, region, name, ports):
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
|
||||
|
||||
def get_listeners(account_number, region, name):
|
||||
"""
|
||||
Gets the listeners configured on an elb and returns a array of tuples
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:return: list of tuples
|
||||
"""
|
||||
|
||||
conn = assume_service(account_number, 'elb', region)
|
||||
elbs = conn.get_all_load_balancers(load_balancer_names=[name])
|
||||
if elbs:
|
||||
return elbs[0].listeners
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from boto.exception import BotoServerError
|
||||
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
|
||||
from lemur.plugins.lemur_aws import iam, elb
|
||||
from lemur.plugins import lemur_aws as aws
|
||||
@ -42,11 +43,18 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
# }
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
if private_key:
|
||||
try:
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
except BotoServerError as e:
|
||||
if e.error_code != 'EntityAlreadyExists':
|
||||
raise Exception(e)
|
||||
|
||||
e = find_value('elb', options)
|
||||
if e:
|
||||
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
||||
e = find_value('elb', options)
|
||||
if e:
|
||||
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
||||
else:
|
||||
raise Exception("Unable to upload to AWS, private key is required")
|
||||
|
||||
|
||||
class AWSSourcePlugin(SourcePlugin):
|
||||
@ -75,8 +83,8 @@ class AWSSourcePlugin(SourcePlugin):
|
||||
cert_body, cert_chain = iam.get_cert_from_arn(arn)
|
||||
cert_name = iam.get_name_from_arn(arn)
|
||||
cert = dict(
|
||||
public_certificate=cert_body,
|
||||
intermediate_certificate=cert_chain,
|
||||
body=cert_body,
|
||||
chain=cert_chain,
|
||||
name=cert_name
|
||||
)
|
||||
certs.append(cert)
|
||||
|
@ -2,7 +2,7 @@ from moto import mock_iam, mock_sts
|
||||
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
from lemur.tests.certs import EXTERNAL_VALID_STR, PRIVATE_KEY_STR
|
||||
from lemur.tests.vectors import EXTERNAL_VALID_STR, PRIVATE_KEY_STR
|
||||
|
||||
|
||||
def test_get_name_from_arn():
|
||||
|
@ -1,352 +0,0 @@
|
||||
"""
|
||||
.. module: lemur.common.services.issuers.plugins.cloudca
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import ssl
|
||||
import base64
|
||||
from json import dumps
|
||||
|
||||
import arrow
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.exceptions import LemurException
|
||||
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
|
||||
from lemur.plugins import lemur_cloudca as cloudca
|
||||
|
||||
from lemur.authorities import service as authority_service
|
||||
|
||||
API_ENDPOINT = '/v1/ca/netflix' # TODO this should be configurable
|
||||
|
||||
|
||||
class CloudCAException(LemurException):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr("CloudCA request failed: {0}".format(self.message))
|
||||
|
||||
|
||||
class CloudCAHostNameCheckingAdapter(HTTPAdapter):
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
conn.assert_hostname = False
|
||||
|
||||
|
||||
def remove_none(options):
|
||||
"""
|
||||
Simple function that traverse the options and removed any None items
|
||||
CloudCA really dislikes null values.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
new_dict = {}
|
||||
for k, v in options.items():
|
||||
if v:
|
||||
new_dict[k] = v
|
||||
|
||||
# this is super hacky and gross, cloudca doesn't like null values
|
||||
if new_dict.get('extensions'):
|
||||
if len(new_dict['extensions']['subAltNames']['names']) == 0:
|
||||
del new_dict['extensions']['subAltNames']
|
||||
|
||||
return new_dict
|
||||
|
||||
|
||||
def get_default_issuance(options):
|
||||
"""
|
||||
Gets the default time range for certificates
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
if not options.get('validityStart') and not options.get('validityEnd'):
|
||||
start = arrow.utcnow()
|
||||
options['validityStart'] = start.floor('second').isoformat()
|
||||
options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY'))\
|
||||
.ceil('second').isoformat()
|
||||
return options
|
||||
|
||||
|
||||
def convert_to_pem(der):
|
||||
"""
|
||||
Converts DER to PEM Lemur uses PEM internally
|
||||
|
||||
:param der:
|
||||
:return:
|
||||
"""
|
||||
decoded = base64.b64decode(der)
|
||||
return ssl.DER_cert_to_PEM_cert(decoded)
|
||||
|
||||
|
||||
def convert_date_to_utc_time(date):
|
||||
"""
|
||||
Converts a python `datetime` object to the current date + current time in UTC.
|
||||
|
||||
:param date:
|
||||
:return:
|
||||
"""
|
||||
d = arrow.get(date)
|
||||
return arrow.utcnow().replace(day=d.naive.day).replace(month=d.naive.month).replace(year=d.naive.year)\
|
||||
.replace(microsecond=0)
|
||||
|
||||
|
||||
def process_response(response):
|
||||
"""
|
||||
Helper function that processes responses from CloudCA.
|
||||
|
||||
:param response:
|
||||
:return: :raise CloudCAException:
|
||||
"""
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if res['returnValue'] != 'success':
|
||||
current_app.logger.debug(res)
|
||||
if res.get('data'):
|
||||
raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']]))
|
||||
else:
|
||||
raise CloudCAException(res['returnMessage'])
|
||||
else:
|
||||
raise CloudCAException("There was an error with your request: {0}".format(response.status_code))
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_auth_data(ca_name):
|
||||
"""
|
||||
Creates the authentication record needed to authenticate a user request to CloudCA.
|
||||
|
||||
:param ca_name:
|
||||
:return: :raise CloudCAException:
|
||||
"""
|
||||
role = authority_service.get_authority_role(ca_name)
|
||||
if role:
|
||||
return {
|
||||
"authInfo": {
|
||||
"credType": "password",
|
||||
"credentials": {
|
||||
"username": role.username,
|
||||
"password": role.password # we only decrypt when we need to
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
|
||||
|
||||
|
||||
class CloudCA(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', CloudCAHostNameCheckingAdapter())
|
||||
self.url = current_app.config.get('CLOUDCA_URL')
|
||||
|
||||
if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'):
|
||||
self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH')
|
||||
self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE')
|
||||
else:
|
||||
current_app.logger.warning(
|
||||
"No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA"
|
||||
)
|
||||
|
||||
super(CloudCA, self).__init__(*args, **kwargs)
|
||||
|
||||
def post(self, endpoint, data):
|
||||
"""
|
||||
HTTP POST to CloudCA
|
||||
|
||||
:param endpoint:
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
data = dumps(dict(data.items() + get_auth_data(data['caName']).items()))
|
||||
|
||||
# we set a low timeout, if cloudca is down it shouldn't bring down
|
||||
# lemur
|
||||
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
|
||||
return process_response(response)
|
||||
|
||||
def get(self, endpoint):
|
||||
"""
|
||||
HTTP GET to CloudCA
|
||||
|
||||
:param endpoint:
|
||||
:return:
|
||||
"""
|
||||
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
|
||||
return process_response(response)
|
||||
|
||||
def random(self, length=10):
|
||||
"""
|
||||
Uses CloudCA as a decent source of randomness.
|
||||
|
||||
:param length:
|
||||
:return:
|
||||
"""
|
||||
endpoint = '/v1/random/{0}'.format(length)
|
||||
response = self.session.get(self.url + endpoint, verify=self.ca_bundle)
|
||||
return response
|
||||
|
||||
def get_authorities(self):
|
||||
"""
|
||||
Retrieves authorities that were made outside of Lemur.
|
||||
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/listCAs'.format(API_ENDPOINT)
|
||||
authorities = []
|
||||
for ca in self.get(endpoint)['data']['caList']:
|
||||
try:
|
||||
authorities.append(ca['caName'])
|
||||
except AttributeError:
|
||||
current_app.logger.error("No authority has been defined for {}".format(ca['caName']))
|
||||
|
||||
return authorities
|
||||
|
||||
|
||||
class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
|
||||
title = 'CloudCA'
|
||||
slug = 'cloudca-issuer'
|
||||
description = 'Enables the creation of certificates from the cloudca API.'
|
||||
version = cloudca.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
def create_authority(self, options):
|
||||
"""
|
||||
Creates a new certificate authority
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
# this is weird and I don't like it
|
||||
endpoint = '{0}/createCA'.format(API_ENDPOINT)
|
||||
options['caDN']['email'] = options['ownerEmail']
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
options = dict(options.items() + self.auth_data(options['caParent']).items())
|
||||
|
||||
options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat()
|
||||
options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat()
|
||||
|
||||
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
|
||||
json = process_response(response)
|
||||
roles = []
|
||||
|
||||
for cred in json['data']['authInfo']:
|
||||
role = {
|
||||
'username': cred['credentials']['username'],
|
||||
'password': cred['credentials']['password'],
|
||||
'name': "_".join([options['caName'], cred['credentials']['username']])
|
||||
}
|
||||
roles.append(role)
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
cert = convert_to_pem(json['data']['certificate'])
|
||||
else:
|
||||
cert = convert_to_pem(json['data']['rootCertificate'])
|
||||
|
||||
intermediates = []
|
||||
for i in json['data']['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
return cert, "".join(intermediates), roles,
|
||||
|
||||
def create_certificate(self, csr, options):
|
||||
"""
|
||||
Creates a new certificate from cloudca
|
||||
|
||||
If no start and end date are specified the default issue range
|
||||
will be used.
|
||||
|
||||
:param csr:
|
||||
:param options:
|
||||
"""
|
||||
endpoint = '{0}/enroll'.format(API_ENDPOINT)
|
||||
# lets default to two years if it's not specified
|
||||
# we do some last minute data massaging
|
||||
options = get_default_issuance(options)
|
||||
|
||||
cloudca_options = {
|
||||
'extensions': options['extensions'],
|
||||
'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(),
|
||||
'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(),
|
||||
'creator': options['creator'],
|
||||
'ownerEmail': options['owner'],
|
||||
'caName': options['authority'].name,
|
||||
'csr': csr,
|
||||
'comment': options['description']
|
||||
}
|
||||
|
||||
response = self.post(endpoint, remove_none(cloudca_options))
|
||||
|
||||
# we return a concatenated list of intermediate because that is what aws
|
||||
# expects
|
||||
cert = convert_to_pem(response['data']['certificate'])
|
||||
|
||||
intermediates = [convert_to_pem(response['data']['rootCertificate'])]
|
||||
for i in response['data']['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
return cert, "".join(intermediates),
|
||||
|
||||
|
||||
class CloudCASourcePlugin(SourcePlugin, CloudCA):
|
||||
title = 'CloudCA'
|
||||
slug = 'cloudca-source'
|
||||
description = 'Discovers all SSL certificates in CloudCA'
|
||||
version = cloudca.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
options = {
|
||||
'pollRate': {'type': 'int', 'default': '60'}
|
||||
}
|
||||
|
||||
def get_certificates(self, **kwargs):
|
||||
certs = []
|
||||
for authority in self.get_authorities():
|
||||
certs += self.get_cert(ca_name=authority)
|
||||
return
|
||||
|
||||
def get_cert(self, ca_name=None, cert_handle=None):
|
||||
"""
|
||||
Returns a given cert from CloudCA.
|
||||
|
||||
:param ca_name:
|
||||
:param cert_handle:
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/getCert'.format(API_ENDPOINT)
|
||||
response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
raw = process_response(response)
|
||||
|
||||
certs = []
|
||||
for c in raw['data']['certList']:
|
||||
cert = convert_to_pem(c['certValue'])
|
||||
|
||||
intermediates = []
|
||||
for i in c['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
certs.append({
|
||||
'public_certificate': cert,
|
||||
'intermediate_cert': "\n".join(intermediates),
|
||||
'owner': c['ownerEmail']
|
||||
})
|
||||
|
||||
return certs
|