Compare commits

..

853 Commits
0.2.1 ... 0.7.0

Author SHA1 Message Date
c0c6ff51e2 Merge pull request #1372 from castrapel/acme_validation_dns_provider_option
R53: Extend only TXT records
2018-06-20 10:50:15 -07:00
4384cbb953 Merge branch 'master' into acme_validation_dns_provider_option 2018-06-20 10:34:38 -07:00
3397fb6560 R53: Extend only TXT records 2018-06-20 10:33:35 -07:00
b231521ff6 Merge pull request #1369 from castrapel/acme_validation_dns_provider_option
Acme validation dns provider option
2018-06-19 21:24:15 -07:00
3efc709e03 tests 2018-06-19 21:16:35 -07:00
dda7f54a16 lint 2018-06-19 20:58:00 -07:00
2d33d3e2b8 lint 2018-06-19 20:35:00 -07:00
d50c9c7748 Merge branch 'master' into acme_validation_dns_provider_option 2018-06-19 16:45:25 -07:00
665a0bcffe Update requirements 2018-06-19 16:38:05 -07:00
a141b8c5ea Support concurrent issuance in Route53 for LetsEncrypt 2018-06-19 16:27:58 -07:00
9d710702a4 Merge pull request #1348 from Netflix/doppins/boto3-equals-1.7.39
[Doppins] Upgrade dependency boto3 to ==1.7.39
2018-06-15 07:48:35 -07:00
e835fa6073 Upgrade dependency boto3 to ==1.7.39 2018-06-14 23:37:07 +00:00
c0d037b9e9 Merge pull request #1347 from castrapel/dyn2
Dyn
2018-06-14 08:16:05 -07:00
b2bc431823 Merge branch 'master' into dyn2 2018-06-14 08:06:31 -07:00
9c5140006b update requirements while we're at it 2018-06-14 08:04:35 -07:00
4e72cb96c9 Graceful cancellation of pending cert and order details in log for acme failure 2018-06-14 08:02:34 -07:00
f88e81ffef Merge pull request #1342 from castrapel/dyn_dns_fix
Limit dns queries to 10 attempts
2018-06-14 07:49:18 -07:00
17861289c8 Merge branch 'master' into dyn_dns_fix 2018-06-13 15:37:08 -07:00
b99aad743b remove linuxdst plugin 2018-06-13 15:15:09 -07:00
b1ce4d630d update requirements 2018-06-13 15:15:09 -07:00
135f2b710c Limit dns queries to 10 attempts 2018-06-13 15:14:48 -07:00
e8c18bd9b6 Merge pull request #1335 from castrapel/dyn_dns_fix
Dyn dns fix
2018-06-13 14:35:42 -07:00
76621e497f boto update. They updated between this and the last change 2018-06-13 14:27:50 -07:00
065e0edc5f lint 2018-06-13 14:22:45 -07:00
d72792ff37 Fix unique dyn situation where zone does not match tld, and there's a deeper zone 2018-06-13 14:08:39 -07:00
f9239b008e Merge remote-tracking branch 'upstream/master' 2018-06-12 08:08:01 -07:00
b31c7357ed update requirements 2018-06-12 08:07:18 -07:00
7c6d6f5297 Merge pull request #1171 from dmitryzykov/linuxdst
Remove linuxdst plugin
2018-06-12 07:56:39 -07:00
e225139011 Merge branch 'master' into linuxdst 2018-06-12 07:45:02 -07:00
37edf80321 Merge pull request #1320 from Netflix/update_reqs
update requirements
2018-06-12 07:44:45 -07:00
038f5dc554 Merge branch 'master' into linuxdst 2018-06-12 07:40:40 -07:00
5e964fad39 update requirements 2018-06-12 07:35:46 -07:00
3800d67d71 Merge pull request #1222 from castrapel/master
LetsEncrypt support . Version bump.
2018-06-12 07:33:17 -07:00
7f5d1a0b6b sync error 2018-06-11 15:40:15 -07:00
92860cffca Default configuration for DNS providers 2018-06-11 13:32:53 -07:00
2aced5c010 Merge branch 'master' into master 2018-06-09 10:28:24 -07:00
4e1879715d Merge pull request #1284 from Netflix/up-reqs-2
Update requirements again
2018-06-05 12:47:04 -07:00
403f70d6db Update requirements again 2018-06-05 09:51:19 -07:00
b33256c809 Merge pull request #1275 from Netflix/up-reqs
Update requirements
2018-06-04 13:41:27 -07:00
e39fac32ec Update requirements 2018-06-04 13:32:51 -07:00
80e3331596 Merge branch 'master' into master 2018-05-30 08:24:00 -07:00
2a3af5214e Merge branch 'master' into linuxdst 2018-05-29 18:54:37 -07:00
4911d713a5 Fix import metrics in notifications/messaging.py (#1254)
`from lemur import metrics` is incorrect for notifications/messaging.py
because that is importing the `metrics` module rather than the
instanciated `lemur.extensions.metrics` object.  This will cause errors
if you import notifications/messaging.py elsewhere, since it can cause
circular dependencies.

Change-Id: Ice28c480373601420fc83bae2d27bb6467cdb752
2018-05-29 18:54:16 -07:00
5e24f685c1 lint error 2018-05-29 10:46:24 -07:00
97d3621705 convert description to TEXT column 2018-05-29 10:23:01 -07:00
544a02ca3f Addressing comments. Updating copyrights. Added function to determine authorative name server 2018-05-29 10:23:01 -07:00
ae26e44cc2 Merge branch 'master' into master 2018-05-25 11:09:23 -07:00
b0f9d33b32 Requirements update 2018-05-25 11:07:26 -07:00
c5e7e5ab68 Merge pull request #1253 from Netflix/quicker_count
Sort and page
2018-05-25 11:04:05 -07:00
5e3add0b81 docstring 2018-05-24 15:21:38 -07:00
9ccd43c29b Merge branch 'master' into quicker_count 2018-05-24 12:57:40 -07:00
9fc6c9aaf7 Sort and page 2018-05-24 12:55:52 -07:00
2d61200a05 Merge pull request #1252 from jchuong/docker-test
Add VIRTUAL_ENV to docker-compose
2018-05-24 12:42:09 -07:00
268d826158 Add VIRTUAL_ENV to docker-compose
This fixes `make test` in the docker-compose, as `up-reqs` checks for
this envvar before installing requirements.  Since this is in a docker
container for testing, just allow pip to install without a virtualenv.

Change-Id: I511efa9fab8d393bf9f2b8e80408fb8cf155bafa
2018-05-23 17:17:23 -07:00
a47b6c330d Use serial_number instead of serial (#1251)
* Add code coverage badge to README

* fixing docs (#1231)

* Change cert.serial to serial_number

This fixes deprecation warning coming from cryptography package about
using cert.serial instead of serial_number.

Change-Id: I252820974c77cc1b80639920a5e8c2e874819dda
2018-05-23 16:04:30 -07:00
de52fa7f48 fix v1 backwards compatibility 2018-05-16 08:00:33 -07:00
680f4966a1 acme v2 support 2018-05-16 07:46:37 -07:00
a9b9b27a0b fix tests 2018-05-10 12:58:04 -07:00
52e7ff9919 Allow specification of dns provider name only 2018-05-10 12:58:04 -07:00
f4a010e505 Merge branch 'master' into master 2018-05-09 07:52:07 -07:00
0bd14488bb Update requirements, handle more lemur_acme exceptions, and remove take a tour button 2018-05-08 15:35:03 -07:00
6500559f8e Fix issue with automatically renewing acme certificates 2018-05-08 14:54:10 -07:00
642dbd4098 Merge branch 'master' into linuxdst 2018-05-08 12:09:05 -07:00
a8187d15c6 quick lint 2018-05-08 11:04:25 -07:00
df5168765b more tests 2018-05-08 11:03:17 -07:00
c26ae16060 fixing docs (#1231) 2018-05-08 10:58:48 -07:00
9ccb8fb838 Alembic simplification 2018-05-07 15:14:32 -07:00
e68b3d2cbd 0.7 release 2018-05-07 09:58:24 -07:00
1be3f8368f dyn support 2018-05-04 15:01:01 -07:00
3e64dd4653 Additional work 2018-05-04 15:01:01 -07:00
48dde287d8 Merge branch 'master' into master 2018-05-01 12:36:32 -07:00
4da2f33892 Merge pull request #1229 from seils/bugfix-1228
Fix URL for Latest Docs image
2018-04-30 10:16:49 -07:00
74ca13861c Merge branch 'master' into master 2018-04-27 11:19:23 -07:00
532872b3c6 dns_provider ui 2018-04-27 11:18:51 -07:00
d37f730ee8 Merge branch 'master' into bugfix-1228 2018-04-27 08:54:08 -07:00
5e744c4c52 Merge pull request #1230 from seils/coverage-badge
Add code coverage badge to README
2018-04-27 08:53:19 -07:00
858c4eb808 Add code coverage badge to README 2018-04-27 08:46:11 -04:00
3ffeb8ab00 Fix URL for Latest Docs image 2018-04-26 19:45:00 -04:00
0579b2935c Print variable value instead of name (#1227)
* Print variable value instead of name

* Fixed ordering and variable name for stdout string
2018-04-26 09:39:42 -07:00
c5cb01bd33 Merge branch 'master' into master 2018-04-26 09:16:31 -07:00
efd5836e43 fix test 2018-04-26 09:04:13 -07:00
f0f2092fb4 Some unit tests 2018-04-25 11:19:34 -07:00
e09b7eb978 Selectively enable CORS. (#1220) 2018-04-24 17:10:38 -07:00
3e5db9eedb Check for default rotation policy before updating db (#1223) 2018-04-24 16:55:26 -07:00
91500d1022 Minor comment & stdout corrections (#1225) 2018-04-24 16:53:51 -07:00
51d2990eb9 Merge branch 'master' of github.com:castrapel/lemur 2018-04-24 09:48:33 -07:00
38b8df4a07 lint 2018-04-24 09:48:14 -07:00
211027919f Merge branch 'master' into master 2018-04-24 09:43:20 -07:00
38c33395c8 Merge branch 'castrapel-hackday' 2018-04-24 09:41:26 -07:00
7704f51441 Working acme flow. Pending DNS providers UI 2018-04-24 09:38:57 -07:00
ae63808678 Update administration.rst (#1221) 2018-04-23 12:15:56 -07:00
81e349e07d Merge branch 'master' into hackday 2018-04-23 10:11:49 -07:00
49cdf1c7cf Merge pull request #1219 from castrapel/forcible_remove_python-ldap
reqs update
2018-04-23 09:34:39 -07:00
7e36b0e8fd comment 2018-04-23 09:26:36 -07:00
552c07e932 reqs update 2018-04-23 09:23:23 -07:00
44e3b33aaa More stuff. Will prioritize this more next week 2018-04-20 14:49:54 -07:00
a8ce219016 Readthedocs doesn't have the necessary c header files to build python-ldap. (#1215) 2018-04-19 13:57:35 -07:00
b9e93065f7 Removing the need for a separate requirements txt (#1214) 2018-04-19 13:26:49 -07:00
78f9ceb995 Merge pull request #1212 from titouanc/docs-influxdb
[add] Reference lemur-influxdb as 3rd party plugin
2018-04-16 15:18:24 -07:00
1904a187e0 Merge branch 'master' into docs-influxdb 2018-04-16 15:12:09 -07:00
0320a9aece Merge pull request #1211 from titouanc/fix-pip10
[fix] Pip imports for pip 10
2018-04-16 15:11:42 -07:00
4e94e51218 [add] Reference lemur-influxdb as 3rd party plugin 2018-04-16 20:15:25 +02:00
4392657a71 [fix] Pip imports for pip 10 2018-04-16 19:41:28 +02:00
fbce1ef7c7 temp digicert fix 2018-04-13 15:50:55 -07:00
309d10c4e2 stuff 2018-04-13 15:50:55 -07:00
f43100a247 py3 2018-04-13 15:50:55 -07:00
4d05a09a20 fix_changes 2018-04-13 15:50:55 -07:00
3538f1a629 fix_errors 2018-04-13 15:50:55 -07:00
993958c356 up-reqs 2018-04-13 15:50:55 -07:00
2d6d2357b5 DNS Providers list returned 2018-04-13 15:50:55 -07:00
a66d85b63d clean up a bit 2018-04-13 15:50:55 -07:00
b0bd0435c4 more stuff 2018-04-13 15:50:54 -07:00
b2e6938815 WIP: Add support for Acme/LetsEncrypt with DNS Provider integration 2018-04-13 15:50:54 -07:00
d66dd543bf actually update deps 2018-04-13 15:50:53 -07:00
de7a5a30d1 unpin flask in requirements.in 2018-04-13 15:50:53 -07:00
40c35dc77b Update more dependencies. Remove hashes 2018-04-13 15:50:53 -07:00
5dd03098e5 actually update deps 2018-04-13 15:50:53 -07:00
672a28bb28 Update auth keys, change python version to satisfy tests 2018-04-13 15:50:53 -07:00
8ea2f5253a unpin flask in requirements.in 2018-04-13 15:50:53 -07:00
1e0146a453 Merge pull request #1208 from castrapel/fixdate
Fix date
2018-04-13 15:39:42 -07:00
c03133622f Correct validities 2018-04-13 15:18:17 -07:00
8303cfbd2b Fix datetime 2018-04-13 14:53:45 -07:00
3ef550f738 Merge branch 'master' into hackday 2018-04-12 12:49:52 -07:00
c8767e23bf Merge pull request #1204 from castrapel/up-reqs
Up reqs
2018-04-12 12:49:00 -07:00
f302408712 py3 2018-04-12 12:34:08 -07:00
c88c0b0127 fix_changes 2018-04-12 08:59:06 -07:00
acb1eab24e fix_errors 2018-04-12 08:58:04 -07:00
6cd2205f1f up-reqs 2018-04-12 08:52:47 -07:00
f6fd262618 DNS Providers list returned 2018-04-11 15:56:00 -07:00
5125990c4c clean up a bit 2018-04-11 07:48:04 -07:00
52cb145333 ecc: add the support for ECC (#1191)
* ecc: add the support for ECC

update generate_private_key to support ECC.  Move key types to constant.  Update UI for the new key types

* ecc: Remove extra line to fix linting

* ecc: Fix flake8 lint problems

* Update options.tpl.html
2018-04-10 16:54:17 -07:00
c6bd93fe85 PostgreSQL is required, not optional due to JSON column usage, so link to quickstart instructions and add create_config statement. (#1198) 2018-04-10 16:54:02 -07:00
6a762d463f Documenting connection pool config settings (#1197) 2018-04-10 16:50:58 -07:00
5beb319b27 more stuff 2018-04-10 16:04:07 -07:00
12622d5847 Adding metrics for request timings. (#1190) 2018-04-10 15:55:02 -07:00
a9baaf4da4 add(plugins): Added a statsd plugin for lemur (#1189) 2018-04-10 15:15:03 -07:00
f61098b874 WIP: Add support for Acme/LetsEncrypt with DNS Provider integration 2018-04-10 14:28:53 -07:00
8ca4f730e8 lemur_digicert: Do not truncate valid_to anymore (#1187)
* lemur_digicert: Do not truncate valid_to anymore

The valid_to field for Digicert supports YYYY-MM-DDTHH:MM:SSZ so we should stop truncating

* lemur_digicert: Update unit tests for valid_to
2018-04-10 13:23:09 -07:00
0b5f85469c Merge pull request #1164 from intgr/fix-boolean-filter
Fix filtering on boolean columns, broken with SQLAlchemy 1.2 upgrade
2018-04-09 09:36:08 -07:00
8e2b2123f1 Fix filtering on boolean columns, broken with SQLAlchemy 1.2 upgrade
SQLAlchemy 1.2 does not allow comparing string values to boolean
columns. This caused errors like:

    sqlalchemy.exc.StatementError: (builtins.TypeError) Not a boolean value: 'true'

For more details see http://docs.sqlalchemy.org/en/latest/changelog/migration_12.html#boolean-datatype-now-enforces-strict-true-false-none-values
2018-04-09 18:59:23 +03:00
b4b9a913b3 Merge pull request #1177 from castrapel/up-reqs-4-9-2018
update requirements
2018-04-09 07:57:41 -07:00
2dc6478c34 update requirements 2018-04-09 07:46:08 -07:00
28614b5793 remove linuxdst plugin 2018-04-04 14:49:25 +03:00
4a0103a88d SFTP destination plugin (#1170)
* add sftp destination plugin
2018-04-03 10:30:19 -07:00
fb494bc32a Merge pull request #1158 from castrapel/up_reqs_3292018
Updating requirements and add makefile function to automate req updates
2018-03-29 09:30:51 -07:00
de9c00b293 Merge branch 'master' into up_reqs_3292018 2018-03-29 09:11:23 -07:00
3e5cbb40ce Updating requirements 2018-03-29 09:10:41 -07:00
47793635b2 Merge pull request #1150 from castrapel/issue_1089
Allow quotes for exact match
2018-03-29 09:07:26 -07:00
259800ce35 Merge branch 'master' into issue_1089 2018-03-29 08:48:52 -07:00
a38f286fb9 Merge pull request #1152 from castrapel/issue_1151_remove_get_pending_certificates
Remove get_pending_certificates from verisign issuer
2018-03-29 08:48:37 -07:00
b6ffbfa40e Merge branch 'master' into issue_1151_remove_get_pending_certificates 2018-03-28 10:04:37 -07:00
b814a4f009 Remove get_pending_certificates from verisign issuer 2018-03-28 08:56:28 -07:00
4ed6b7727a Merge branch 'master' into issue_1089 2018-03-28 08:35:32 -07:00
c3a2781507 Allow quotes for exact match 2018-03-28 08:33:43 -07:00
ffba1d2b85 Merge pull request #1149 from castrapel/up-deps
dep upgrades
2018-03-28 07:59:48 -07:00
248409e43f dep upgrades 2018-03-28 07:43:25 -07:00
a316cbba73 [add] Docs and default config for metric plugins (#1148) 2018-03-27 15:51:32 -07:00
12135c445b Merge pull request #1138 from castrapel/useractive
check if user active properly
2018-03-26 13:31:13 -07:00
844202f36b check if user active properly 2018-03-26 13:14:22 -07:00
9b4a124c08 Merge pull request #1137 from castrapel/req_up
Upodate reqs
2018-03-26 10:41:53 -07:00
ab1b31604c Upodate reqs 2018-03-26 10:08:23 -07:00
a8b18480aa Merge pull request #1124 from Netflix/doppins/alembic-equals-0.9.9
[Doppins] Upgrade dependency alembic to ==0.9.9
2018-03-26 09:42:10 -07:00
b5e4df5c16 Merge branch 'master' into doppins/alembic-equals-0.9.9 2018-03-26 09:08:45 -07:00
2dbcc7a297 Merge pull request #1128 from Netflix/doppins/moto-equals-1.3.1
[Doppins] Upgrade dependency moto to ==1.3.1
2018-03-26 09:08:30 -07:00
1730b3bacc Merge branch 'master' into doppins/alembic-equals-0.9.9 2018-03-26 09:00:20 -07:00
d730ffbc72 Merge branch 'master' into doppins/moto-equals-1.3.1 2018-03-26 08:59:46 -07:00
d36fececd6 Merge pull request #1135 from Netflix/doppins/python-dateutil-equals-2.7.2
[Doppins] Upgrade dependency python-dateutil to ==2.7.2
2018-03-26 08:59:12 -07:00
0caafea777 Merge branch 'master' into doppins/python-dateutil-equals-2.7.2 2018-03-26 08:33:44 -07:00
c847339b0e Merge pull request #1129 from Netflix/doppins/pytest-equals-3.5.0
[Doppins] Upgrade dependency pytest to ==3.5.0
2018-03-26 08:28:35 -07:00
58bb08b604 Upgrade dependency python-dateutil to ==2.7.2 2018-03-26 15:10:21 +00:00
98d303c6c0 Merge branch 'master' into doppins/pytest-equals-3.5.0 2018-03-26 08:07:15 -07:00
9514edafba Merge pull request #1133 from Netflix/doppins/python-dateutil-equals-2.7.1
[Doppins] Upgrade dependency python-dateutil to ==2.7.1
2018-03-26 08:06:18 -07:00
adb9149413 Upgrade dependency python-dateutil to ==2.7.1 2018-03-24 19:23:33 +00:00
c51fed5307 allowing null basic contraints (#1131) 2018-03-23 11:38:47 -07:00
db746f1296 Adds support for CDLDistributionPoints. (#1130) 2018-03-23 08:51:18 -07:00
62046aed59 Upgrade dependency pytest to ==3.5.0 2018-03-22 23:49:52 +00:00
5e0e8804c0 Upgrade dependency moto to ==1.3.1 2018-03-22 18:29:22 +00:00
416791d4c5 Merge pull request #1127 from castrapel/fixrequests2
Pin requests and pyopenssl
2018-03-22 09:33:52 -07:00
5ee11ed4e0 Merge branch 'master' into fixrequests2 2018-03-22 09:28:10 -07:00
3b2ef95798 pin requests and pyopenssl 2018-03-22 09:23:23 -07:00
827e4c65a7 pin requests and pyopenssl 2018-03-22 09:06:23 -07:00
fef89feb62 Upgrade dependency alembic to ==0.9.9 2018-03-22 14:44:00 +00:00
42f92306a5 Update more dependencies. Remove hashes 2018-03-21 15:03:28 -07:00
44b8fd6ef5 actually update deps 2018-03-21 15:03:28 -07:00
5a86ebe318 Update auth keys, change python version to satisfy tests 2018-03-21 15:03:28 -07:00
1e3df62993 [fix] No internal server error when trying to Google Auth an unregistered user (#1109) 2018-03-21 15:03:28 -07:00
662eaf4933 Remove non-ASCII character (#1104) 2018-03-21 15:03:28 -07:00
3fd82e51bd unpin flask in requirements.in 2018-03-21 15:03:28 -07:00
154e38b42e Merge pull request #1115 from castrapel/up-some-deps2
Update more dependencies. Remove hashes
2018-03-21 14:54:57 -07:00
915cdeb426 Merge branch 'master' into up-some-deps2 2018-03-21 14:49:39 -07:00
e15836e9ca Update more dependencies. Remove hashes 2018-03-21 14:48:51 -07:00
8e1eae9a45 Merge pull request #1114 from castrapel/up-some-deps
Unpin some dependencies in requirement in files, update requirements.txt
2018-03-21 13:39:39 -07:00
d67542d7f5 actually update deps 2018-03-21 12:46:30 -07:00
a202d082e8 Merge branch 'master' into up-some-deps 2018-03-21 12:43:33 -07:00
4087f1c03b Update auth keys, change python version to satisfy tests 2018-03-21 11:57:19 -07:00
bbacb7e210 [fix] No internal server error when trying to Google Auth an unregistered user (#1109) 2018-03-21 11:57:19 -07:00
19cf8f6bdd Remove non-ASCII character (#1104) 2018-03-21 11:57:19 -07:00
f05d1750ee unpin flask in requirements.in 2018-03-21 11:57:19 -07:00
fa696b56c2 Merge pull request #1103 from castrapel/flask-up
unpin flask in requirements.in
2018-03-21 10:41:03 -07:00
3f52cd9c2b Merge branch 'master' into flask-up 2018-03-21 10:31:07 -07:00
d44a1934fe Update auth keys, change python version to satisfy tests 2018-03-21 10:29:08 -07:00
08f66df860 [fix] No internal server error when trying to Google Auth an unregistered user (#1109) 2018-03-21 08:14:54 -07:00
48d9a3ec8a Remove non-ASCII character (#1104) 2018-03-20 16:54:30 -07:00
de0b4ddc99 unpin flask in requirements.in 2018-03-20 15:43:25 -07:00
6e1bb0c49c Merge pull request #1088 from castrapel/requirements3
Requirements for dev, docs, and tests
2018-03-19 11:41:38 -07:00
d4597b6bb6 typo fix 2018-03-19 11:03:47 -07:00
52f5930744 requirements for dev, docs, and tests 2018-03-19 11:02:46 -07:00
9504ad3b80 Merge pull request #1087 from castrapel/requirements2
requirements.txt
2018-03-19 10:46:37 -07:00
d2c7f8a963 Addition of requirements.txt and requirements.in with pinned versions for all dependencies, sub-dependencies, along with hashes 2018-03-19 09:39:25 -07:00
ff05deaa1f Merge pull request #1084 from Netflix/doppins/pyldap-equals-3.0.0
[Doppins] Upgrade dependency pyldap to ==3.0.0
2018-03-19 07:33:16 -07:00
b233f567ce Merge branch 'master' into doppins/pyldap-equals-3.0.0 2018-03-19 07:21:57 -07:00
b30d2c9536 Merge pull request #1083 from Netflix/doppins/paramiko-equals-2.4.1
[Doppins] Upgrade dependency paramiko to ==2.4.1
2018-03-19 07:21:09 -07:00
5dd37ea696 Merge branch 'master' into doppins/pyldap-equals-3.0.0 2018-03-19 07:10:01 -07:00
49393070e0 Merge branch 'master' into doppins/paramiko-equals-2.4.1 2018-03-19 07:09:56 -07:00
fdb6dd4077 Merge pull request #1086 from Netflix/revert_req_logging
Revert req logging
2018-03-16 14:26:15 -07:00
74a516cde0 nt 2018-03-16 14:15:03 -07:00
58da68d72f Revert "Requirements and Elasticsearch logging configuration"
This reverts commit c08d3dd82f.
2018-03-16 14:10:12 -07:00
918250ce78 Merge pull request #1085 from castrapel/requirements_logging
Requirements and logging
2018-03-16 12:14:10 -07:00
c7ca3949f6 info level, and new variable name 2018-03-16 11:55:53 -07:00
bbf5e95186 fix unusued import 2018-03-16 10:07:47 -07:00
462e757f92 Merge branch 'master' into requirements_logging 2018-03-16 08:51:25 -07:00
58798f1513 undo gitignore change 2018-03-16 08:41:12 -07:00
087490e26a add newlines 2018-03-16 08:38:59 -07:00
c08d3dd82f Requirements and Elasticsearch logging configuration 2018-03-16 08:36:10 -07:00
430cb5ea1b Upgrade dependency pyldap to ==3.0.0 2018-03-14 12:32:14 +00:00
9b1c279fd5 Upgrade dependency paramiko to ==2.4.1 2018-03-13 01:40:29 +00:00
17be8b626d EntryPoints digicert_cis_source missing comma (#1082) 2018-03-10 09:44:51 -08:00
412757b178 Merge pull request #1071 from Netflix/notif-fix
Fix cloned notifications
2018-02-27 13:14:58 -08:00
18c64fafe4 address comment 2018-02-27 12:34:18 -08:00
77a1600c13 Fix cloned notifications 2018-02-27 10:57:43 -08:00
59ce586ea4 Merge pull request #1069 from Netflix/pending_fix
comments on alembic changes. resolve invalid usage of log_service.create
2018-02-26 12:44:21 -08:00
5fe28f6503 Description modification 2018-02-26 12:37:31 -08:00
1f641c0ba6 Description modification 2018-02-26 12:36:40 -08:00
cca3797669 comments on alembic changes. resolve invalid usage of log_service.create 2018-02-26 12:08:31 -08:00
c9cb5800ec Merge pull request #1068 from Netflix/pending_fix
fix pending cert db changes
2018-02-26 09:52:07 -08:00
a28fdac242 fix pending cert db changes 2018-02-26 09:43:08 -08:00
0724fcffeb Merge pull request #1067 from Netflix/unq-const
unq constraint
2018-02-26 08:34:46 -08:00
7032abf2e7 Merge branch 'master' into unq-const 2018-02-26 08:03:31 -08:00
9e8fa5827d unq constraint 2018-02-24 23:15:39 -08:00
5d18838868 Use Cloudflare as DNS provider for LE certs (#945)
* Use Cloudflare as DNS provider for LE certs

* Better handle dns_provider plugins
2018-02-22 08:17:28 -08:00
2578970f7d Async Certificate Issuing using Pending Certificates (#1037)
* Add PendingCertificate model

This change creates a DB table called pending_certificates and
associated mapping relationship tables from pending certificate to
roles, rotation policy, destination, sources, etc.

The table is generated on initialization of Lemur. A pending
certificate holds most of the information of a Certificate, while it has
not be issued so that it can later backfill the information when the CA
has issued the certificate.

Change-Id: I277c16b776a71fe5edaf0fa0e76bbedc88924db0
Tickets: PBL-36499

* Create a PendingCertificate if cert is empty

IssuePlugins should return empty cert bodies if the request failed to
complete immediately (such as Digicert).  This way, we can immediately
return the certificate, or if not just place into PendingCertificates
for later processing.

+ Fix relation from Certificate to Pending Certificate, as view only.
There is no real need for anything more than that since Pending cert
only needs to know the cert to replace when it is issued later.

+ Made PendingCertificate private key be empty: UI does not allow
private key on 'Create' but only on 'Import'.  For Instart, we require
the private key but upstream does not necessarily need it.  Thus, if
someone at Instart wants to create a CSR / key combo, they should
manually issue the cert themselves and import later.  Otherwise you
should let Lemur generate that.  This keeps the workflow transparent for
upstream Lemur users.

Change-Id: Ib74722a5ed5792d4b10ca702659422739c95ae26
Tickets: PBL-36343

* Fix empty private_key when create Pending Cert

On creation of a certificate with a CSR, there is no option for private
key.  In this case, we actually have a dictionary with private_key as
key, but the value is None.  This fixes the strip() called on NoneType.

Change-Id: I7b265564d8095bfc83d9d4cd14ae13fea3c03199
Tickets: PBL-36499

* Source sync finds and uses pending certificate

When a source syncs certificates, it will check for a pending
certificate.  If that is found via external_id (given by digicert as
order_id) then it will use the found Pending Certificate's fields to
create a new certificate.  Then the pending certificate is deleted.

Tickets: PBL-36343
Change-Id: I4f7959da29275ebc47a3996741f7e98d3e2d29d9

* Add Lemur static files and views for pending certs

This adds the basic static files to view pending certificates in a
table.

Tickets: PBL-36343
Change-Id: Ia4362e6664ec730d05d280c5ef5c815a6feda0d9

* Add CLI and plugin based pending fetch

This change uses the adds a new function to issuer plugins to fetch
certificates like source, but for one order.  This way, we can control
which pending certificates to try and populate instead of getting all
certificates from source.

Tickets: PBL-36343
Change-Id: Ifc1747ccdc2cba09a81f298b31ddddebfee1b1d6

* Revert source using Pending Certificate

Tickets: PBL-36343
Change-Id: I05121bc951e0530d804070afdb9c9e09baa0bc51

* Fix PendingCertificate init getting authority id

Should get authority id from authority.id instead of the authority_id
key in kwargs.

Change-Id: Ie56df1a5fb0ab2729e91050f3ad1a831853e0623
Tickets: n/a

* Add fixtures and basic test for PendingCertificate

Change-Id: I4cca34105544d40dac1cc50a87bba93d8af9ab34
Tickets: PBL-36343

* Add User to create_certificate parameters

create_certificate now takes a User, which will be used to populate the
'creator' field in certificates.service.upload().  This allows the UI
populate with the current user if the owner does not exist in Lemur.

+ Fix chain being replaced with version from pending certificate, which
may be empty (depends on plugin implementation).

Change-Id: I516027b36bc643c4978b9c4890060569e03f3049
Tickets: n/a

* Fix permalink and filters to pending certs

Fixes the permalink button to get a single pending certificate
Add argument filter parsing for the pending certificate API
Fix comment on API usage
Added get_by_name for pending_certificate (currently unused, but useful
for CLI, instead of using IDs)

Change-Id: Iaa48909c45606bec65dfb193c13d6bd0e816f6db
Tickets: PBL-36910

* Update displayed fields for Pending Certificates

There are a number of unused / unpopulated fields from Certificate UI
that does apply to Pending Certificates.  Those ones were removed, and
added other useful fields:
Owner, number of attempts to fetch and date created

Change-Id: I3010a715f0357ba149cf539a19fdb5974c5ce08b
Tickets: PBL-36910

* Add common name (cn) to Pending Certificate model

Fixes the UI missing the CN for Pending Certificate, as it was
originally being parsed from the generated certificate.  In the case of
pending certificate, the CN from the user generates the request, which
means a pending cert can trust the original user putting in the CN
instead of having to parse the not-yet-generated certificate.  There is
no real possibility to return a certificate from a pending certificate
where the CN has changed since it was initially ordered.

Change-Id: I88a4fa28116d5d8d293e58970d9777ce73fbb2ab
Tickets: PBL-36910

* Fix missing imports for service filter

+ Removed duplicate get_by_name function from old merge

Change-Id: I04ae6852533aa42988433338de74390e2868d69b
Tickets: PBL-36910

* Add private key viewing to Pending Certificates

Add private key API for Pending Certificates, with the same
authorization as Certificates (only owner, creator or owner-roles can
view private key).

Change-Id: Ie5175154a10fe0007cc0e9f35b80c0a01ed48d5b
Tickets: PBL-36910

* Add edit capability to pending certificates

Like editing certificates, we should be able to modify some parts of a
pending certificate so the resulting certificate has the right
references, owner, etc.

+ Added API to update pending certificate
+ Fix UI to use pending certificate scope instead of reusing Certificate
+ Change pending_certificate.replaces to non-passive association, so
that updates do affect it (similar to roles/notifications/etc)

Tickets: PBL-36910
Change-Id: Ibbcb166a33f0337e1b14f426472261222f790ce6

* Add common_name parsing instead using kwargs

To fix tests where common name may not be passed in, use the CSR
generated to find the official common name.

Change-Id: I09f9258fa92c2762d095798676ce210c5d7a3da4
Tickets: PBL-36343

* Add Cancel to pending certificates and plugins

This allows pending certificates to be cancelled, which will be handled
by the issuer plugin.

Change-Id: Ibd6b5627c3977e33aca7860690cfb7f677236ca9
Tickets: PBL-36910

* Add API for Cancelling Pending Certificate

Added the DELETE handler for pending_certificates, which will cancel and
delete the pending certificate from the pending certs table on
successful cancellation via Issuer Plugin.

+ Add UT for testing cancel API

Change-Id: I11b1d87872e4284f6e4f9c366a15da4ddba38bc4
Tickets: PBL-36910

* Remove Export from Pending Certificates

Pending Certificates doesn't need an export since it should just be
fetched by Lemur via plugins, and the CSR is viewable via the UI.

Change-Id: I9a3e65ea11ac5a85316f6428e7f526c3c09178ae
Tickets: PBL-36910

* Add cancel button functionality to UI

This adds the Cancel option to the dropdown of pending certificates.

+ Adds modal window for Note (may not be required for all issuers, just
Digicert)
+ Add schema for cancel input
+ Fix Digitcert plugin for non-existant orders

When an order is actually issued, then attempting to cancel will return
a 403 from Digicert.  This is a case where it should only be done once
we know the pending cert has been sitting for too long.

Change-Id: I256c81ecd142dd51dcf8e38802d2c202829887b0
Tickets: PBL-36910

* Fix test_pending_cancel UT

This change creates and injects a pending cert, which will then be used
for the ID so it can be canceled by the unit test.

Change-Id: I686e7e0fafd68cdaeb26438fb8504d79de77c346
Tickets: PBL-36343

* Fix test_digicert on non-existent order

cancelling a non-existent order is fine since we're cancelling it

Change-Id: I70c0e82ba2f4b8723a7f65b113c19e6eeff7e68c
Tickets: PBL-36343

* Add migrations for PendingCertificates

Added revision for Pending Certificates table and foreign key mapping
tables.

Change-Id: Ife8202cef1e6b99db377851264639ba540b749db
Tickets: n/a

* Fix relationship copy from Pending to Certificate

When a Pending Certificate is changed to a full Certificate, the
relationship fields are not copied via vars() function, as it's not a
column but mapped via association table.  This adds an explicit copy for
these relations.  Which will properly copy them to the new Certificate,
and thus also update destinations.

Change-Id: I322032ce4a9e3e67773f7cf39ee4971054c92685
Tickets: PBL-36343

* Fix renaming of certificates and unit tests

The rename flag was not used to rename certificates on creation as
expected.

Fixed unit test, instead of expunging the session, just copy the
pending_certificate so we don't have a weird reference to the object
that can't be copied via vars() function.

Change-Id: I962943272ed92386ab6eab2af4ed6d074d4cffa0
Tickets: PBL-36343

* Updated developer docs for async certs

Added blurb for implementing new issuer functions.

Change-Id: I1caed6e914bcd73214eae2d241e4784e1b8a0c4c
Tickets: n/a
2018-02-22 08:13:16 -08:00
f44fe81573 fix for https://github.com/Netflix/lemur/issues/1045 (#1056) 2018-02-20 08:28:11 -08:00
aa5d97f49b Update setup.py (#1055) 2018-01-26 10:38:30 -08:00
f262c93912 Option to suppress SSL errors (#1044) 2018-01-17 09:17:03 -08:00
763c5e8356 Add DIGICERT_ORDER_TYPE to Digicert plugin (#1025)
* Add DIGICERT_ORDER_TYPE to Digicert plugin

This allows lemur.conf.py to control which kind of certificate to
order.  User defined options are not currently supported in the the UI,
so we cannot create multiple Digicert authorities at runtime for
separate certificate types.

Change-Id: I06c216ec3c476e0001b240530626a86464be999e

* Fix Mock URL for Digicert test

Change-Id: Ida7c0ed1bd120c9024bea091c03b7d1ecfa66498

* Add documentation for DIGICERT_ORDER_TYPE

Change-Id: I0bc347883b628416eb7f13a7c60c937dcb6ae0c2
2018-01-13 18:06:17 -08:00
050295ea20 Fix DigiCert issuer plugin revoke URL (#1041)
The URL for revoking DigiCert certificates was incorrect.

Change-Id: I39fb7d290a2a649ab08a47e7dcbe18a8c0bd8a59
2018-01-11 17:12:21 -08:00
77044f56fc Upgrade dependency pytest to ==3.3.2 (#1038) 2018-01-04 17:06:24 -08:00
eea413a90f Modifying the way we report metrics. Relying on metric tags instead of the the metric name for additional dimensions. (#1036) 2018-01-02 15:26:31 -08:00
8cad2f9f56 Version bump. (#1034) 2018-01-02 14:08:56 -08:00
64ac32f683 6.0 release. (#1033) 2018-01-02 14:03:38 -08:00
1287c3dc4a CRL verify: handle "Remove from CRL" status as not revoked (#1028)
Per RFC 5280 section 6.3.3 (k):
https://tools.ietf.org/html/rfc5280#section-6.3.3
2018-01-02 13:39:02 -08:00
9d7fc9db8c [Doppins] Upgrade dependency boto3 to ==1.5.7 (#1024) 2018-01-02 13:11:44 -08:00
99b10c436a CRL verify: skip unknown URI schemes like ldap:// and add unit tests (#1027) 2018-01-02 13:11:17 -08:00
bb54085c20 Downgrading library again. (#1032) 2018-01-02 12:36:45 -08:00
9a0ada75fa Upgrading satellizer library. (#1031) 2018-01-02 09:12:06 -08:00
848ce8c978 Refactoring authentincation to support GET and POST requests. Closes #990. (#1030) 2018-01-01 19:11:29 -08:00
7b8df16c9e Fix typo in default SSH key path. (#1026) 2017-12-20 09:09:56 -08:00
7a84f38db9 Don't write files from the test suite (#1020)
The lemur_email.tests.test_render test would fail when running unittests
from a read-only source tree.
2017-12-12 10:14:39 -08:00
ba4de07ad8 Improve certificate details view, make information more concise (#1021)
The "Description" field can now display multi-line text content.

The "Authority" field now displays the authority name in Lemur (if
known) as well as issuer's name. For imported certs, "Imported" is
displayed.
2017-12-12 09:49:30 -08:00
b2d87940d6 Allow sorting and filtering by camelCase field names (#1019)
The API exposes camelCase field names everywhere, but only accepted
underscore_field_names in 'filter' or 'sort' GET attributes. Now both
are allowed.
2017-12-12 09:44:53 -08:00
6edc5180c7 fix roles assigned in the ui for sso (#1017)
This commit fixes the ability to assign roles to people in the ui
when the user is SSO. The idea is if a role is ever assigned via
SSO it becomes a "SSO Role" or a "Third Party" Role. by setting
third_party to true on the role object.

Once a role is marked as third party it can no longer be controlled
through the ui for SSO Users. (for ui users this poses no functional
change). It must be controlled via SSO.
2017-12-11 13:51:45 -08:00
f0c895a008 Upgrade dependency acme to ==0.20.0 (#1016) 2017-12-07 09:12:21 -08:00
6d6716b8a2 Upgrade dependency requests-mock to ==1.4.0 (#1013) 2017-12-07 09:12:08 -08:00
d64a010c39 Upgrade dependency pytest to ==3.3.1 (#1014) 2017-12-07 09:11:51 -08:00
e1f241bd55 Don't send notifications that are marked inactive (#1015)
Apparently previously Lemur ignored the "active" flag of notifications.
2017-12-06 08:32:24 -08:00
ad88637f22 Adding some niceties around the way users are associated with tokens. (#1012)
* Adding some niceties around the way users are associated with tokens.

- Includes user typeahead
- Tooltips
- User information displayed in table
- Default to current user when no user is passed
2017-12-05 10:57:17 -08:00
a756a74b49 Ensures we can get multiple endpoints with the same name but different ports. (#1011) 2017-12-04 13:13:02 -08:00
c311c0a221 Pinning JS versions (#1010) 2017-12-04 11:29:02 -08:00
ecc0934657 Adding cli command to clear out pending symantec certificates. (#1009) 2017-12-04 10:04:12 -08:00
c402f1ff87 add per user api keys to the backend (#995)
Adds in per user api keys to the backend of lemur.
the basics are:
  - API Keys are really just JWTs with custom second length TTLs.
  - API Keys are provided in the exact same ways JWTs are now.
  - API Keys can be revoked/unrevoked at any time by their creator
    as well as have their TTL Change at anytime.
  - Users can create/view/list their own API Keys at will, and
    an admin role has permission to modify all api keys in the
    instance.

Adds in support for lemur api keys to the frontend of lemur.
doing this required a few changes to the backend as well, but it is
now all working (maybe not the best way though, review will determine
that).

  - fixes inconsistency in moduleauthor name I inputted during the
    first commit.
  - Allows the revoke schema to optionally allow a full api_key object.
  - Adds `/users/:user_id/api_keys/:api_key` and `/users/:user_id/api_keys`
    endpoints.
  - normalizes use of `userId` vs `userId`
  - makes `put` call respond with a JWT so the frontend can show
    the token on updating.
  - adds in the API Key views for clicking "API Keys" on the main nav.
  - adds in the API Key views for clicking into a users edit page.
  - adds tests for the API Key backend views I added.
2017-12-04 08:50:31 -08:00
eb810f1bf0 Upgrade dependency jinja2 to ==2.10 (#992) 2017-12-03 10:08:13 -08:00
c067573193 Upgrade dependency paramiko to ==2.4.0 (#994) 2017-12-03 10:07:30 -08:00
553c119356 Upgrade dependency SQLAlchemy-Utils to ==0.32.21 (#991) 2017-12-03 10:06:32 -08:00
e62cb1b6b8 Upgrade dependency boto3 to ==1.4.8 (#1002) 2017-12-03 10:06:10 -08:00
4da243a59e Upgrade dependency moto to ==1.1.25 (#997) 2017-12-03 10:05:44 -08:00
622192e75e [Doppins] Upgrade dependency pytest to ==3.3.0 (#993)
* Upgrade dependency pytest to ==3.3.0
2017-12-03 10:05:31 -08:00
81a6ec644a Upgrade dependency tabulate to ==0.8.2 (#1006) 2017-12-03 10:05:05 -08:00
58100cda8b Upgrade dependency arrow to ==0.12.0 (#1005) 2017-12-03 10:04:51 -08:00
734ab5f3cd Upgrade dependency pyldap to ==2.4.45 (#998) 2017-12-03 10:02:28 -08:00
d855f752c8 Upgrade dependency marshmallow to ==2.15.0 (#1008) 2017-12-03 09:33:18 -08:00
5ac3ecb85e Added revoke support to cfssl plugin (#1007)
* Added revoke support to cfssl plugin
2017-11-29 14:33:22 -08:00
dfb9e3a0c8 Add nodejs-legacy to provide the 'node' command (#1004)
Affecting Ubuntu 16.04.3 LTS:

Following the directions of http://lemur.readthedocs.io/en/latest/quickstart/index.html, the make release command fails as the command 'node' cannot be found.

Adding nodejs-legacy solves the issue and allows the build to complete.

(lemur) lemur@lemur1:/www/lemur$ make release
--> Installing dependencies
npm install
npm WARN deprecated gulp-minify-css@1.2.4: Please use gulp-clean-css
npm WARN deprecated bower@1.8.2: ...psst! Your project can stop working at any moment because its dependencies can change. Prevent this by migrating to Yarn                                                                                 : https://bower.io/blog/2017/how-to-migrate-away-from-bower/
npm WARN deprecated gulp-foreach@0.1.0: Either use gulp-tap or gulp-flatmap, depending on your needs
npm WARN deprecated express@2.5.11: express 2.x series is deprecated
npm WARN deprecated connect@1.9.2: connect 1.x series is deprecated
npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN deprecated graceful-fs@1.2.3: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to graceful-fs@^4.0.0 as soon as poss                                                                                 ible. Use 'npm ls graceful-fs' to find it in the tree.
npm WARN deprecated node-uuid@1.4.8: Use uuid module instead
npm WARN deprecated minimatch@0.3.0: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue
npm WARN prefer global marked@0.3.6 should be installed with -g

> optipng-bin@3.1.4 postinstall /www/lemur/node_modules/optipng-bin
> node lib/install.js

sh: 1: node: not found
npm WARN install:optipng-bin@3.1.4 optipng-bin@3.1.4 postinstall: `node lib/install.js`
npm WARN install:optipng-bin@3.1.4 spawn ENOENT

> jpegtran-bin@3.2.0 postinstall /www/lemur/node_modules/jpegtran-bin
> node lib/install.js

sh: 1: node: not found
npm WARN install:jpegtran-bin@3.2.0 jpegtran-bin@3.2.0 postinstall: `node lib/install.js`
npm WARN install:jpegtran-bin@3.2.0 spawn ENOENT

> gifsicle@3.0.4 postinstall /www/lemur/node_modules/gifsicle
> node lib/install.js

sh: 1: node: not found
npm WARN install:gifsicle@3.0.4 gifsicle@3.0.4 postinstall: `node lib/install.js`
npm WARN install:gifsicle@3.0.4 spawn ENOENT

> Lemur@ postinstall /www/lemur
> bower install --allow-root --config.interactive=false

/usr/bin/env: ‘node’: No such file or directory


Makefile:24: recipe for target 'release' failed
make: *** [release] Error 1
(lemur) lemur@lemur1:/www/lemur$ which node
(lemur) lemur@lemur1:/www/lemur$

Installing the package to solve the issue.

vsnine@lemur1:~$ sudo apt-get install nodejs-legacy
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
  nodejs-legacy
0 upgraded, 1 newly installed, 0 to remove and 79 not upgraded.
Need to get 27.7 kB of archives.
After this operation, 81.9 kB of additional disk space will be used.
Get:1 http://ca.archive.ubuntu.com/ubuntu xenial-updates/universe amd64 nodejs-legacy all 4.2.6~dfsg-1ubuntu4.1 [27.7 kB]
Fetched 27.7 kB in 0s (52.4 kB/s)
Selecting previously unselected package nodejs-legacy.
(Reading database ... 73230 files and directories currently installed.)
Preparing to unpack .../nodejs-legacy_4.2.6~dfsg-1ubuntu4.1_all.deb ...
Unpacking nodejs-legacy (4.2.6~dfsg-1ubuntu4.1) ...
Processing triggers for man-db (2.7.5-1) ...
Setting up nodejs-legacy (4.2.6~dfsg-1ubuntu4.1) ...
vsnine@lemur1:~$ which node
/usr/bin/node
vsnine@lemur1:~$
2017-11-27 09:37:14 -08:00
c2b2ce1f11 Allowing the export of CAs that don't have a chain. (#1000) 2017-11-21 11:42:23 -08:00
cecfe47540 Adding the ability to revoke enmasse (#999) 2017-11-21 09:36:10 -08:00
4b544ae207 CSR Export Plugin (#988)
This plugin allows a certificate to be exported as a CSR via OpenSSL
x509.  The workflow will be:
* Create self-signed cert via Cryptography authority
* Export CSR via this plugin
* Sign your own cert outside of Lemur
* Import new cert with private key

Change-Id: Id3f7db2506bd959236cd3a6df622841058abda5a
2017-11-14 10:11:06 -08:00
e30e17038b Removing unused import. (#989) 2017-11-14 09:24:26 -08:00
7e2c16ee38 Fixes for using ACME with Route53 (#986)
* Changes required for functional Route53 operations

* Changes required for functional ACME operations with Route53

* Changes required for functional ACME operations with Route53, need external ID
2017-11-13 10:19:54 -08:00
041f3a22fa Added ability to set custom roles for users logging in via oauth provider (#985) 2017-11-10 08:38:33 -08:00
f990ef27cf Adding sentry tracking to issued with certificate deployment. (#978) 2017-10-26 15:21:13 -07:00
bef762e0d6 Upgrade dependency acme to ==0.19.0 (#957) 2017-10-25 13:44:01 -07:00
0d001b358e [Doppins] Upgrade dependency tabulate to ==0.8.1 (#953)
* Upgrade dependency tabulate to ==0.8.0

* Upgrade dependency tabulate to ==0.8.1
2017-10-25 11:19:25 -07:00
c1cd5c71e0 [Doppins] Upgrade dependency moto to ==1.1.19 (#946)
* Upgrade dependency moto to ==1.1.18

* Upgrade dependency moto to ==1.1.19
2017-10-25 11:06:36 -07:00
d4209510c2 Adding some additional exception capturing during certificate parsing. (#976) 2017-10-25 08:19:07 -07:00
620e279453 Caa (#975)
* Adding verisign error code for a CAA failure.

* Tweaking error msg.
2017-10-24 14:46:33 -07:00
bbf73c48a3 Adding health exception tracking. (#977) 2017-10-24 14:04:51 -07:00
9319dda0ec Added ability to ignore cert for oauth2 provider (#971)
* Added ability to ignore cert for oauth2 provider

This is useful for development environments where the OAuth provider
doesn't have a valid cert!

* Setting default for OAUTH2_VERIFY_CERT to true
2017-10-20 16:36:14 -07:00
14f5340802 During higher loads, retrying the connection attempt is often required for the CIS api. (#972) 2017-10-12 10:37:58 -07:00
0152985e64 Adding serial numbers when certificates with the same name are encoun… (#970)
* Adding serial numbers when certificates with the same name are encountered.
2017-10-11 13:20:19 -07:00
e43268f585 Source plugin (#965)
* Ensure that None values aren't passed.
2017-10-09 10:37:44 -07:00
7ef788752e Source plugin (#964)
* Another minor fix.
2017-10-06 17:39:31 -07:00
b66d7ce1fd Source plugin (#963)
* Ensuring that we have default options for source plugins.

* Handle duplicate serials. Serials are not unique across issuers.

* Minor fix.
2017-10-06 13:22:03 -07:00
dc34652efd Source plugin (#962)
* Ensuring that we have default options for source plugins.

* Handle duplicate serials. Serials are not unique across issuers.
2017-10-06 08:49:05 -07:00
e0d2fb0de1 Ensuring that we have default options for source plugins. (#961) 2017-10-05 17:27:45 -07:00
e0d9443141 Ensuring existing users are also given the default role. (#960) 2017-10-05 16:47:52 -07:00
a6305a5cae Adding Digicert CIS Sourceplugin (#959)
* Adding necessary features to complete backfill

* Fixing pagination logic.
2017-10-04 16:56:01 -07:00
9e2578be1e Adding necessary features to complete backfill (#958) 2017-10-04 14:57:57 -07:00
09b8f532a7 Adding cli to mass revoke certificates. (#955) 2017-10-03 10:51:53 -07:00
e0939a2856 Adding some default data to put. (#950) 2017-09-29 14:49:07 -07:00
90f4b458e3 Adding the lemur identity to be able to re-issue certificates. (#949) 2017-09-29 14:07:40 -07:00
f5213deb67 Removing revocation comments for now. (#947) 2017-09-29 10:53:15 -07:00
bb08b1e637 Initial work allowing certificates to be revoked. (#941)
* Initial work allowing for certificates to be revoked.
2017-09-28 18:27:56 -07:00
ea6f5c920b Update index.rst (#942)
Fixed typo for libsasl3-dev (was libsas13-dev).
2017-09-27 09:44:03 -07:00
54ff4cddbf Disallow issuing certificates from inactive authority (#936) 2017-09-25 15:34:49 -07:00
645641f4bd Avoid redundant key_view log entries (#937)
Don't re-request private key when it's already loaded in frontend.
2017-09-25 15:34:07 -07:00
97d83890e0 Various minor cleanups and fixes (#938)
* Documentation fixes

* Various docstring and help string fixes

* Minor code cleanups

* Removed redundant .gitignore entry, ignored package-lock.json.
* 'return' statement in certificates.service.render was redundant
* Split up too long line
* Non-matching tags in templates
2017-09-25 15:33:42 -07:00
ec5dec4a16 Add option to disable owner email address in CSR subject (#939) 2017-09-25 15:32:08 -07:00
4cfb621423 Upgrade dependency moto to ==1.1.14 (#940) 2017-09-25 15:31:39 -07:00
c381331c10 Upgrade dependency pyjwt to ==1.5.3 (#901) 2017-09-25 09:19:54 -07:00
a7923f2a06 Upgrade dependency six to ==1.11.0 (#926) 2017-09-25 09:19:40 -07:00
e5f7172c97 [Doppins] Upgrade dependency paramiko to ==2.3.1 (#927)
* Upgrade dependency paramiko to ==2.3.0

* Upgrade dependency paramiko to ==2.3.1
2017-09-25 09:19:24 -07:00
43fff0450b Upgrade dependency acme to ==0.18.2 (#928) 2017-09-25 09:19:08 -07:00
107fd3fce1 [Doppins] Upgrade dependency raven to ==6.2.1 (#933)
* Upgrade dependency raven to ==6.2.0

* Upgrade dependency raven to ==6.2.1
2017-09-25 09:18:57 -07:00
1a9b6dec26 [Doppins] Upgrade dependency moto to ==1.1.13 (#931)
* Upgrade dependency moto to ==1.1.12

* Upgrade dependency moto to ==1.1.13
2017-09-25 09:18:40 -07:00
444be5bb7f Updated Quikstart (#934)
Got some failures doing a clean install on Ubuntu 17.04 Zesty Zapus (Final) from virtualboxes.org
2017-09-22 12:35:25 -07:00
5ebfa018ee [Doppins] Upgrade dependency moto to ==1.1.11 (#922)
* Upgrade dependency moto to ==1.1.7

* Upgrade dependency moto to ==1.1.8

* Upgrade dependency moto to ==1.1.9

* Upgrade dependency moto to ==1.1.10

* Upgrade dependency moto to ==1.1.11
2017-09-21 10:31:45 -07:00
a6dab5e1ee a bit more ldap documentaion (#930) 2017-09-21 06:00:26 -07:00
f766871824 Create default rotation policy with name (#924) 2017-09-18 09:09:59 -07:00
ba29bbe3be Upgrade dependency pyOpenSSL to ==17.2.0 (#918) 2017-09-13 20:54:54 -07:00
d711031ce9 Upgrade dependency moto to ==1.1.6 (#919) 2017-09-13 20:54:43 -07:00
af5c19cc52 Solving conflicts 2017-09-13 09:41:19 -07:00
359fbd2d73 Pinning version of PyOpenSSL #873 2017-09-13 09:39:52 -07:00
e8b9853367 Fixes 873 by explicitly declaring pyopenssl version. (#917) 2017-09-13 09:30:20 -07:00
376b2b8051 Upgrade dependency moto to ==1.1.5 (#916) 2017-09-12 16:01:24 -07:00
e8d0af87e4 Upgrade dependency SQLAlchemy-Utils to ==0.32.16 (#895) 2017-09-12 09:59:49 -07:00
a4267320b0 Upgrade dependency Flask-Script to ==2.0.6 (#900) 2017-09-12 09:59:23 -07:00
52dd42701a Upgrade dependency moto to ==1.1.4 (#915) 2017-09-12 09:58:38 -07:00
fc9b1e5b12 server_default from "False" to sa.false() (#913) 2017-09-11 09:19:19 -07:00
2ecfaa41cf Add pyldap mock for readthedocs (#912) 2017-09-11 09:18:03 -07:00
7106c4fdcf Sync docs requirements.txt (#910) 2017-09-10 10:41:46 -07:00
9420ca9949 Upgrade dependency acme to ==0.18.1 (#908) 2017-09-08 16:59:49 -07:00
956a1851a2 Upgrade dependency moto to ==1.1.3 (#909) 2017-09-08 16:59:39 -07:00
dafed86179 Improve certificate name normalization: remove Unicode characters, etc. (#906)
* Accented characters are replaced with non-accented version (ä -> a)
* Spaces are replaced with '-' (previously they were removed)
* Multiple non-alphanumeric characters are collapsed into one '-'
2017-09-08 10:52:22 -07:00
e72efce071 Upgrade dependency acme to ==0.18.0 (#902) 2017-09-07 18:09:52 -07:00
77b9658dba Upgrade dependency pyldap to ==2.4.37 (#903) 2017-09-07 18:09:37 -07:00
090c984ca3 Upgrade dependency pytest to ==3.2.2 (#904) 2017-09-07 18:09:15 -07:00
2ff25b656f Upgrade dependency moto to ==1.1.2 (#905) 2017-09-07 18:09:07 -07:00
ff4d1edd63 remove duplicated ldap_bind_uri description (#898) 2017-09-04 10:12:40 -07:00
79d12578c7 basic ldap support (#842) 2017-09-03 20:41:43 -07:00
c0784b40e0 Upgrade dependency Flask-Migrate to ==2.1.1 (#892) 2017-08-29 20:20:39 -07:00
ff87c487c8 It's too expensive to attempt to load all certificates associated with a given notification. Some queries such as default are associated with a large number of certificates. We have little control over when these objects are loaded, but when marshalled they are lazyloaded via SQLAlachemy. If a user needs to get all the certificates associated with a certificate they should use the /notifications/<id>/certificates endpoints that support pagination. (#891) 2017-08-28 17:57:39 -07:00
82b43b5a9d Create signal hooks and handler for dumping CSR and certificate details (#882) 2017-08-28 17:35:56 -07:00
4b4e159a8e [Doppins] Upgrade dependency moto to ==1.1.1 (#888)
* Upgrade dependency moto to ==1.1.0

* Upgrade dependency moto to ==1.1.1
2017-08-28 17:35:12 -07:00
bb1c339655 Fix ability to remove all roles from authority (#880) 2017-08-28 17:35:01 -07:00
aca6d6346f Removing legacy requirement for nodejs. Closes #866 (#887) 2017-08-25 10:12:56 -07:00
e7efaf4365 Prevent creation of empty SubjAltNames extension in CSR (#883) 2017-08-18 09:10:56 -07:00
c6d76f580e Disable unused Flask Principal sessions (#881)
Lemur uses its own auth token for authentication; logging out doesn't
properly dispose of the Flask Principal session.
2017-08-17 09:24:35 -07:00
941df0366d Fix roles display on user screen and fix removing user roles (#879) 2017-08-17 09:24:10 -07:00
7762d6ed52 Reworked sensitive domain name and restriction logic (#878)
* This is a fix for a potential security issue; the old code had edge
  cases with unexpected behavior.
* LEMUR_RESTRICTED_DOMAINS is no more, instead LEMUR_WHITELISTED_DOMAINS
  is a list of *allowed* domain name patterns. Per discussion in PR #600
* Domain restrictions are now checked everywhere: in domain name-like
  CN (common name) values and SAN DNSNames, including raw CSR requests.
* Common name values that contain a space are exempt, since they cannot
  be valid domain names.
2017-08-16 19:24:49 -07:00
466df367e6 Upgrade dependency boto3 to ==1.4.6 (#874) 2017-08-16 09:56:22 -07:00
b0c8787cfa Upgrade dependency marshmallow to ==2.13.6 (#877) 2017-08-16 09:56:08 -07:00
cf805f530f Prevent unintended access to sensitive fields (passwords, private keys) (#876)
Make sure that fields specified in filter, sortBy, etc. are model fields
and may be accessed. This is fixes a potential security issue.

The filter() function allowed guessing the content of password hashes
one character at a time.

The sort() function allowed the user to call an arbitrary method of an
arbitrary model attribute, for example sortBy=id&sortDir=distinct would
produce an unexpected error.
2017-08-16 09:38:42 -07:00
b40c6a1c67 Upgrade dependency pem to ==17.1.0 (#872) 2017-08-10 15:08:11 -07:00
3a62010445 Upgrade dependency pytest to ==3.2.1 (#871) 2017-08-09 15:00:15 -07:00
3b4e7d9169 Fixed typo (#870) 2017-08-09 08:40:22 -07:00
4245ba0d15 Upgrade dependency acme to ==0.17.0 (#866) 2017-08-06 11:19:10 -07:00
95e4c23db1 Upgrade dependency factory-boy to ==2.9.2 (#868) 2017-08-06 11:19:00 -07:00
f5e120ad2e Update readme.txt (#869) 2017-08-04 12:42:27 -07:00
fab146b328 [Doppins] Upgrade dependency factory-boy to ==2.9.1 (#863)
* Upgrade dependency factory-boy to ==2.9.0

* Upgrade dependency factory-boy to ==2.9.1
2017-08-02 09:17:25 -07:00
5aeadf8f98 [Doppins] Upgrade dependency psycopg2 to ==2.7.3 (#858)
* Upgrade dependency psycopg2 to ==2.7.2

* Upgrade dependency psycopg2 to ==2.7.3
2017-08-02 09:16:38 -07:00
5f9c655594 Upgrade dependency Flask-Migrate to ==2.1.0 (#861) 2017-08-02 09:16:21 -07:00
dd18cac702 Upgrade dependency boto3 to ==1.4.5 (#862) 2017-08-02 09:16:01 -07:00
b76ab902e5 Upgrade dependency pytest to ==3.2.0 (#865) 2017-08-02 09:15:42 -07:00
f5082e2d3a Starting transition away from not_before and not_after. (#854) 2017-07-14 09:24:59 -07:00
61c493fc91 Adding additional failure conditions to sentry tracking. (#853)
* Adding additional failure conditions to sentry tracking.

* Removing sentry extension as a circular import.
2017-07-13 14:49:04 -07:00
6779e19ac9 Adding enum migration. (#852) 2017-07-13 13:12:53 -07:00
443eb43d1f Adding the ability to specify a per-certificate rotation policy. (#851) 2017-07-12 16:46:11 -07:00
560bd5a872 Upgrade dependency acme to ==0.16.0 (#850) 2017-07-12 15:53:32 -07:00
8f35a64faf Upgrade dependency pyjwt to ==1.5.2 (#846) 2017-07-12 15:52:50 -07:00
7507f6be50 Updating documentation (#849) 2017-07-05 20:17:19 -07:00
ac3b441456 Upgrade dependency pytest to ==3.1.3 (#847) 2017-07-05 19:02:59 -07:00
53113e5eeb Add auditing for creating or updating a cert. (#845) 2017-07-04 06:39:16 -07:00
9d5db3ec12 This should not have been upgraded as it breaks mTLS (#844) 2017-06-29 16:29:26 -07:00
169dcb86e2 supporting the ability to push exceptions to sentry (#843) 2017-06-29 14:12:38 -07:00
e4f5224f42 set ses email content type to utf-8 instead of string (#841) 2017-06-28 09:44:19 -07:00
98907e66e9 Minor fixes to S3.put signature (#840) 2017-06-27 16:18:34 -07:00
c05343d58e Adds the ability for destination plugins to be sub-classed from Expor… (#839)
* Adds the ability for destination plugins to be sub-classed from ExportDestination. These plugins have the extra option of specifying an export plugin before the destination receives the data. Closes #807.

* fixing tests
2017-06-26 12:03:24 -07:00
541fbc9a6d Use named kwargs rather than args when calling s3 put (#830) 2017-06-20 11:28:19 -07:00
ef08e02333 [Doppins] Upgrade dependency paramiko to ==2.2.1 (#833)
* Upgrade dependency paramiko to ==2.1.3

* Upgrade dependency paramiko to ==2.2.0

* Upgrade dependency paramiko to ==2.2.1
2017-06-14 09:20:35 -07:00
35cc7ef8d7 Adding support for private DigiCert certificates (#835) 2017-06-14 09:20:24 -07:00
e77382864b Fixing KeyError on error handling (#834) 2017-06-14 09:07:27 -07:00
b5fd802005 Upgrade dependency acme to ==0.15.0 (#831) 2017-06-09 09:03:07 -07:00
98897f3c98 Upgrade dependency pytest to ==3.1.2 (#832) 2017-06-09 09:02:55 -07:00
d49bb8a6ca Upgrade dependency Flask-RESTful to ==0.3.6 (#828) 2017-06-03 20:25:11 -07:00
05f2d3b2d9 Upgrade dependency moto to ==1.0.1 (#829) 2017-06-03 20:24:51 -07:00
d4d6d832b1 Fixing audit filtering and sorting. (#827) 2017-06-02 09:07:22 -07:00
9c92138f2d Fixing autorotation failures. (#825)
* Fixing issue with auto rotation failing due to a change in the way certificate data is serialized.
2017-06-02 08:59:42 -07:00
5a4806bc43 Allowing description to be optional. (#826) 2017-06-01 17:09:04 -07:00
54105e221e Upgrade dependency Flask-Migrate to ==2.0.4 (#822) 2017-05-31 08:58:54 -07:00
adfc76aa79 Upgrade dependency pytest to ==3.1.1 (#823) 2017-05-31 08:58:38 -07:00
3e3f7af796 Upgrade dependency cryptography to ==1.9 (#821) 2017-05-30 09:03:46 -07:00
07969f7e10 Ensuring IPAddresses and IPNetworks are correctly serialized. (#818) 2017-05-26 10:48:26 -07:00
249ab23df4 Upgrade dependency acme to ==0.14.2 (#817) 2017-05-25 17:40:55 -07:00
3141b47fba Catch OAuth providers that want the params sent as data (#800) 2017-05-25 10:21:29 -07:00
31f4cf0253 adding url context path to html templates (#814) 2017-05-25 10:20:32 -07:00
21d48b32c9 Fixing an issue with uploading to cloudfront. (#815) 2017-05-25 10:10:12 -07:00
11bd42af82 Correct status code for basic-auth (#813)
* ensuring those using basic auth recieve a correct status code when their password is incorrect

* Fixing oauth status codes
2017-05-23 09:48:31 -07:00
feac9cb3a3 Upgrade dependency pytest to ==3.1.0 (#811) 2017-05-23 09:31:18 -07:00
f6b5012f56 Add Check of DB connections on healthcheck URL (#812) 2017-05-22 17:15:41 -07:00
f9b388c658 Modifying the was s3 uploading works. (#810)
* Modiying the was s3 uploading works.

* Fixing pep8
2017-05-20 12:07:44 -07:00
4093f4669a Switching remaining uses of boto to boto3. (#809) 2017-05-20 11:09:55 -07:00
9594f2cd8d Upgrading moto and fixing test that break due to deprecation. (#808)
* Upgrading moto and fixing test that break due to deprecation.

* Adding region.
2017-05-20 10:40:22 -07:00
380203eb53 Adding the ability to upload to cloudfront via the 'path' parameter. Cloudfront destinations must be created separately. (#805)
Closes #277
2017-05-18 13:49:17 -07:00
307a73c752 Fixing some confusion between 401 vs 403 error code. 401 indicates that the user should attempt to authenticate again. Where as 403 indicates the user is authenticated but not allowed to complete an action. (#804)
Closes #767
2017-05-18 13:20:17 -07:00
7ad471a810 Upgrade dependency acme to ==0.14.1 (#801) 2017-05-16 13:33:21 -07:00
1184f9d070 Upgrade dependency freezegun to ==0.3.9 (#803) 2017-05-16 13:32:20 -07:00
3050aca3e6 Minor fixes to the domains UI. (#798)
* Fixes checkbox input.

* Fixes notification message.
2017-05-15 19:14:12 -07:00
8c41c6785d Fixes issue where domains without any associated certificates are not searchable. (#797) 2017-05-15 19:07:32 -07:00
092ce0f9d8 Closes #792. (#796) 2017-05-15 19:07:16 -07:00
97dceb5623 fixed typo in supervisord example config (#790) 2017-05-12 09:18:32 -07:00
23b6df536f Fix Minor Typo in index.rst (#793)
Changed LEMUR_DEFAUTL_ORGANIZATION to LEMUR_DEFAULT_ORGANIZATION
2017-05-12 09:17:52 -07:00
95b4206986 Removing tests folder from coverage report. (#788) 2017-05-11 19:42:53 -07:00
914de78576 Adds migration to fix keys on unique index. Closes #743. (#785) 2017-05-10 12:13:42 -07:00
ecf00fe9d6 Splitting out the default date issuance logic for CIS and CC. CIS assumes years is converted to validity_end while CC prefers validity_years over validity_end. (#784) 2017-05-10 12:05:03 -07:00
7257e791ff Upgrade dependency acme to ==0.14.0 (#777) 2017-05-08 19:27:33 -07:00
c71b3a319d Log the audit logs (#781) 2017-05-08 09:43:26 -07:00
767147aef1 Check for unknown as status is no longer represented as a boolean (#780) 2017-05-08 09:43:19 -07:00
ce5a45037a Fix for status representation in the view (#778) 2017-05-05 11:04:40 -07:00
9c9ca37586 Enabling hex serial numbers without breaking backward compatibility. (#779)
* Enabling hex serial numbers without breaking backward compatibility.

* Fixing tests.
2017-05-05 11:04:09 -07:00
381cd2e1ff Updated apache config (#776)
You guys asked for one that worked... It took me a little while to tweak, esp. since I'm not a guru with python.  The comment about needing mod_wsgi isn't true, unless you want to run lemur as a cgi program... I suspect that's from an older version that ran as cgi and not as a standalone webserver.
2017-05-04 08:45:55 -07:00
2a2d5a5583 Adding an example digicert url. Closes #700. (#775) 2017-05-01 10:59:49 -07:00
5c41dafc97 fix unit and interval transposition in schemas.py (#752) (#774) 2017-04-30 12:23:34 -07:00
6367a98134 Creating a user named 'lemur' in postgres (#773)
Creating a user named 'lemur' in postgres
2017-04-28 15:31:08 -07:00
0bbe2b0331 config LEMUR_MAIL to LEMUR_EMAIL (#772)
I referenced https://github.com/Netflix/lemur/blob/master/lemur/plugins/lemur_email/plugin.py and it appears this configuration option should be "LEMUR_EMAIL"
2017-04-28 15:01:21 -07:00
6a77d511e8 Upgrade dependency xmltodict to ==0.11.0 (#769) 2017-04-28 15:00:41 -07:00
989e3733a2 Add docker setup for running tests on a docker enabled dev environment. (#771) 2017-04-28 09:28:06 -07:00
fbc24ea400 There is an issue when iterating over extensions where certificates might not have been issued in adherence with basic constraints. Here we log these errors instead of failing out right. (#770) 2017-04-27 17:45:34 -07:00
2b8c2f612e Upgrade dependency pyjwt to ==1.5.0 (#768) 2017-04-27 12:16:36 -07:00
4905020e77 ensuring stdout has a default log level (#766) 2017-04-27 10:11:47 -07:00
75787d20bc ensuring that lemur's default user has a valid email (#765) 2017-04-27 09:53:35 -07:00
ca9f120988 fixing some pep8 issues (#764) 2017-04-27 09:44:39 -07:00
5fb6753445 Upgrade dependency marshmallow to ==2.13.5 (#753) 2017-04-27 09:20:03 -07:00
e86954e8ea Destination Plugin/Lemur_linuxdst (#736)
* Added lemur_linuxdst

* Revert "Added lemur_linuxdst"

This reverts commit 010c19bd1937320189ee5a0660f9e356221121f3.

* added plugin\lemur_linuxdst

Destination plugin for a target linux host

* Update remote_host.py

* Update plugin.py

* Update remote_host.py

* Update plugin.py

* Update plugin.py

* chaning var and funct names

* Write data with local temp

* .

* .

* typo

* tested plugin successfully

* Update plugin.py

* Update remote_host.py

* removed whitespace

* set permissions on exported keys to 600

sftp.chmod(dst_dir_cn + '/' + dst_file, (stat.S_IRUSR))

* Update plugin.py

* Update remote_host.py

* Update plugin.py

* added 'paramiko==2.1.2'

required for lemur_linuxdst plugin

* data stored in clear text at rest

* Update plugin.py

* Update plugin.py

* Update remote_host.py
2017-04-27 09:19:49 -07:00
604cd60dbe Return correct intermediate certificate on digicert creation. (#762)
This commit also removes the unused DIGICERT_INTERMEDIATE env
var as it is not used.
2017-04-27 09:14:20 -07:00
05f4ae8e58 Hexify cert serial (#763)
* Hexify serial at the serialization layer

* Fix for flakey test. Change test to test for uppercased string
2017-04-27 09:13:04 -07:00
88ac783fd2 PEP8 Fixes (#760) 2017-04-25 09:23:18 -07:00
bc66ede9aa Fixing Bandit findings and adding travis Bandit job (#759)
* Fixes for Bandit

This commit fixes a couple of issues so that Bandit can run
cleanly using medium+ severity and confidence filtering.

* Adding Lemur Bandit job to TravisCI
2017-04-24 18:37:03 -07:00
1c295896e6 Add test for when there are no notifications on a certificate (#757) 2017-04-24 09:04:49 -07:00
f90076abe9 Update index.rst (#754)
Seems the api for these actions have changed. Thought I would update the documentation around this. Let me know if I've misunderstood something.
2017-04-19 16:06:32 -07:00
01aa372e59 Version bump. (#751) 2017-04-08 13:23:48 -07:00
479ac81aa9 0.5 Release (#750) 2017-04-08 13:17:24 -07:00
9c69c6d129 [Doppins] Upgrade dependency marshmallow-sqlalchemy to ==0.13.1 (#719)
* Upgrade dependency marshmallow-sqlalchemy to ==0.13.0

* Upgrade dependency marshmallow-sqlalchemy to ==0.13.1
2017-04-08 12:43:51 -07:00
ea1e9cb4c6 Upgrade dependency psycopg2 to ==2.7.1 (#721) 2017-04-08 12:34:17 -07:00
dac7a77afb Upgrade dependency gunicorn to ==19.7.1 (#733) 2017-04-08 12:33:57 -07:00
9b21197fec Upgrade dependency SQLAlchemy-Utils to ==0.32.14 (#745) 2017-04-08 12:33:46 -07:00
e4255649c0 Upgrade dependency acme to ==0.13.0 (#746) 2017-04-08 12:33:28 -07:00
81aff42e03 Removing this exception handling, that error should be caught above. (#749) 2017-04-07 16:01:40 -07:00
221851abc1 supervisor ; cause services not to start (#744)
the ; in the supervisor/conf.d/app.conf file cause the service not to start.
2017-04-06 09:21:13 -07:00
7f019583f2 Don’t set ‘custom_expiration_date’ if validity years is set in the UI. (#742)
* Don’t set ‘custom_expiration_date’ if validity years is set in the UI.

* Use single quotes instead of double quotes.
2017-04-04 17:11:17 -07:00
e18a188723 Spell fixes in docs (#740) 2017-03-30 21:09:30 -07:00
f91ae5b319 Fixes bug where authority status was not set correctly. (#739) 2017-03-29 10:10:51 -07:00
dd39b9ebe8 adding url context path to build, adding documentation on url contextpath (#737) 2017-03-28 15:21:13 -07:00
15896a3b11 Fix spelling error in LEMUR_DEFAULT_COUNTRY (#734) 2017-03-22 15:49:16 -07:00
e092606181 Upgrade dependency marshmallow to ==2.13.4 (#732) 2017-03-20 09:08:26 -07:00
a4707c5fc9 added a few steps (#731)
Added a few steps that are needed during the install on a fresh Ubuntu image
2017-03-18 21:36:26 -07:00
f0dde845db Adding ability to exclude certificates from expiration (#730)
* adding ability to exclude certificates from expiration

* fixing tests
2017-03-15 11:25:19 -07:00
b0ea027769 Underscores should not be in hostnames (#728) 2017-03-15 08:41:06 -07:00
d9f2faa462 Upgrade dependency pytest to ==3.0.7 (#727) 2017-03-14 15:06:54 -07:00
7b4d31d4f6 added steps for loading custom plugin (#725)
* added steps for loading custom plugin

added steps for loading a custom plugin into Lemur once the files have been put into place (/www/lemur/lemur/plugins/) and the setup.py file (/www/lemur/setup.py) has been modified.

* updated __init__.py section


except Exception as e:
2017-03-14 09:30:22 -07:00
522e182694 added python3-dev to dependencies (#724)
make release fails without it
2017-03-13 15:45:10 -07:00
6c8a6620d2 specify python3 when creating virtualenv (#723)
Lemur is developed against Python3.5. If you do not specify the Python version it is possible the virtualenv will be built on a different version.
2017-03-13 13:58:44 -07:00
d68b2b22e0 Update bower.json (#722)
Angular angular-sanitize is pulling in an incompatible version of angular knocking out the webUI by breaking chart.js.
2017-03-13 12:28:08 -07:00
a4068001a3 Updating docs to align with normal deployment. (#718) 2017-03-12 15:01:21 -07:00
574fed2618 Upgrade dependency marshmallow to ==2.13.3 (#717) 2017-03-11 11:07:17 -08:00
8762e1c5ae Issue #703 bugfix (#711)
* Ensures that both AKI serial/issue _and_ keyid won't be included.
Validation issues crop up if both types of AKI fields are present.

* Ensure that SAN extension includes the certificate's common name

* Fix scenario where subAltNames are getting dropped when applying a template

* Ensure that SAN includes the CN

* Ensuring that getting here without a SAN extension won't break things.

* New cleaner approach

* Some bits of handling the extensions are a bit hacky, requiring access to attributes inside the objects in x509.
I think this is pretty clean though.

* lintian check

* Fixing tests
2017-03-10 09:09:18 -08:00
d94e3113ff Upgrade dependency marshmallow to ==2.13.2 (#716) 2017-03-10 09:08:34 -08:00
3c5b2618c0 Rely on the lemur generating the correct name for rotated certificates. (#714)
* Rely on the lemur generating the correct name for rotated certificates.

* Fixing tests.
2017-03-09 13:09:20 -08:00
602c5580d3 Only validates values if present in options. Fixing authority test to parse plugin information. (#713) 2017-03-06 20:38:04 -08:00
038beafb5e Upgrade dependency gunicorn to ==19.7.0 (#709) 2017-03-04 18:28:35 -08:00
14923f8c07 Upgrade dependency marshmallow to ==2.13.1 (#710) 2017-03-04 18:28:24 -08:00
b715687617 Ensuring that we don't fail cleaning if it doesn't exist. (#708) 2017-03-03 16:03:52 -08:00
c46fa5d69c Ensures the rotation has a value during migration. (#707) 2017-03-03 15:16:25 -08:00
310e1d4501 Adds support for filtering by UI. Closes #702. (#706) 2017-03-03 15:07:26 -08:00
fc957b63ff Source syncing tweaks. (#705)
* Allow owner to be specified when syncing certs.

* Ensuring non-endpoint plugins don't fail to complete syncing.

* Adding in some additional error handling.
2017-03-03 14:53:56 -08:00
d53f64890c Adding max notification constraint. (#704)
* Adds additional constraints to the max notification time. With an increasing number of certificates we need to limit the max notification time to reduce the number of certificates that need to be analyzed for notification eligibility.
2017-03-03 12:59:16 -08:00
5f5583e2cb UI adjustments for mutually exclusive (radio button version) encipher/decipher-only Key Usage #664 (#692)
* UI adjustments to make Key Agreement, Encipher Only, and Decipher Only relationship more user-friendly

* whitespace typo

* Issue #663 switching Encipher/Decipher Only options to be mutually exclusive and un-checkable radio buttons.

* Found a bug in the fields schema that was dropping Key Agreement bit if encipher/decipher only weren't checked
2017-02-16 13:26:56 -08:00
4c11ac9a42 [Doppins] Upgrade dependency acme to ==0.11.1 (#647)
* Upgrade dependency acme to ==0.10.0

* Upgrade dependency acme to ==0.10.1

* Upgrade dependency acme to ==0.10.2

* Upgrade dependency acme to ==0.11.0

* Upgrade dependency acme to ==0.11.1
2017-02-16 13:24:28 -08:00
cf6ad94509 Adjusting the way that certificates are requested. (#643)
* Adjusting the way that certificates are requested.

* Fixing tests.
2017-02-16 13:24:05 -08:00
08bb9c73a0 allow attributes to be excluded from a cert subject (#690)
* allow more flexibility in cert subject name

* clean up logic/remove unnecessary code
2017-02-16 13:21:52 -08:00
8e49194764 Issue 688 cert templates (#689)
* subAltNames were getting wiped out every time a template was selected

* isCritical variables aren't presented in the UI, nor is this information used in determining to use them.
2017-02-10 12:43:41 -08:00
8afcb50a39 Fixing the re-issuance process. Ensuring that certificates that are r… (#686)
* Fixing the re-issuance process. Ensuring that certificates that are re-issued go through the normal schema validation.

* Fixing tests.
2017-02-03 11:21:53 -08:00
0326e1031f adding generic OAuth2 provider (#685)
* adding support for Okta Oauth2

* renaming to OAuth2

* adding documentation of options

* fixing flake8 problems
2017-02-03 10:36:49 -08:00
117009c0a2 Lemur cryptography refactor and updates (#668)
* Renaming the function so it sounds less root-specific

* Refactoring lemur_cryptography
* Adding to the certificate interface an easy way to request the subject and public_key of a certificate
* Turning the create authority functionality into a wrapper of creating a CSR in the certificate codebase and issueing that certificate in this plugin. (Dependent on https://github.com/Netflix/lemur/pull/666 changes first)
* Ensuring that intermediate certificates and signed certificates retain their chain cert data

* Handling extensions that are the responsibility of the CA
Implementing authority_key_identifier for lemur_cryptography signatures and including skeletons of handling the certificate_info_access and crl_distribution_points

* Fixing errors found with linter

* Updating plugin unit tests

* Changing this for Python3. Underlying cryptography library expects these to be bytes now.

* Updating tests to match new function names/interfaces

* Another naming update in the plugin tests

* Appears that create_csr won't like this input without an owner.

* Undoing last commit and putting it into the right place this time.

* create_csr should be good now with these options, and chain certs will be blank in tests

* This won't be blank in issue_certificate, like it will in creating an authority.

* Much cleaner

* unnecessary import
2017-02-01 10:34:24 -08:00
b7833d8e09 Upgrade dependency Flask-Migrate to ==2.0.3 (#682) 2017-01-31 09:15:52 -08:00
3fd39fb823 Upgrade dependency marshmallow to ==2.12.2 (#683) 2017-01-31 09:15:40 -08:00
317b7cabb3 Ensuring usage matched OIDs. (#681) 2017-01-28 23:22:20 -08:00
a59bc1f436 Fixes (#680)
* Adding some additional logging.
2017-01-28 16:40:37 -08:00
c24810b876 Modifying variable to fit epextions. (#679) 2017-01-28 14:07:12 -08:00
bc94353850 Closes #648, also fixes several issues #666. (#678) 2017-01-27 21:05:25 -08:00
f13a3505f3 X509 extensions issue#646 (#666)
* Allowing that create_csr can be called with an additional flag in the csr_config to adjust the BasicConstraints for a CA.

* If there are no SANs, skip adding a blank list of SANs.

* Adding handling for all the extended key usage, key usage, and subject key identifier extensions.

* Fixing lint checks. I was overly verbose.

* This implements marshalling of the certificate extensions into x509 ExtensionType objects in the schema validation code.

* Will create x509 ExtensionType objects in the schema validation stage
* Allows errors parsing incoming options to bubble up to the requestor as ValidationErrors.
* Cleans up create_csr a lot in the certificates/service.py
* Makes BasicConstraints _just another extension_, rather than a hard-coded one
* Adds BasicConstraints option for path_length to the UI for creating an authority
* Removes SAN types which cannot be handled from the UI for authorities and certificates.
* Fixes Certificate() object model so that it doesn't just hard-code only SAN records in the extensions property and actually returns the extensions how you expect to see them. Since Lemur is focused on using these data in the "CSR" phase of things, extensions that don't get populated until signing will be in dict() form.* Trying out schema validation of extensions
2017-01-27 12:31:29 -08:00
4af871f408 Added migration to cover what seem to be missing fields. (#676) 2017-01-27 09:07:20 -08:00
162d5ccb62 Gracefully handle importing certificates with missing data (#674)
* fixing index out of range issue

* catching exceptions is common values aren't set

* fixing lint errors

* fixing unrelated lint/import error
2017-01-24 13:48:53 -08:00
b1723b4985 [Doppins] Upgrade dependency marshmallow to ==2.12.1 (#672)
* Upgrade dependency marshmallow to ==2.12.0

* Upgrade dependency marshmallow to ==2.12.1
2017-01-24 13:46:37 -08:00
6bf7d56d51 Upgrade dependency moto to ==0.4.31 (#673) 2017-01-24 13:46:14 -08:00
9751cbbf83 Upgrade dependency pytest to ==3.0.6 (#671) 2017-01-22 18:03:22 -08:00
8fa5ffa007 Upgrade dependency boto3 to ==1.4.4 (#670) 2017-01-20 13:10:01 -08:00
f353956353 Many fixes to authority/certificate extensions pages (#659)
* Aligning certificate creation between authority and certificate workflows
* Correctly missing and mis-named fields in schemas
* Re-ordering KeyUsage and ExtendedKeyUsage for consistency and clarity
* Adding client authentication to the authority options.

* Missing blank lines for pyflakes linting

* Updating tests for new fields/names/typos
2017-01-18 14:31:17 -08:00
02cfb2d877 Stealing this code form the attachSubAltName function in the certificates workflow. (#655)
The function was wiping out any extensions that weren't SAN names from the authority UI.
2017-01-18 14:24:15 -08:00
1b6f88f6fd Fixing handling of adding custom OIDs in UI (#653)
* is_critical wasn't in the schema, so was getting dropped.
* isCritical in the Javascript wasn't getting assigned if it was unchecked. Now, it will be assumed false if missing.
* The display of critical or not in the list of added custom OIDs was unclear when it was just true/false with no heading. Now it will be displayed as critical or nothing instead.
* The namespace for the checkbox for isCritical was wrong, and didn't get processed with the oid/type/value variables.
2017-01-18 14:20:44 -08:00
9f6ad08c50 Updating hooks. (#660) 2017-01-18 14:16:31 -08:00
25340fd744 Combining Authority Key Identifier extension options in the schema. (#651)
* Combining Authority Key Identifier extension options in the schema.
This makes processing them in the cert/csr generation stage make more sense because they are two options in the same x.509 extension. They were already in the same part of the schema for authorities, but this makes the certificates follow the same pattern, and it allows them to share the same schema/validation layout.

* Updating schema tests to match changes

* Fixing an idiot typo

* I promise to stop using Travis as a typo-corrector soon.
2017-01-18 14:16:19 -08:00
7f2b44db04 Correcting grammar for subca ValidationError message for clarity (#657) 2017-01-18 12:34:16 -08:00
d67b6c6120 Chains are not always a given. (#645) 2017-01-08 17:27:50 -08:00
4cfb5752b2 Upgrade dependency marshmallow to ==2.11.1 (#644) 2017-01-08 14:52:28 -08:00
0d7b2d9f44 Upgrade dependency Flask to ==0.12 (#639) 2017-01-08 10:53:02 -08:00
08ebc4cd59 Upgrade dependency marshmallow-sqlalchemy to ==0.12.1 (#640) 2017-01-08 10:50:37 -08:00
85ae9712e3 Upgrade dependency marshmallow to ==2.11.0 (#642) 2017-01-08 10:49:41 -08:00
83128f3019 Fixing elb sync issues. (#641)
* Fixing elb sync issues.

* Fixing de-duplications of names.
2017-01-05 16:06:34 -08:00
7aa5ba9c6b Fixing an IAM syncing issue. Were duplicates were not properly sync'd… (#638)
* Fixing an IAM syncing issue. Were duplicates were not properly sync'd with Lemur. This resulted in a visibility gap. Even 'duplicates' need to sync'd to Lemur such that we can track rotation correctly. Failing on duplicates lead to missing those certificates and the endpoints onto which they were deployed. This commit removes the duplicate handling altogether.

* Fixing tests.
2017-01-04 17:46:47 -08:00
e5dee2d7e6 Adding additional metrics for when destinations fail to upload. (#637) 2016-12-28 09:52:23 -08:00
b0232b804e Removing cloned date defaults. (#636) 2016-12-27 11:35:53 -08:00
de7cec35c6 Clean refactor (#635)
* Adding rotation to the UI.

* Removing spinkit dependency.

* refactoring source cleaning
2016-12-27 10:31:33 -08:00
700c57b807 Rotation ui (#633)
* Adding rotation to the UI.

* Removing spinkit dependency.
2016-12-26 15:55:11 -08:00
ce75bba2c3 Replacement refactor. (#631)
* Deprecating replacement keyword.

* Def renaming.
2016-12-26 11:09:50 -08:00
46f8ebd136 Modifying the way rotation works. (#629)
* Modifying the way rotation works.

* Adding docs.

* Fixing tests.
2016-12-23 13:18:42 -08:00
f8279d6972 Fixes a bug where pagination was incorrect. (#628) 2016-12-21 18:39:21 -08:00
072ca4da4f Adding some additional output to rotation command. (#627) 2016-12-21 13:34:14 -08:00
8c5c30dfd4 Adding some additional output to expiration command. (#626) 2016-12-21 11:01:21 -08:00
edc0116a3a urllib3 still failing. (#625) 2016-12-21 11:01:09 -08:00
c1b2c3689c [Doppins] Upgrade dependency requests to ==2.12.4 (#543)
* Upgrade dependency requests to ==2.12.2

* Upgrade dependency requests to ==2.12.3

* Upgrade dependency requests to ==2.12.4
2016-12-21 10:06:30 -08:00
6746cc33a0 Upgrade dependency factory-boy to ==2.8.1 (#616) 2016-12-21 10:01:46 -08:00
74723d1a1f Adding ability to modify ELBv2 endpoints. (#624) 2016-12-21 08:23:14 -08:00
fccb8148d5 Upgrade dependency marshmallow to ==2.10.5 (#615) 2016-12-21 07:19:32 -08:00
3a4ebbf92c Upgrade dependency SQLAlchemy-Utils to ==0.32.12 (#614) 2016-12-21 07:19:10 -08:00
48735e685c Upgrade dependency boto3 to ==1.4.3 (#623) 2016-12-20 18:28:07 -08:00
cdcae4efb0 Closes #594 (#621) 2016-12-20 14:26:39 -08:00
f7c795c7f6 Closes #577. (#622) 2016-12-20 14:26:29 -08:00
beba2ba092 Adding additional reporting and refactoring existing setup. (#620) 2016-12-20 12:48:14 -08:00
9ac10a97ce Fix acme tests (#619)
* Ensures that in-active users are not allowed to login.

* Ensuring acme issuer loads correctly.
2016-12-19 22:59:23 -08:00
2f5f82d797 Ensures that in-active users are not allowed to login. (#618) 2016-12-19 22:58:57 -08:00
c7fdb2acd7 adding required variables (#611) 2016-12-18 18:21:22 -08:00
51c7216b70 Fixing configuration value. (#610)
* Fixing and configuration value.

* Pinning fake factory.
2016-12-18 18:21:12 -08:00
0f3ffaade0 Fall back to CN for CA name when organization is not available (#607)
In-house CAs may not have the organization field filled out.
2016-12-16 16:27:25 -08:00
156b98f7f0 Ensuring that rotation only happens for certificates with endpoints to rotate. (#606) 2016-12-15 15:20:21 -08:00
a09faac9a7 Endpoint sync fixes (#604) 2016-12-15 10:26:59 -08:00
d20c552248 Fixing issues with rotation. (#603)
* Fixing issues with rotation.

* Fixing tests
2016-12-14 17:30:13 -08:00
f7fdf7902d Upgrade dependency boto to ==2.45.0 (#601) 2016-12-14 16:53:47 -08:00
b327963925 Plugin base classes: update method signatures & fix raise (#598)
This way IDEs can verify method overrides in subclasses, otherwise these
are flagged as erroneous.

Changed base classes to properly raise NotImplementedError; previously
they would cause "TypeError: exceptions must derive from BaseException"

Also fixed exception handling in sources.service.clean().
2016-12-14 13:42:29 -08:00
1eb3d563c6 Fix error reporting for certs without private key (#599) 2016-12-14 13:25:56 -08:00
02991c70a9 Allow Lemur "start" to use the global config. (#596)
* allowing our runserver to use the config specified by -c

* Maintaining config for gunicorn
2016-12-14 13:23:50 -08:00
71ddbb409c Minor documentation fixes/tweaks (#597)
Mostly typos, grammar errors and inconsistent indentation in code
examples.

Some errors detected using Topy (https://github.com/intgr/topy), all
changes verified by hand.
2016-12-14 09:29:04 -08:00
fbcedc2fa0 Specifying a recommended postgres version (#592) 2016-12-13 11:22:10 -08:00
3dad818af2 ensuring our index gets created (#591) 2016-12-13 11:13:44 -08:00
5dc0fa91e8 Upgrade dependency boto3 to ==1.4.2 (#550) 2016-12-13 09:53:49 -08:00
565c9ae98d adding missing init (#587) 2016-12-13 09:21:31 -08:00
2d6aa620b4 Attempting to upgrade to node LTS (#585)
* Attempting to upgrade to node LTS

* Updating travis config to node
2016-12-13 08:50:12 -08:00
03d5a6cfe1 Refactors how notifications are generated. (#584) 2016-12-12 11:22:49 -08:00
a5c47e4fdc Upgrade dependency Flask-Migrate to ==2.0.2 (#582) 2016-12-12 10:42:57 -08:00
9581278481 Upgrade dependency cryptography to ==1.7 (#583) 2016-12-12 10:42:45 -08:00
1c3ac21291 Ensuring the digicert session is handled correctly (#579) 2016-12-11 08:38:59 -08:00
25faf05807 Upgrade dependency boto to ==2.44.0 (#578) 2016-12-08 17:31:53 -08:00
968dd52f6f Fixes (#576)
* Fixing email notification

* Adding endpoint expiration

* Fixing endpoint type for ELBs

* Allowing verisign to include additional SANs
2016-12-08 15:52:27 -08:00
a4b32b0d31 Fixing up notification testing (#575) 2016-12-08 11:33:40 -08:00
be1415fbd4 Ensuring new cli is available (#574) 2016-12-08 09:11:19 -08:00
b5901a1570 adding needed migration files (#573) 2016-12-07 17:31:59 -08:00
bdc6dc8683 Fixing a bug were extensions got a default value (#572) 2016-12-07 17:28:18 -08:00
5087fa67dc skipping a few tests that aren't ready yet (#571) 2016-12-07 16:52:00 -08:00
fc205713c8 Certificate rotation enhancements (#570) 2016-12-07 16:24:59 -08:00
9adc5ad59e Adding last updated time (#569) 2016-12-07 15:43:57 -08:00
f63ccd033d Ensuring that endpoints without output_schema work as expected (#568) 2016-12-07 15:40:29 -08:00
d7c0e2ec35 Ensuring that certificates returned from digicert are in the proper format (#564) 2016-12-06 12:25:52 -08:00
00da52f32e Ensuring that CSRs are correctly validated under python3 (#565) 2016-12-06 12:25:43 -08:00
287c684866 Ensuring that certificates returned from digicert are in the proper format (#564) 2016-12-06 12:10:39 -08:00
e94cf6ddc9 Ensuring that certificates returned from digicert are in the proper format (#564) 2016-12-06 12:05:18 -08:00
81272a2f7a Moving validation to server start. (#563) 2016-12-05 16:43:38 -08:00
e622a49b72 Adding better error handling around certificate rotation (#562) 2016-12-05 15:12:55 -08:00
9030aed8a4 Ensuring that our syncing process can find duplicate certifcates that do no need to be sync'd (#560) 2016-12-05 11:08:29 -08:00
eee534a161 Upgrade dependency pytest to ==3.0.5 (#559) 2016-12-05 10:54:54 -08:00
344abbda66 fixing signature (#556) 2016-12-02 13:48:50 -08:00
834814f867 adding additional status code metrics (#555) 2016-12-02 13:02:59 -08:00
7f823a04cd Ensuring that acme and cryptography respect different key types (#554) 2016-12-02 10:54:18 -08:00
0f5e925a1a Ensuring that default-issuer is set (#553) 2016-12-02 09:54:16 -08:00
e0c79389ca Allowing tar to be installed without git or other development tools (#552) 2016-12-01 16:20:46 -08:00
a40bc65fd4 Default authority. (#549)
* Enabling the specification of a default authority, if no default is found then the first available authority is selected

* PEP8

* Skipping tests relying on keytool
2016-12-01 15:42:03 -08:00
81bf98c746 Enabling RSA2048 and RSA4096 as available key types (#551)
* Enabling RSA2048 and RSA4096 as available key types

* Fixing re-issuance
2016-12-01 15:41:53 -08:00
41b59c5445 adding required variables to digicert issuer (#546) 2016-12-01 10:50:25 -08:00
e1bbf9d80c Improving endpoint rotation logic (#545) 2016-11-30 15:11:17 -08:00
bd2abdf45f Upgrade dependency arrow to ==0.10.0 (#541) 2016-11-30 15:07:36 -08:00
abb91fbb65 fixing a few minor issue with cloning (#544) 2016-11-30 10:54:53 -08:00
f9b16a2110 csr as string (#542) 2016-11-29 18:50:20 -08:00
588ac1d6a6 Digicert cis fixes (#540) 2016-11-29 17:15:39 -08:00
058d2938fb migrating off of openssl (#539) 2016-11-29 11:30:44 -08:00
3db3214cbe installing the digicert CIS plugin (#537) 2016-11-29 10:02:40 -08:00
bfc80f982c minor fixes and downgrading requests (#535) 2016-11-28 16:50:26 -08:00
727bc87ede Log fixes (#534)
* tying up some loose ends with event logging

* Ensuring creators can access
2016-11-28 14:13:16 -08:00
e2143d3ee8 tweaking the way data is returned (#532) 2016-11-28 12:29:03 -08:00
b46ff4158a Initial workon the digicert high issuance api. (#531) 2016-11-28 10:50:58 -08:00
734233257c Upgrade dependency arrow to ==0.9.0 (#529) 2016-11-27 15:27:12 -08:00
250558baf3 Ensuring that authority owners can access certificates issued by that… (#526)
* Ensuring that authority owners can access certificates issued by that authority
2016-11-25 20:35:07 -08:00
8e5323e2d7 migrating flask imports (#525) 2016-11-22 21:11:20 -08:00
06a920502c Updating readme with supported python verisions (#524) 2016-11-22 17:09:21 -08:00
d5d036b412 adding a work around for new gunicorn (#523) 2016-11-22 16:47:29 -08:00
9d03e75d9b tweaking a few things to support the new marshmallow (#522) 2016-11-22 15:14:19 -08:00
0158807847 Upgrade dependency cryptography to ==1.6 (#521) 2016-11-21 21:38:42 -08:00
06a3f3ea0d version bump (#520) 2016-11-21 15:29:31 -08:00
12ae0a587d teaking the way exceptions are handled (#519) 2016-11-21 15:26:17 -08:00
b3aa057d58 Upgrade deps. (#517) 2016-11-21 14:29:20 -08:00
dd6d332166 Removing python2 compatibility. (#518) 2016-11-21 14:03:04 -08:00
6eca2eb147 Re-working the way audit logs work.
* Adding more checks.
2016-11-21 11:28:11 -08:00
744e204817 Initial work on #74. (#514)
* Initial work on #74.

* Fixing tests.

* Adding migration script.

* Excluding migrations from coverage report.
2016-11-21 09:19:14 -08:00
d45e7d6b85 [WIP] - 422 elb rotate (#493)
* Initial work on certificate rotation.

* Adding ability to get additional certificate info.

* - Adding endpoint rotation.
- Removes the g requirement from all services to enable easier testing.
2016-11-18 11:27:46 -08:00
6fd47edbe3 Adds the ability to clone existing certificates. (#513) 2016-11-17 16:19:52 -08:00
a616310eb7 Fixing an issue were aws certificates plugins might not have a chain. (#512) 2016-11-17 14:47:10 -08:00
2130029f90 Adding new notification templates. (#511) 2016-11-17 14:16:59 -08:00
d11f254476 Closes: #469 (#510) 2016-11-17 12:16:30 -08:00
d54a11ad11 Ensuring coverage is run. (#509) 2016-11-17 11:11:09 -08:00
a9361fe428 Endpoints should be visible to all. (#508) 2016-11-17 10:45:26 -08:00
5345170a4f Ensuring that the passed in configuration has precedence over the environment config. (#507) 2016-11-17 09:31:37 -08:00
d0ccd85afe Adding coverage. (#506)
* Adding coverage.

* Attempting to adding coverage.

* Adding coveragerc.
2016-11-16 16:44:51 -08:00
520404c215 fix string -> byte conversion on python2 (#472) 2016-11-16 16:03:38 -08:00
9ac1756011 removing new 'active' logic for the time being (#505) 2016-11-16 15:56:24 -08:00
851d74da3d Ensuring that private key is in string format before it gets stored (#504)
* Ensuring that private key is in string format before it gets stored

* Fixing failing test.
2016-11-16 15:05:25 -08:00
3f2691c5d4 Minor fixes. (#502) 2016-11-16 13:23:35 -08:00
eaf34b1c8b Disabling the protect active flag (#498) 2016-11-16 09:31:02 -08:00
e9219adfb5 Ensuring model's have a basic __repr__. (#499) 2016-11-16 09:30:54 -08:00
9eddaf66cb adding human readable string (#500) 2016-11-16 09:30:46 -08:00
0a29a3fa2a Adding release notes. (#459) 2016-11-15 16:44:40 -08:00
9bb0787410 Ensuring that duplicates are migrated correctly. (#496)
* Ensuring that duplicates are migrated correctly.

* fixing typo
2016-11-15 16:43:45 -08:00
dd14fd202d clean out ADMINS references (#495)
* add variables to the documentation forwq oauth2

* remove old reference to ADMINS to get rid of any confusion
2016-11-15 16:43:28 -08:00
114deba06e Adding the ability to silence notifications on creation. (#490) 2016-11-12 09:29:42 -08:00
0334f1094d fixing documentation typo (#489) 2016-11-11 13:35:24 -08:00
7af68c3cc0 Adding additional metric gathering for failed sync operations. (#488) 2016-11-11 13:28:01 -08:00
953d3a08e7 Adding example request to documentation. (#487) 2016-11-11 12:54:12 -08:00
f141ae78f3 Typo. (#485) 2016-11-10 14:40:59 -08:00
94d619cfa6 Minor errors. (#484) 2016-11-10 14:34:45 -08:00
89470a0ce0 Adding default validity and retry logic. (#483) 2016-11-10 11:23:37 -08:00
e6b291d034 Time (#482)
* adding python 3.5 as a target

* adding env flag

* Aligning on arrow dates.
2016-11-09 10:56:22 -08:00
b0eef03c73 adding python 3.5 as a target (#481)
* adding python 3.5 as a target

* adding env flag
2016-11-08 15:22:50 -08:00
25a6c722b6 Adding digicert documentation. (#480) 2016-11-08 14:56:05 -08:00
67a5993926 fixing type in ciphers (#479) 2016-11-08 12:23:21 -08:00
aa979e31fd Digicert plugin (#478)
* Initial work on digicert plugin.

* Adding certificate pickup, to digicert plugin.

* Removing and rotating test api key.
2016-11-07 14:40:00 -08:00
b74df2b3e4 Minor changes for python3. (#477) 2016-11-07 14:33:07 -08:00
4afedaf537 Fixes (#476)
* Ensures that Vault can accept bytes and strings.

* Make restricted domains optional.

* Fixing notify flag.
2016-11-04 09:16:41 -07:00
2b79474060 Trying this to fix defaulting org to Netflix (#475) 2016-11-02 09:12:47 -07:00
a6360ebfe5 Adding pending certificate metric. (#473) 2016-11-01 14:24:45 -07:00
d99681904e Fixing test to take python3 into account. (#460)
* Fixing test to take python3 into account.
2016-10-31 17:02:08 -07:00
1ac1a44e83 San alt name (#468) 2016-10-31 11:00:15 -07:00
f990f92977 Fixing typo in documentation for LEMUR_DEFAULT_ORGANIZATIONAL_UNIT spelling (#467) 2016-10-27 20:26:28 -07:00
490d5b6e6c python2.x .base64url_decode has a single parameter and incoming data is utf-8.. need to convert so string (#463) 2016-10-26 00:50:00 -07:00
4b7fc8551c fix(web): send JSON for all errors (#464)
Configure werkzeug to output JSON error messages for the benefit of
downstream clients. This also allows for metrics collection in all cases
where werkzeug is outputting an exception.
2016-10-26 00:46:43 -07:00
cd9c112218 Implement a CFSSL issuer plugin (#452)
* Implement CFSSL issuer plugin

Implement a Lemur plugin for generating certificates from the open
source certificate authority CFSSL
(https://github.com/cloudflare/cfssl). The plugin interacts with CFSSL
through the CFSSL REST API. The CFSSL configuration is defined in the
lemur.conf.py property file using property names prefixed with "CFSSL_".

* Update documentation to include CFSSL plugin
2016-10-22 00:52:18 -07:00
a8f44944b1 Closes #415 2016-10-17 23:23:14 -07:00
d31c9b19ce Closes #412. Allows 'name' be a valid attribute to specify a role. (#457) 2016-10-16 03:56:13 -07:00
fb178866f4 Fixes an issue with the source tests failing. (#456) 2016-10-16 03:55:37 -07:00
f921b67fff Removing the ability to use spaces in custom names. (#455) 2016-10-15 04:56:25 -07:00
c367e4f73f Prevents the silencing of notifications that are actively deployed. (#454)
* Renaming 'active' to 'notify' as this is clearer and more aligned to what this value is actually controlling. 'active' is now a property that depends on whether any endpoints were found to be using the certificate. Also added logic for issue #405 disallowing for a certificates' notifications to be silenced when it is actively deployed on an endpoint.

* Adding migration script to alter 'active' column.
2016-10-15 00:12:11 -07:00
dcb18a57c4 Adds option to restrict certificate expiration dates to weekdays. (#453)
* Adding ability to restrict certificate creation to weekdays.

* Ensuring that we test for weekends.
2016-10-15 00:04:35 -07:00
1b861baf0a Updating gulp-protractor dependency (#451)
Following the quickstart instructions, I ran into issues at `make develop` because
several dependencies couldn't be resolved. Several errors like this in the output
of `npm install`:

```
npm ERR! TypeError: Cannot read property 'latest' of undefined
npm ERR!     at next (/usr/share/npm/lib/cache.js:687:35)
npm ERR!     at /usr/share/npm/lib/cache.js:675:5
npm ERR!     at saved (/usr/share/npm/node_modules/npm-registry-client/lib/get.js:142:7)
npm ERR!     at /usr/lib/nodejs/graceful-fs/polyfills.js:133:7
npm ERR!     at Object.oncomplete (fs.js:107:15)
npm ERR! If you need help, you may report this log at:
npm ERR!     <http://github.com/isaacs/npm/issues>
npm ERR! or email it to:
npm ERR!     <npm-@googlegroups.com>

npm ERR! System Linux 3.13.0-92-generic
npm ERR! command "/usr/bin/nodejs" "/usr/bin/npm" "install"
npm ERR! cwd /home/lemur/lemur
npm ERR! node -v v0.10.25
npm ERR! npm -v 1.3.10
npm ERR! type non_object_property_load
```

`npm list` yielded this output at the bottom:

```
npm ERR! missing: @types/jasmine@^2.2.31, required by protractor@4.0.9
npm ERR! missing: @types/node@^6.0.35, required by protractor@4.0.9
npm ERR! missing: @types/q@^0.0.30, required by protractor@4.0.9
npm ERR! missing: @types/selenium-webdriver@~2.53.30, required by protractor@4.0.9
npm ERR! missing: adm-zip@0.4.7, required by protractor@4.0.9
npm ERR! missing: chalk@^1.1.3, required by protractor@4.0.9
npm ERR! missing: glob@^7.0.3, required by protractor@4.0.9
npm ERR! missing: jasmine@2.5.2, required by protractor@4.0.9
npm ERR! missing: jasminewd2@0.0.10, required by protractor@4.0.9
npm ERR! missing: optimist@~0.6.0, required by protractor@4.0.9
npm ERR! missing: q@1.4.1, required by protractor@4.0.9
npm ERR! missing: saucelabs@~1.3.0, required by protractor@4.0.9
npm ERR! missing: selenium-webdriver@2.53.3, required by protractor@4.0.9
npm ERR! missing: source-map-support@~0.4.0, required by protractor@4.0.9
npm ERR! missing: webdriver-manager@^10.2.2, required by protractor@4.0.9
npm ERR! not ok code 0
```

lemur depends explicitly on gulp-protractor 0.0.11 explicitly
gulp-protractor 0.0.11 depends on protractor at _any_ version (*)
The latest versions of protractor (@4) require much newer versions of nodejs
according to the Compatibility section of
https://www.npmjs.com/package/protractor.

gulp-protractor 0.0.12 fixes and constrains some of these dependencies better
and adds a debug option, and fixes a few typos in comments and metadata.
https://github.com/mllrsohn/gulp-protractor/compare/0.0.11...0.0.12
2016-10-14 14:06:13 -07:00
10d833e598 Added Symantec plugin error checking for invalid domain suffix (#449) 2016-10-13 15:23:56 -07:00
708d85abeb Fixes a bug where certificates discovered by lemur's source plugins were not given the appropriate default notifications. (#447) 2016-10-11 21:08:13 -07:00
ee028382df Show only roles that the user is a member of, in list view, for other views show all roles such that certificates and authorities can be shared across teams/groups. (#446) 2016-10-11 17:56:38 -07:00
c05a49f8c9 Fixes an issuer where a member of a role is not able to add new users to said role. (#445) 2016-10-11 17:24:15 -07:00
35cfb50955 add variables to the documentation forwq oauth2 (#444) 2016-10-11 17:23:25 -07:00
f179e74a4a Fix Java export default password generator (#441)
When exporting a certificate, the password is an optional parameter.
When a password is not supplied by the caller, a default password is
generated by the method. The generation library creates the random
password as a bytes object. The bytes object raises an error in the
'keytool' command used to export the certificate. The keytool is
expecting the password to be a str object.

The fix is to decode the generated password from a bytes object to a str
object.

The associated Java plugin tests have been updated to verify the export
method returns the password as a str object. In addition, the tests have
been updated to correctly test the export methods response object. The
original tests treated the response as a single object. The current
export methods return a tuple of data (type, password, data).

In order to make the tests compatible with both Python2 and Python3, the
'six' library was used to test the password is in fact a string.
2016-10-10 22:43:23 -07:00
9065aa3750 Update the private key regex validation (#435)
* Update the private key regex validation

Private keys provided by the Let's Encrypt certificate authority as part
of their certificate bundle fail the import/upload certificate private
key validation. The validation is looking for a specific character
sequence at the begin of the certificate. In order to support valid
Let's Encrypt private keys, the regex has been updated to check for both
the existing sequence and the Let's Encrypt character sequence.

Example Let's Encrypt private key:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvsiwV8A5+r0tQ
QzUAJO0DfoEb9tMWvoFi0DLs9tx88IwMqItPPl9+RNzQnv9qqZR1h4W97sxP8aWY
...
AeS667IJO/2DMKaGiEldaVZtgqdUhCL8Rm4XUFVb1GjLa03E4VRU6W7eQ4hgT2a7
cHDAR8MiovNyfT0fm8Xz3ac=
-----END PRIVATE KEY-----

* Add private key regex for footer

Update the import/upload private key validation regex to verify both the
header and footer are matching.
2016-10-10 22:42:09 -07:00
96e42c793e Refactors the default notification option. Also ensures that notifications and destinations are easier to test. (#437) 2016-10-09 00:06:53 -07:00
72a390c563 Ensure the openssl and cryptography work under python3. (#438) 2016-10-09 00:06:15 -07:00
a19c918c68 Closes #411 (#439) 2016-10-09 00:06:03 -07:00
c45c23ae6f Detect root (#440)
* Closes #430
2016-10-09 00:05:50 -07:00
5cbf5365c5 Active S3 destination plugin (#433)
* Activate the AWS S3 destination plugin

Add the AWS S3 destination plugin to the list of available Lemur
plugins.

Update the S3 destination plugin's "accountNumber" option to be of type
'str' to handle account numbers starting with zeros.

Update Lemur's utils for parsing certificates to correctly encode the
X509 certificates before loading for python3.

* Add S3 destination plugin test

Added simple test to verify S3 destination plugin is available.
2016-10-08 17:06:20 -07:00
3ad7a37f95 Fix import certificate private key encoding (#434)
When importing a certificate, the private key is passed to the
import/upload process from the UI as a str object. In Python3 this
raises two issues when processing the private key - the private key
validation fails and database insert of the certificate fails.

The fix in both cases is to correctly encode the private key as a bytes
object.
2016-10-08 17:04:54 -07:00
6cac2838e3 Fix for missing profile pic. (#429) 2016-09-27 13:02:01 -07:00
fbbf7f90f6 Fix test certificates module hanging issue (#427)
* Fix test certificates module hanging issue

When executing the lemur/tests/test_certificates.py module's tests, all
tests are executed, but the test process appears to hang and never
completes with the display of the results for the tests.

The hanging issue is traced to the two test methods:
test_import(logged_in_user) and test_upload(logged_in_user). The issue
has to do with the test methods' using the logged_in_user(app) fixture from
the conftest.py module as the method parameter.

The test methods at issue require the session, db, and app fixtures to
be initialized for the tests to complete successfully. The
logged_in_user() fixture only initializes the app fixture. Updating the
test_import() and test_upload() methods parameters to be the "session"
fixture fixes the hanging issue and the tests complete successfully.

This is the command being used to execute the tests...
$ py.test -s -v lemur/tests/test_certificates.py

* Update fix for test certificates hanging issue

Based on feedback from the original pull request for this fix, added the
session fixture to the logged_in_user fixture and reverted the
test_import() and test_upload() methods to use the logged_in_user
(instead of the session fixture).
2016-09-27 13:01:37 -07:00
1ea75a5d2d fix(certificates): import re module (#428) 2016-09-21 22:54:46 -07:00
3ce87c8a6b Fix code reference to older version of flake8 (#426)
The pre-commit module contained a reference to the variable
DEFAULT_CONFIG which does not exist in the version of flake8 packaged
with the project. The DEFAULT_CONFIG defines the path to the current
user's local flake8 config file.

The flake8 2.6.2 version packaged with project has been refactored to
set the local flake8 config file in the flake8/main.py module.
2016-09-18 11:05:29 -07:00
39645a1a84 feat(certificates): add support for restricted domains (#424)
Lemur's documentation already mentions LEMUR_RESTRICTED_DOMAINS, a list
of regular expressions matching domains only administrators can issue
certificates for. An option to mark domains as sensitive existed in the
API, however the configuration option was not implemented.

Now both ways of sensitivity are checked in the same place.
2016-09-12 16:59:14 -07:00
a60e372c5a Ensuring that password hashes are compared correctly under python3 2016-09-07 13:25:51 -07:00
76cece7b90 Ensuring that private keys are retrieved correctly under python3. (#422) 2016-09-07 12:34:50 -07:00
ca2944d566 Ensuring the inactive certificates are not alerted on. (#418) 2016-08-29 15:46:35 -07:00
53d0636574 Python3 (#417)
* Fixing tests.

* Fixing issue where decrypted credentials were not returning valid strings.

* Fixing issues with python3 authentication.
2016-08-29 08:58:53 -07:00
7e6278684c Python3 (#416)
* Fixing issue where decrypted credentials were not returning valid strings.
2016-08-26 16:02:23 -07:00
2d7a6ccf3c Owner email (#414)
* Ensuring python2 works with unicode strings.

* adding in owner DN

* fixing tests

* Upgrading requests.

* Fixing tests.
2016-08-25 10:09:46 -07:00
18b99c0de4 Fixing an issue where openssl can't find the certificates to create PKCS12 files (#408) 2016-08-17 10:33:59 -07:00
96674571a5 Fix a typo. UI -> API (#407) 2016-07-29 18:29:44 -07:00
29a330b1f4 Orphaned certificates (#406)
* Fixing whitespace.

* Fixing syncing.

* Fixing tests
2016-07-28 13:08:24 -07:00
a644f45625 Adding some simplified reporting. (#403)
* Adding issuance report.

* Fixing whitespace.
2016-07-27 12:41:32 -07:00
3db669b24d Ensuring that the temporary certificate is created correctly (#400) 2016-07-12 18:07:11 -07:00
f38868a97f Fixing various problems with the syncing of endpoints, throttling sta… (#398)
* Fixing various problems with the syncing of endpoints, throttling stale endpoints etc.
2016-07-12 08:40:49 -07:00
4f3dc5422c Allowing the role-user associated to be updated. (#396)
* Allowing the role-user associated to be updated.

* Fixing tests

* Fixing tests, for real.
2016-07-07 13:03:10 -07:00
1ba7181067 Fixed an issue were default notifications were added even when updati… (#395)
* Fixed an issue were default notifications were added even when updating a certificate, resulting in duplicate notifications.

* Ensuring imported certificates get the same treatment.
2016-07-07 11:44:11 -07:00
74bf54cb8f Slack spruce up (#394)
* Formatting slack message.

* Tweaking tests.
2016-07-06 10:27:13 -07:00
d4732d3ab0 Closes #335. (#392) 2016-07-04 16:08:16 -07:00
cb9631b122 Closes #356. (#391) 2016-07-04 15:38:51 -07:00
4077893d08 Ensuring that destinations require private keys by default. (#390)
* Ensuring that destinations require private keys by default.
2016-07-04 15:30:20 -07:00
4ee1c21144 Closes #372 (#389)
* Closes #372
2016-07-04 14:32:46 -07:00
c8eca56690 Closes #366 (#387) 2016-07-04 13:03:46 -07:00
300e2d0b7d Adding plugin tests. (#385)
* Adding plugin tests.

* Fixing some python 2/3 incompatibilities.
2016-07-01 11:32:19 -07:00
a8040777b3 Upgrading plugin docs with better example. (#386) 2016-07-01 10:50:18 -07:00
e34de921b6 Target Individuals for Certificates (#384)
* Allowing individual users to be targeted for a role.

* Ensuring that even new users get a per user-role
2016-07-01 09:04:39 -07:00
a04f707f63 Fixing readme badges (#382) 2016-06-30 09:06:14 -07:00
9aec899bfd Fixing a few errors.
* Fixing organizational_unit and common name

* FIxing organization name and allow creaters to view CA.
2016-06-29 16:16:37 -07:00
afb66df1a4 Adding plugin information to docs. (#379)
* Adding documentation about the installed plugins.

* Adding new default option.
2016-06-29 10:08:54 -07:00
54b888bb08 Adding a toy certificate authority. (#378) 2016-06-29 09:05:39 -07:00
eefff8497a Adding a new default issuer. 2016-06-28 17:46:26 -07:00
ecbab64c35 Adding endpoint migration script. (#376) 2016-06-28 16:12:56 -07:00
c8447dea3d Fixing a few issues with startup. (#374) 2016-06-28 14:28:05 -07:00
5021e8ba91 Adding ACME Support (#178) 2016-06-27 15:57:53 -07:00
f846d78778 S3 destination (#371) 2016-06-27 15:11:46 -07:00
fe9703dd94 Closes #284 (#336) 2016-06-27 14:40:46 -07:00
b44a7c73d8 Kubernetes desination plugin (#357)
* Kubernetes desination plugin

* fixing build warnings

* fixing build warnings
2016-06-27 14:40:01 -07:00
9ae27f1415 Merge pull request #368 from kevgliss/367-role-permission
Fixes #367
2016-06-23 13:44:46 -07:00
19b928d663 Fixes #367 2016-06-23 13:29:59 -07:00
5193342b3a Merge pull request #365 from kevgliss/docs
Updating flake8 ignore
2016-06-23 09:59:08 -07:00
109fb4bb45 Updating flake8 ignore 2016-06-23 09:40:55 -07:00
d6ccd812c2 Merge pull request #364 from kevgliss/docs
Updating requirements.txt
2016-06-23 09:20:56 -07:00
81a6228028 Updating requirements.txt 2016-06-23 09:20:35 -07:00
eeb216b75e Merge pull request #362 from kevgliss/docs
Fixing documentation requirement.
2016-06-22 14:05:13 -07:00
6714595fee Fixing documentation requirement. 2016-06-22 14:04:41 -07:00
025924c4f7 Merge pull request #361 from kevgliss/docs
Aadding an httpdomain version
2016-06-22 14:02:50 -07:00
7c10c8dac7 adding an httpdomain version 2016-06-22 13:59:32 -07:00
daea8f6ae4 Bug fixes (#355)
* we should not require password to update users

* Fixing an issue were roles would not be added.
2016-06-13 17:22:45 -07:00
41d1fe9191 Using UTC time in JWT token creation (#354)
As stated in PyJWT's documentation [1] and JWT specification [2][3], UTC
times must be used. This commit fixes JWT decoding in servers not using
UTC time.

[1] https://pypi.python.org/pypi/PyJWT/1.4.0
[2] https://tools.ietf.org/html/rfc7519#section-4.1.6
[3] https://tools.ietf.org/html/rfc7519#section-2
2016-06-13 11:18:07 -07:00
7d50e4d65f Merge pull request #353 from mikegrima/issue352
Fix for Issue #352.
2016-06-09 15:13:37 -07:00
9a653403ae Fix for Issue #352. 2016-06-08 16:41:31 -07:00
77f13c9edb Fixing issue were, after a user changes their mind validity years wil… (#349) 2016-06-06 12:11:40 -07:00
d95b1a0a41 release bump (#348) 2016-06-06 09:01:19 -07:00
d9cc4980e8 Fixing destination upload. (#347)
* Fixing an issue where uploaded certificates would have a name of 'None'

* Clarifying comment.

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

* adding some fields

* Adding Source Plugin change.

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

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

* Fixing python3

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

* Fixing some lint.

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

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

* Adding migration scripts.

* Adding endpoints field for future use.

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

* Fixing tests

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

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

* Fixing tests
2016-05-16 11:09:50 -07:00
c11034b9bc Fixes various issues. (#317) 2016-05-16 09:23:48 -07:00
58e8fe0bd0 Fixes various issues. (#316) 2016-05-13 14:35:38 -07:00
a0c8765588 Various bug fixes. (#314) 2016-05-12 12:38:44 -07:00
9022059dc6 Marshmallowing roles (#313) 2016-05-10 14:22:22 -07:00
7f790be1e4 Marsmallowing users (#312) 2016-05-10 14:19:24 -07:00
93791c999d Marsmallowing destinations (#311) 2016-05-10 13:43:26 -07:00
5e9f1437ad Marsmallowing sources (#310) 2016-05-10 13:16:33 -07:00
f9655213b3 Marshmallowing notifications. (#308) 2016-05-10 11:27:57 -07:00
008d608ec4 Fixing error in notifications. (#307) 2016-05-09 17:35:18 -07:00
78c8d12ad8 Cleaning up the way authorities are selected and upgrading uib dependencies. 2016-05-09 17:17:00 -07:00
df0ad4d875 Authorities marshmallow addition (#303) 2016-05-09 11:00:16 -07:00
776e0fcd11 Slack plugin for notifications (#305) 2016-05-08 09:07:16 -07:00
6ec3bad49a Closes #278 (#298)
* Closes #278
2016-05-05 15:28:17 -07:00
52f44c3ea6 Closes #278 and #199, Starting transition to marshmallow (#299)
* Closes #278  and #199, Starting transition to marshmallow
2016-05-05 12:52:08 -07:00
941d36ebfe Merge pull request #302 from kevgliss/301-p12-no-chain
Closes #301
2016-05-04 17:07:42 -07:00
db8243b4b4 Closes #301 2016-05-04 16:56:05 -07:00
f919b7360e Merge pull request #294 from kevgliss/regex
Regex
2016-04-25 17:20:52 -07:00
8e1b7c0036 Removing validation because regex is hard 2016-04-25 16:13:33 -07:00
9b0e0fa9c2 removing validtion from openssl 2016-04-25 16:11:37 -07:00
565d7afa92 Merge pull request #293 from kevgliss/devdocs
Fixes #291
2016-04-25 12:30:54 -07:00
c914ba946f Merge pull request #292 from kevgliss/docs
Fixes #285 Renames sync_sources function to sync to align documentation.
2016-04-25 12:16:47 -07:00
6f9280f64a Adding gulp path 2016-04-25 12:16:33 -07:00
8fe460e401 Fixes #291 2016-04-25 11:34:05 -07:00
b9fe359d23 Fixes #285 Renames sync_sources function to sync to align documentation. 2016-04-25 11:21:25 -07:00
2c6d494c32 Merge pull request #290 from kevgliss/289-java-export-intermediates
Fixes #289 and #275
2016-04-21 16:46:11 -07:00
dbd1279226 Fixes #289 and #275 2016-04-21 16:22:19 -07:00
b463fcf61b Merge pull request #280 from kevgliss/SAN-hotfix
Fixes an issue where custom OIDs would clear out san extensions
2016-04-11 12:04:24 -07:00
82b4f5125d Fixes an issue where custom OIDs would clear out san extensions 2016-04-11 11:17:18 -07:00
3f89d6d009 Merge pull request #271 from kevgliss/195
Closes #195
2016-04-08 12:01:10 -07:00
676f843c92 Merge pull request #276 from kevgliss/san-hotfix
Fixes an issue where custom OIDs would clear out san extensions
2016-04-07 10:30:12 -07:00
c2387dc120 Fixes an issue where custom OIDs would clear out san extensions 2016-04-07 10:29:08 -07:00
9a8e1534c0 Merge pull request #274 from kevgliss/metric_fix
Fixing an issue were metrics would not be sent
2016-04-05 10:50:46 -07:00
dbc4964e94 Fixing an issue were metrics would not be sent 2016-04-05 10:23:33 -07:00
00b263f345 Merge pull request #273 from kevgliss/216
Closes #216
2016-04-01 16:59:49 -07:00
62d03b0d41 Closes #216 2016-04-01 16:54:33 -07:00
b5a4b293a9 Merge pull request #270 from kevgliss/248
Closes #248
2016-04-01 14:28:52 -07:00
bfcfdb83a7 Closes #195 2016-04-01 14:27:57 -07:00
4ccbfa8164 Closes #248 2016-04-01 13:29:08 -07:00
675d10c8a6 Merge pull request #269 from kevgliss/263
Closes #263
2016-04-01 13:08:13 -07:00
2cde7336dc Closes #263 2016-04-01 13:01:56 -07:00
169490dbec Merge pull request #268 from kevgliss/252
Closes #252
2016-04-01 10:16:10 -07:00
3ceb297276 Merge pull request #267 from kevgliss/261
Closes #261
2016-04-01 10:12:10 -07:00
12633bfed6 Merge pull request #266 from kevgliss/tox
removing testing support for py33
2016-04-01 10:11:59 -07:00
5958bac2a2 Merge pull request #265 from kevgliss/257
Closes #257
2016-04-01 10:11:32 -07:00
37f2d5b8b0 Closes #252 2016-04-01 10:09:28 -07:00
47891d2953 Closes #261 2016-04-01 09:58:19 -07:00
af68571f4e removing testing support for py33 2016-04-01 09:52:19 -07:00
d0ec925ca3 Merge pull request #264 from kevgliss/246
Closes #246
2016-04-01 09:51:10 -07:00
939194158a Closes #257 2016-04-01 09:49:44 -07:00
576265e09c Closes #246 2016-04-01 09:19:36 -07:00
dfaf45344c Merge pull request #250 from lfaraone/patch-1
Remove duplicate `install` in Quickstart
2016-03-01 09:21:04 -08:00
6c378957e9 Remove duplicate install in Quickstart 2016-03-01 04:12:10 +00:00
e8f9bc80a0 Merge pull request #249 from kevgliss/master
Updating docs
2016-02-29 12:51:47 -08:00
a30b8b21e4 updating postgres login 2016-02-29 08:53:35 -08:00
12204852aa changeing the default port to 8000 2016-02-29 08:48:27 -08:00
edba980b56 Merge pull request #245 from mikegrima/issue243
Removed deprecated auth api endpoint.
2016-02-16 17:11:41 -08:00
ba666ddbfa Removed deprecated auth api endpoint. 2016-02-16 15:04:53 -08:00
35f9f59c57 Merge pull request #242 from kevgliss/version_bump
version bump
2016-02-05 13:13:01 -08:00
ac1f493338 version bump 2016-02-05 13:12:21 -08:00
1c3c70d460 Merge pull request #241 from kevgliss/0.2.2.release
adding changelog
2016-02-05 13:01:35 -08:00
e8e7bdf9e0 adding changelog 2016-02-05 13:00:59 -08:00
d263e0e60c Merge pull request #240 from kevgliss/234-truststore-permission
Adding a new flag to export plugins 'requires_key' that specifies whe…
2016-01-29 12:55:41 -08:00
028d86c0bb Adding a new flag to export plugins 'requires_key' that specifies whether the export plugin needs access to the private key. Defaults to True. 2016-01-29 12:45:18 -08:00
f8b6830013 Merge pull request #239 from kevgliss/228-filter-values
Fixing documentation for filter format
2016-01-29 11:54:13 -08:00
49a40c50e8 Merge pull request #238 from kevgliss/231-authority-owner
associating new authorities with the owner roles
2016-01-29 11:47:56 -08:00
2ba48995fe Fixing documentation for filter format 2016-01-29 11:47:16 -08:00
3cc8ade6d8 associating new authorities with the owner roles 2016-01-29 10:59:04 -08:00
39c9a0a299 Merge pull request #237 from kevgliss/218_password_regex
relaxing keystore password validation
2016-01-29 10:37:49 -08:00
3ad317fb6d Merge pull request #236 from kevgliss/migration_script_fixups
Removing per 2.0 migration scripts
2016-01-29 10:30:41 -08:00
bd46440d12 relaxing keystore password validation 2016-01-29 10:29:04 -08:00
f3a28814ae Merge pull request #235 from kevgliss/226_replaces
Makes 'replacements' a non-required attribute for importing. Closes #226
2016-01-29 09:42:42 -08:00
9f8f64b9ec removing pre 2.0 migration scripts, and adding documentation for correct path during init 2016-01-29 09:22:12 -08:00
1e524a49c0 making 'replacements' a non-require attribute for importing. Closes #226 2016-01-29 09:02:51 -08:00
467c276fca Merge pull request #227 from AlexCline/fix_postinstall_for_224
Use the local bower instead of the global one.
2016-01-20 16:35:00 -08:00
f610e39418 Use the local bower instead of the global one.
This change updates package.json's postinstall command to use the
locally installed bower, rather than the global bower which might
not exist or might not be in the current user's PATH.
2016-01-20 17:10:41 -05:00
27d977b2fa Merge pull request #214 from ebgcdev/master
Minor spelling fix
2016-01-13 09:21:36 -08:00
b36e72bfcc Minor spelling fix
Using the possessive “Your” rather than “You’re” in “Your passphrase
is:”
2016-01-12 22:04:42 -08:00
e49701228d Merge pull request #212 from kevgliss/rolling
Adding a rolling metric count
2016-01-11 15:34:20 -08:00
48f8b33d7d Adding a rolling metric count 2016-01-11 15:26:32 -08:00
d87ace8c89 Merge pull request #211 from kevgliss/hotfix
fixing an issue were urllib does not like unicode
2016-01-11 10:38:45 -08:00
b1326d4145 fixing an issue were urllib does not like unicode 2016-01-11 10:31:58 -08:00
7c2862c958 Merge pull request #210 from kevgliss/hotfix
Fixes an assumption that 'subAltNames' are always passed to the API.
2016-01-11 09:08:38 -08:00
0a4f5ad64d Fixing an assumption that 'subAltNames' are always passed to the API. 2016-01-10 17:33:19 -08:00
c617a11c55 Merge pull request #209 from kevgliss/migrate_chain
Adding command to transparently rotate the chain on an ELB
2016-01-10 14:37:29 -08:00
053167965a Adding command to transparently rotate the chain on an ELB 2016-01-10 14:20:36 -08:00
a7ac45b937 Merge pull request #206 from kevgliss/syncing
Fixing issue where we were seeing AWS API errors due to certificates …
2016-01-08 16:39:51 -08:00
5482bbf4bd Fixing issue where we were seeing AWS API errors due to certificates not having private keys and could not be uploaded or 'synced' 2016-01-07 13:42:46 -08:00
0a58e106b5 Merge pull request #205 from rpicard/rpicard/fixgooglesso
Fix how the provider settings are passed to Satellizer
2016-01-05 17:31:35 -08:00
a1395a5808 Fix how the provider settings are passed to Satellizer 2016-01-05 17:26:09 -08:00
a0d50ef03a Merge pull request #203 from kevgliss/ssoHostfix
fixing typo
2016-01-05 09:41:12 -08:00
685e2c8b6d fixing typo 2016-01-05 09:40:53 -08:00
c6d9a20fe5 Merge pull request #202 from kevgliss/hotfix
reverting depedency
2016-01-04 13:58:36 -08:00
4a952d867b reverting depedency 2016-01-04 13:58:12 -08:00
cb4cf43fcf Merge pull request #201 from kevgliss/hotfix
Fixing setup.py
2016-01-04 11:48:19 -08:00
1bce7a832b Fixing setup.py 2016-01-04 11:46:07 -08:00
574234f70f Merge pull request #200 from kevgliss/requirements
updating dependencies
2016-01-04 10:41:24 -08:00
42e5470dd0 updating dependencies 2016-01-04 10:36:39 -08:00
8199365324 Merge pull request #194 from CameronNemo/patch-1
docs/quickstart: fix port number
2015-12-31 14:27:18 -08:00
86c92eb31e docs/quickstart: fix port number 2015-12-31 12:57:18 -08:00
d9fd952c03 Merge pull request #193 from kevgliss/docs
Improving documentation layout
2015-12-31 11:21:48 -08:00
967c7ded8d Improving documentation layout 2015-12-31 11:12:56 -08:00
a4bf847b56 Merge pull request #192 from kevgliss/sensitive-domains
Adds ability for domains to be marked as sensitive and only be allowe…
2015-12-30 15:36:31 -08:00
d6917155e8 Fixing tests 2015-12-30 15:32:01 -08:00
3f024c1ef4 Adds ability for domains to be marked as sensitive and only be allowed to be issued by an admin closes #5 2015-12-30 15:11:08 -08:00
96d253f0f9 Merge pull request #191 from kevgliss/bump
version bump
2015-12-30 09:15:30 -08:00
9b166fb9a9 version bump 2015-12-30 09:15:11 -08:00
378 changed files with 24920 additions and 7282 deletions

5
.coveragerc Normal file
View File

@ -0,0 +1,5 @@
[report]
include = lemur/*.py
omit = lemur/migrations/*
lemur/tests/*

8
.gitignore vendored
View File

@ -1,3 +1,4 @@
/.cache
.coverage .coverage
.tox .tox
.DS_Store .DS_Store
@ -12,6 +13,7 @@
MANIFEST MANIFEST
test.conf test.conf
pip-log.txt pip-log.txt
package-lock.json
/htmlcov /htmlcov
/cover /cover
/build /build
@ -26,5 +28,7 @@ pip-log.txt
docs/_build docs/_build
.editorconfig .editorconfig
.idea .idea
test.conf lemur/tests/tmp
lemur/tests/tmp
/lemur/plugins/lemur_email/tests/expiration-rendered.html
/lemur/plugins/lemur_email/tests/rotation-rendered.html

View File

@ -8,7 +8,7 @@
"eqeqeq": true, "eqeqeq": true,
"immed": true, "immed": true,
"indent": 2, "indent": 2,
"latedef": true, "latedef": false,
"newcap": false, "newcap": false,
"noarg": true, "noarg": true,
"quotmark": "single", "quotmark": "single",
@ -22,6 +22,8 @@
"angular": false, "angular": false,
"moment": false, "moment": false,
"toaster": false, "toaster": false,
"d3": false,
"self": false,
"_": false "_": false
} }
} }

10
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,10 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: v0.9.1
hooks:
- id: trailing-whitespace
- id: flake8
- id: check-merge-conflict
- repo: git://github.com/pre-commit/mirrors-jshint
sha: v2.9.5
hooks:
- id: jshint

View File

@ -1,18 +1,17 @@
sudo: false
language: python language: python
sudo: required
dist: trusty
node_js:
- "6.2.0"
addons: addons:
postgresql: "9.4" postgresql: "9.4"
matrix: matrix:
include: include:
- python: "2.7" - python: "3.5"
env: TOXENV=py27 env: TOXENV=py35
- python: "3.3"
env: TOXENV=py33
- python: "3.4"
env: TOXENV=py34
cache: cache:
directories: directories:
@ -22,14 +21,27 @@ cache:
env: env:
global: global:
- PIP_DOWNLOAD_CACHE=".pip_download_cache" - PIP_DOWNLOAD_CACHE=".pip_download_cache"
# do not load /etc/boto.cfg with Python 3 incompatible plugin
# https://github.com/travis-ci/travis-ci/issues/5246#issuecomment-166460882
- BOTO_CONFIG=/doesnotexist
before_script: before_script:
- psql -c "create database lemur;" -U postgres - psql -c "create database lemur;" -U postgres
- psql -c "create user lemur with password 'lemur;'" -U postgres - psql -c "create user lemur with password 'lemur;'" -U postgres
- npm config set registry https://registry.npmjs.org
- npm install -g bower - npm install -g bower
- pip install --upgrade setuptools
install:
- pip install coveralls
- pip install bandit
script: script:
- make test - make test
- bandit -r . -ll -ii -x lemur/tests/,docs
after_success:
- coveralls
notifications: notifications:
email: email:

View File

@ -1,10 +1,206 @@
Changelog Changelog
========= =========
0.2.2 - `master` _
0.7 - `2018-05-07`
~~~~~~~~~~~~~~
This release adds LetsEncrypt support with DNS providers Dyn, Route53, and Cloudflare, and expands on the pending certificate functionality.
The linux_dst plugin will also be deprecated and removed.
The pending_dns_authorizations and dns_providers tables were created. New columns
were added to the certificates and pending_certificates tables, (For the DNS provider ID), and authorities (For options).
Please run a database migration when upgrading.
The Let's Encrypt flow will run asynchronously. When a certificate is requested through the acme-issuer, a pending certificate
will be created. A cron needs to be defined to run `lemur pending_certs fetch_all_acme`. This command will iterate through all of the pending
certificates, request a DNS challenge token from Let's Encrypt, and set the appropriate _acme-challenge TXT entry. It will
then iterate through and resolve the challenges before requesting a certificate for each pending certificate. If a certificate
is successfully obtained, the pending_certificate will be moved to the certificates table with the appropriate properties.
Special thanks to all who helped with this release, notably:
- The folks at Cloudflare
- dmitryzykov
- jchuong
- seils
- titouanc
Upgrading
---------
.. note:: This release will need a migration change. Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
0.6 - `2018-01-02`
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
.. note:: This version not yet released and is under active development Happy Holidays! This is a big release with lots of bug fixes and features. Below are the highlights and are not exhaustive.
Features:
* Per-certificate rotation policies, requires a database migration. The default rotation policy for all certificates.
is 30 days. Every certificate will gain a policy regardless of if auto-rotation is used.
* Adds per-user API Keys, allows users to issue multiple long-lived API tokens with the same permission as the user creating them.
* Adds the ability to revoke certificates from the Lemur UI/API, this is currently only supported for the digicert CIS and cfssl plugins.
* Allow destinations to support an export function. Useful for file system destinations e.g. S3 to specify the export plugin you wish to run before being sent to the destination.
* Adds support for uploading certificates to Cloudfront.
* Re-worked certificate metadata pane for improved readability.
* Adds support for LDAP user authentication
Bugs:
* Closed `#767 <https://github.com/Netflix/lemur/issues/767>`_ - Fixed issue with login redirect loop.
* Closed `#792 <https://github.com/Netflix/lemur/issues/792>`_ - Fixed an issue with a unique constraint was violated when replacing certificates.
* Closed `#752 <https://github.com/Netflix/lemur/issues/752>`_ - Fixed an internal server error when validating notification units.
* Closed `#684 <https://github.com/Netflix/lemur/issues/684>`_ - Fixed migration failure when null values encountered.
* Closes `#661 <https://github.com/Netflix/lemur/issues/661>`_ - Fixed an issue where default values were missing during clone operations.
Special thanks to all who helped with this release, notably:
- intgr
- SecurityInsanity
- johanneslange
- RickB17
- pr8kerl
- bunjiboys
See the full list of issues closed in `0.6 <https://github.com/Netflix/lemur/milestone/5>`_.
Upgrading
---------
.. note:: This release will need a migration change. Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
0.5 - `2016-04-08`
~~~~~~~~~~~~~~~~~~
This release is most notable for dropping support for python2.7. All Lemur versions >0.4 will now support python3.5 only.
Big thanks to neilschelly for quite a lot of improvements to the `lemur-cryptography` plugin.
Other Highlights:
* Closed `#501 <https://github.com/Netflix/lemur/issues/501>`_ - Endpoint resource as now kept in sync via an
expiration mechanism. Such that non-existant endpoints gracefully fall out of Lemur. Certificates are never
removed from Lemur.
* Closed `#551 <https://github.com/Netflix/lemur/pull/551>`_ - Added the ability to create a 4096 bit key during certificate
creation. Closed `#528 <https://github.com/Netflix/lemur/pull/528>`_ to ensure that issuer plugins supported the new 4096 bit keys.
* Closed `#566 <https://github.com/Netflix/lemur/issues/566>`_ - Fixed an issue changing the notification status for certificates
without private keys.
* Closed `#594 <https://github.com/Netflix/lemur/issues/594>`_ - Added `replaced` field indicating if a certificate has been superseded.
* Closed `#602 <https://github.com/Netflix/lemur/issues/602>`_ - AWS plugin added support for ALBs for endpoint tracking.
Special thanks to all who helped with this release, notably:
- RcRonco
- harmw
- jeremyguarini
See the full list of issues closed in `0.5 <https://github.com/Netflix/lemur/milestone/4>`_.
Upgrading
---------
.. note:: This release will need a slight migration change. Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
0.4 - `2016-11-17`
~~~~~~~~~~~~~~~~~~
There have been quite a few issues closed in this release. Some notables:
* Closed `#284 <https://github.com/Netflix/lemur/issues/284>`_ - Created new models for `Endpoints` created associated
AWS ELB endpoint tracking code. This was the major stated goal of this milestone and should serve as the basis for
future enhancements of Lemur's certificate 'deployment' capabilities.
* Closed `#334 <https://github.com/Netflix/lemur/issues/334>`_ - Lemur not has the ability
to restrict certificate expiration dates to weekdays.
Several fixes/tweaks to Lemurs python3 support (thanks chadhendrie!)
This will most likely be the last release to support python2.7 moving Lemur to target python3 exclusively. Please comment
on issue #340 if this negatively affects your usage of Lemur.
See the full list of issues closed in `0.4 <https://github.com/Netflix/lemur/milestone/3>`_.
Upgrading
---------
.. note:: This release will need a slight migration change. Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
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 an 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 to 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 an 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
explicit, 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 0.2.1 - 2015-12-14
@ -19,7 +215,7 @@ Changelog
0.2.0 - 2015-12-02 0.2.0 - 2015-12-02
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
* Closed #120 - Error messages not displaying long enough * 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 #121 - Certificate create form should not be valid until a Certificate Authority object is available
@ -35,8 +231,8 @@ Changelog
0.1.5 - 2015-10-26 0.1.5 - 2015-10-26
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
* **SECURITY ISSUE**: Switched from use a AES static key to Fernet encryption. * **SECURITY ISSUE**: Switched from use an AES static key to Fernet encryption.
Affects all versions prior to 0.1.5. If upgrading this will require a data migration. 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>`_ see: `Upgrading Lemur <https://lemur.readthedocs.io/administration#UpgradingLemur>`_

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM python:3.5
RUN apt-get update
RUN apt-get install -y make python-software-properties curl
RUN curl -sL https://deb.nodesource.com/setup_7.x | bash -
RUN apt-get update
RUN apt-get install -y nodejs libldap2-dev libsasl2-dev libldap2-dev libssl-dev
RUN pip install -U setuptools
RUN pip install coveralls bandit
WORKDIR /app
COPY . /app/
RUN pip install -e .
RUN pip install "file://`pwd`#egg=lemur[dev]"
RUN pip install "file://`pwd`#egg=lemur[tests]"

View File

@ -1,4 +1,3 @@
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
@ -187,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2014 Netflix, Inc. Copyright 2018 Netflix, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -1,16 +1,38 @@
NPM_ROOT = ./node_modules NPM_ROOT = ./node_modules
STATIC_DIR = src/lemur/static/app STATIC_DIR = src/lemur/static/app
SHELL=/bin/bash
USER := $(shell whoami)
develop: update-submodules setup-git develop: update-submodules setup-git
@echo "--> Installing dependencies" @echo "--> Installing dependencies"
ifeq ($(USER), root)
@echo "WARNING: It looks like you are installing Lemur as root. This is not generally advised."
npm install --unsafe-perm
else
npm install npm install
endif
pip install "setuptools>=0.9.8" pip install "setuptools>=0.9.8"
# order matters here, base package must install first # order matters here, base package must install first
pip install -e . pip install -e .
pip install "file://`pwd`#egg=lemur[dev]" pip install "file://`pwd`#egg=lemur[dev]"
pip install "file://`pwd`#egg=lemur[tests]" pip install "file://`pwd`#egg=lemur[tests]"
node_modules/.bin/gulp build node_modules/.bin/gulp build
node_modules/.bin/gulp package node_modules/.bin/gulp package --urlContextPath=$(urlContextPath)
@echo ""
release:
@echo "--> Installing dependencies"
ifeq ($(USER), root)
@echo "WARNING: It looks like you are installing Lemur as root. This is not generally advised."
npm install --unsafe-perm
else
npm install
endif
pip install "setuptools>=0.9.8"
# order matters here, base package must install first
pip install -e .
node_modules/.bin/gulp build
node_modules/.bin/gulp package --urlContextPath=$(urlContextPath)
@echo "" @echo ""
dev-docs: dev-docs:
@ -41,7 +63,7 @@ test: develop lint test-python
testloop: develop testloop: develop
pip install pytest-xdist pip install pytest-xdist
py.test tests -f coverage run --source lemur -m py.test
test-cli: test-cli:
@echo "--> Testing CLI" @echo "--> Testing CLI"
@ -60,7 +82,7 @@ test-js:
test-python: test-python:
@echo "--> Running Python tests" @echo "--> Running Python tests"
py.test lemur/tests || exit 1 coverage run --source lemur -m py.test
@echo "" @echo ""
lint: lint-python lint-js lint: lint-python lint-js
@ -82,4 +104,24 @@ coverage: develop
publish: publish:
python setup.py sdist bdist_wheel upload python setup.py sdist bdist_wheel upload
.PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish up-reqs:
ifndef VIRTUAL_ENV
$(error Please activate virtualenv first)
endif
@echo "--> Updating Python requirements"
pip install --upgrade pip
pip install --upgrade pip-tools
pip-compile --output-file requirements-docs.txt requirements-docs.in -U --no-index
pip-compile --output-file requirements-dev.txt requirements-dev.in -U --no-index
pip-compile --output-file requirements-tests.txt requirements-tests.in -U --no-index
pip-compile --output-file requirements.txt requirements.in -U --no-index
@echo "--> Done updating Python requirements"
@echo "--> Removing python-ldap from requirements-docs.txt"
grep -v "python-ldap" requirements-docs.txt > tempreqs && mv tempreqs requirements-docs.txt
@echo "--> Installing new dependencies"
pip install -e .
@echo "--> Done installing new dependencies"
@echo ""
.PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish release

View File

@ -5,36 +5,31 @@ Lemur
:alt: Join the chat at https://gitter.im/Netflix/lemur :alt: Join the chat at https://gitter.im/Netflix/lemur
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge :target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. image:: https://img.shields.io/pypi/v/lemur.svg
:target: https://pypi.python.org/pypi/lemur/
:alt: Latest Version
.. image:: https://readthedocs.org/projects/lemur/badge/?version=latest .. image:: https://readthedocs.org/projects/lemur/badge/?version=latest
:target: https://lemur.readthedocs.org :target: https://lemur.readthedocs.io
:alt: Latest Docs :alt: Latest Docs
.. image:: https://img.shields.io/badge/NetflixOSS-active-brightgreen.svg
.. image:: https://travis-ci.org/Netflix/lemur.svg .. image:: https://travis-ci.org/Netflix/lemur.svg
:target: https://travis-ci.org/Netflix/lemur :target: https://travis-ci.org/Netflix/lemur
.. image:: https://requires.io/github/Netflix/lemur/requirements.svg?branch=master .. image:: https://coveralls.io/repos/github/Netflix/lemur/badge.svg?branch=master
:target: https://requires.io/github/Netflix/lemur/requirements/?branch=master :target: https://coveralls.io/github/Netflix/lemur?branch=master
:alt: Requirements Status
.. image:: https://badge.waffle.io/Netflix/lemur.png?label=ready&title=Ready
:target: https://waffle.io/Netflix/lemur
:alt: 'Stories in Ready'
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs 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. 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 3.5. We deploy on Ubuntu and develop on OS X.
Project resources Project resources
================= =================
- `Lemur Blog Post <http://techblog.netflix.com/2015/09/introducing-lemur.html>`_ - `Lemur Blog Post <http://techblog.netflix.com/2015/09/introducing-lemur.html>`_
- `Documentation <http://lemur.readthedocs.org/>`_ - `Documentation <http://lemur.readthedocs.io/>`_
- `Source code <https://github.com/netflix/lemur>`_ - `Source code <https://github.com/netflix/lemur>`_
- `Issue tracker <https://github.com/netflix/lemur/issues>`_ - `Issue tracker <https://github.com/netflix/lemur/issues>`_
- `Docker <https://github.com/Netflix/lemur-docker>`_ - `Docker <https://github.com/Netflix/lemur-docker>`_

View File

@ -6,43 +6,45 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"angular": "1.3", "jquery": "~2.2.0",
"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",
"angular-wizard": "~0.4.0", "angular-wizard": "~0.4.0",
"bootswatch": "3.3.1+2", "angular": "1.4.9",
"angular-spinkit": "~0.3.3", "json3": "~3.3",
"angular-bootstrap": "~0.12.0", "es5-shim": "~4.5.0",
"angular-ui-switch": "~0.1.0", "bootstrap": "~3.3.6",
"angular-chart.js": "~0.7.1", "angular-bootstrap": "~1.1.1",
"satellizer": "~0.9.4", "angular-animate": "~1.4.9",
"angularjs-toaster": "~0.4.14", "restangular": "~1.5.1",
"ngletteravatar": "~3.0.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-clipboard": "~1.3.0",
"angularjs-toaster": "~1.0.0",
"angular-chart.js": "~0.8.8",
"ngletteravatar": "~4.0.0",
"bootswatch": "~3.3.6",
"fontawesome": "~4.5.0",
"satellizer": "~0.13.4",
"angular-ui-router": "~0.2.15", "angular-ui-router": "~0.2.15",
"angular-clipboard": "~1.1.1", "font-awesome": "~4.5.0",
"angular-file-saver": "~1.0.1" "lodash": "~4.0.1",
}, "underscore": "~1.8.3",
"devDependencies": { "angular-smart-table": "2.1.8",
"angular-mocks": "~1.3", "angular-strap": ">= 2.2.2",
"angular-scenario": "~1.3", "angular-underscore": "^0.5.0",
"ngletteravatar": "~3.0.1" "angular-translate": "^2.9.0",
"angular-ui-switch": "~0.1.0",
"angular-sanitize": "~1.5.0",
"angular-file-saver": "~1.0.1",
"angular-ui-select": "~0.17.1",
"d3": "^3.5.17"
}, },
"resolutions": { "resolutions": {
"bootstrap": "~3.3.1", "moment": ">=2.8.0 <2.11.0",
"angular": "1.3" "lodash": ">=1.3.0 <2.5.0",
"angular": "1.4.9"
}, },
"ignore": [ "ignore": [
"**/.*", "**/.*",

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
---
version: '2.0'
services:
test:
build: .
volumes:
- ".:/app"
links:
- postgres
command: make test
environment:
SQLALCHEMY_DATABASE_URI: postgresql://lemur:lemur@postgres:5432/lemur
VIRTUAL_ENV: 'true'
postgres:
image: postgres:9.4
environment:
POSTGRES_USER: lemur
POSTGRES_PASSWORD: lemur

View File

@ -7,6 +7,10 @@ Configuration
that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management <CredentialManagement>` 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. for more information.
.. note::
All configuration values are python strings unless otherwise noted.
Basic Configuration Basic Configuration
------------------- -------------------
@ -24,16 +28,14 @@ Basic Configuration
LOG_FILE = "/logs/lemur/lemur-test.log" LOG_FILE = "/logs/lemur/lemur-test.log"
.. data:: DEBUG
.. data:: debug
:noindex: :noindex:
Sets the flask debug flag to true (if supported by the webserver) Sets the flask debug flag to true (if supported by the webserver)
:: ::
debug = False DEBUG = False
.. warning:: .. warning::
This should never be used in a production environment as it exposes Lemur to This should never be used in a production environment as it exposes Lemur to
@ -53,7 +55,7 @@ Basic Configuration
CORS = False CORS = False
.. data:: SQLACHEMY_DATABASE_URI .. data:: SQLALCHEMY_DATABASE_URI
:noindex: :noindex:
If you have ever used sqlalchemy before this is the standard connection string used. Lemur uses a postgres database and the connection string would look something like: If you have ever used sqlalchemy before this is the standard connection string used. Lemur uses a postgres database and the connection string would look something like:
@ -63,11 +65,59 @@ Basic Configuration
SQLALCHEMY_DATABASE_URI = 'postgresql://<user>:<password>@<hostname>:5432/lemur' SQLALCHEMY_DATABASE_URI = 'postgresql://<user>:<password>@<hostname>:5432/lemur'
.. data:: LEMUR_RESTRICTED_DOMAINS .. data:: SQLALCHEMY_POOL_SIZE
:noindex:
The default connection pool size is 5 for sqlalchemy managed connections. Depending on the number of Lemur instances,
please specify per instance connection pool size. Below is an example to set connection pool size to 10.
::
SQLALCHEMY_POOL_SIZE = 10
.. warning::
This is an optional setting but important to review and set for optimal database connection usage and for overall database performance.
.. data:: SQLALCHEMY_MAX_OVERFLOW
:noindex:
This setting allows to create connections in addition to specified number of connections in pool size. By default, sqlalchemy
allows 10 connections to create in addition to the pool size. This is also an optional setting. If `SQLALCHEMY_POOL_SIZE` and
`SQLALCHEMY_MAX_OVERFLOW` are not speficied then each Lemur instance may create maximum of 15 connections.
::
SQLALCHECK_MAX_OVERFLOW = 0
.. note::
Specifying the `SQLALCHEMY_MAX_OVERFLOW` to 0 will enforce limit to not create connections above specified pool size.
.. data:: LEMUR_ALLOW_WEEKEND_EXPIRATION
:noindex: :noindex:
This allows the administrator to mark a subset of domains or domains matching a particular regex as Specifies whether to allow certificates created by Lemur to expire on weekends. Default is True.
*restricted*. This means that only an administrator is allows to issue the domains in question.
.. data:: LEMUR_WHITELISTED_DOMAINS
:noindex:
List of regular expressions for domain restrictions; if the list is not empty, normal users can only issue
certificates for domain names matching at least one pattern on this list. Administrators are exempt from this
restriction.
Cerificate common name is matched against these rules *if* it does not contain a space. SubjectAltName DNS names
are always matched against these rules.
Take care to write patterns in such way to not allow the `*` wildcard character inadvertently. To match a `.`
character, it must be escaped (as `\.`).
.. data:: LEMUR_OWNER_EMAIL_IN_SUBJECT
:noindex:
By default, Lemur will add the certificate owner's email address to certificate subject (for CAs that allow it).
Set this to `False` to disable this.
.. data:: LEMUR_TOKEN_SECRET .. data:: LEMUR_TOKEN_SECRET
:noindex: :noindex:
@ -106,6 +156,12 @@ Basic Configuration
LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s='] LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']
.. data:: DEBUG_DUMP
:noindex:
Dump all imported or generated CSR and certificate details to stdout using OpenSSL. (default: `False`)
Certificate Default Options Certificate Default Options
--------------------------- ---------------------------
@ -145,7 +201,7 @@ and are used when Lemur creates the CSR for your certificates.
LEMUR_DEFAULT_ORGANIZATION = "Netflix" LEMUR_DEFAULT_ORGANIZATION = "Netflix"
.. data:: LEMUR_DEFAULT_ORGANIZATION_UNIT .. data:: LEMUR_DEFAULT_ORGANIZATIONAL_UNIT
:noindex: :noindex:
:: ::
@ -153,6 +209,22 @@ and are used when Lemur creates the CSR for your certificates.
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations" LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations"
.. data:: LEMUR_DEFAULT_ISSUER_PLUGIN
:noindex:
::
LEMUR_DEFAULT_ISSUER_PLUGIN = "verisign-issuer"
.. data:: LEMUR_DEFAULT_AUTHORITY
:noindex:
::
LEMUR_DEFAULT_AUTHORITY = "verisign"
Notification Options Notification Options
-------------------- --------------------
@ -173,47 +245,329 @@ Lemur supports sending certification expiration notifications through SES and SM
.. data:: LEMUR_EMAIL_SENDER .. data:: LEMUR_EMAIL_SENDER
:noindex: :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:: .. note::
If using STMP as your provider you will need to define additional configuration options as specified by Flask-Mail. If using SMTP as your provider you will need to define additional configuration options as specified by Flask-Mail.
See: `Flask-Mail <https://pythonhosted.org/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 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>`_ 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
.. data:: LEMUR_EMAIL
:noindex: :noindex:
Lemur sender's email Lemur sender's email
:: ::
LEMUR_MAIL = 'lemur.example.com' LEMUR_EMAIL = 'lemur.example.com'
.. data:: LEMUR_SECURITY_TEAM_EMAIL .. data:: LEMUR_SECURITY_TEAM_EMAIL
:noindex: :noindex:
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate. This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
:: ::
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com'] LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
.. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS .. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS
:noindex: :noindex:
Lemur notification intervals Lemur notification intervals
:: ::
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2] LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
Authority Options Authentication Options
----------------- ----------------------
Lemur currently supports Basic Authentication, LDAP Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
LDAP Options
~~~~~~~~~~~~
Lemur supports the use of an LDAP server in conjunction with Basic Authentication. Lemur local users can still be defined and take precedence over LDAP users. If a local user does not exist, LDAP will be queried for authentication. Only simple ldap binding with or without TLS is supported.
LDAP support requires the pyldap python library, which also depends on the following openldap packages.
.. code-block:: bash
$ sudo apt-get update
$ sudo apt-get install libldap2-dev libsasl2-dev libldap2-dev libssl-dev
To configure the use of an LDAP server, a number of settings need to be configured in `lemur.conf.py`.
Here is an example LDAP configuration stanza you can add to your config. Adjust to suit your environment of course.
.. code-block:: python
LDAP_AUTH = True
LDAP_BIND_URI='ldaps://secure.evilcorp.net'
LDAP_BASE_DN='DC=users,DC=evilcorp,DC=net'
LDAP_EMAIL_DOMAIN='evilcorp.net'
LDAP_USE_TLS = True
LDAP_CACERT_FILE = '/opt/lemur/trusted.pem'
LDAP_REQUIRED_GROUP = 'certificate-management-access'
LDAP_GROUPS_TO_ROLES = {'certificate-management-admin': 'admin', 'certificate-management-read-only': 'read-only'}
The lemur ldap module uses the `user principal name` (upn) of the authenticating user to bind. This is done once for each user at login time. The UPN is effectively the email address in AD/LDAP of the user. If the user doesn't provide the email address, it constructs one based on the username supplied (which should normally match the samAccountName) and the value provided by the config LDAP_EMAIL_DOMAIN.
The config LDAP_BASE_DN tells lemur where to search within the AD/LDAP tree for the given UPN (user). If the bind with those credentials is successful - there is a valid user in AD with correct password.
Each of the LDAP options are described below.
.. data:: LDAP_AUTH
:noindex:
This enables the use of LDAP
::
LDAP_AUTH = True
.. data:: LDAP_BIND_URI
:noindex:
Specifies the LDAP server connection string
::
LDAP_BIND_URI = 'ldaps://hostname'
.. data:: LDAP_BASE_DN
:noindex:
Specifies the LDAP distinguished name location to search for users
::
LDAP_BASE_DN = 'DC=Users,DC=Evilcorp,DC=com'
.. data:: LDAP_EMAIL_DOMAIN
:noindex:
The email domain used by users in your directory. This is used to build the userPrincipalName to search with.
::
LDAP_EMAIL_DOMAIN = 'evilcorp.com'
The following LDAP options are not required, however TLS is always recommended.
.. data:: LDAP_USE_TLS
:noindex:
Enables the use of TLS when connecting to the LDAP server. Ensure the LDAP_BIND_URI is using ldaps scheme.
::
LDAP_USE_TLS = True
.. data:: LDAP_CACERT_FILE
:noindex:
Specify a Certificate Authority file containing PEM encoded trusted issuer certificates. This can be used if your LDAP server is using certificates issued by a private CA.
::
LDAP_CACERT_FILE = '/path/to/cacert/file'
.. data:: LDAP_REQUIRED_GROUP
:noindex:
Lemur has pretty open permissions. You can define an LDAP group to specify who can access Lemur. Only members of this group will be able to login.
::
LDAP_REQUIRED_GROUP = 'Lemur LDAP Group Name'
.. data:: LDAP_GROUPS_TO_ROLES
:noindex:
You can also define a dictionary of ldap groups mapped to lemur roles. This allows you to use ldap groups to manage access to owner/creator roles in Lemur
::
LDAP_GROUPS_TO_ROLES = {'lemur_admins': 'admin', 'Lemur Team DL Group': 'team@example.com'}
Authentication Providers
~~~~~~~~~~~~~~~~~~~~~~~~
If you are not using an authentication provider you do not need to configure any of these options.
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
.. data:: ACTIVE_PROVIDERS
:noindex:
::
ACTIVE_PROVIDERS = ["ping", "google", "oauth2"]
.. data:: PING_SECRET
:noindex:
::
PING_SECRET = 'somethingsecret'
.. data:: PING_ACCESS_TOKEN_URL
:noindex:
::
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
.. data:: PING_USER_API_URL
:noindex:
::
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
.. data:: PING_JWKS_URL
:noindex:
::
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
.. data:: PING_NAME
:noindex:
::
PING_NAME = "Example Oauth2 Provider"
.. data:: PING_CLIENT_ID
:noindex:
::
PING_CLIENT_ID = "client-id"
.. data:: PING_REDIRECT_URI
:noindex:
::
PING_REDIRECT_URI = "https://<yourlemurserver>/api/1/auth/ping"
.. data:: PING_AUTH_ENDPOINT
:noindex:
::
PING_AUTH_ENDPOINT = "https://<yourpingserver>/oauth2/authorize"
.. data:: OAUTH2_SECRET
:noindex:
::
OAUTH2_SECRET = 'somethingsecret'
.. data:: OAUTH2_ACCESS_TOKEN_URL
:noindex:
::
OAUTH2_ACCESS_TOKEN_URL = "https://<youroauthserver> /oauth2/v1/authorize"
.. data:: OAUTH2_USER_API_URL
:noindex:
::
OAUTH2_USER_API_URL = "https://<youroauthserver>/oauth2/v1/userinfo"
.. data:: OAUTH2_JWKS_URL
:noindex:
::
OAUTH2_JWKS_URL = "https://<youroauthserver>/oauth2/v1/keys"
.. data:: OAUTH2_NAME
:noindex:
::
OAUTH2_NAME = "Example Oauth2 Provider"
.. data:: OAUTH2_CLIENT_ID
:noindex:
::
OAUTH2_CLIENT_ID = "client-id"
.. data:: OAUTH2_REDIRECT_URI
:noindex:
::
OAUTH2_REDIRECT_URI = "https://<yourlemurserver>/api/1/auth/oauth2"
.. data:: OAUTH2_AUTH_ENDPOINT
:noindex:
::
OAUTH2_AUTH_ENDPOINT = "https://<youroauthserver>/oauth2/v1/authorize"
.. data:: OAUTH2_VERIFY_CERT
:noindex:
::
OAUTH2_VERIFY_CERT = True
.. data:: GOOGLE_CLIENT_ID
:noindex:
::
GOOGLE_CLIENT_ID = "client-id"
.. data:: GOOGLE_SECRET
:noindex:
::
GOOGLE_SECRET = "somethingsecret"
Metric Providers
~~~~~~~~~~~~~~~~
If you are not using a metric provider you do not need to configure any of these options.
.. data:: ACTIVE_PROVIDERS
:noindex:
A list of metric plugins slugs to be ativated.
::
METRIC_PROVIDERS = ['atlas-metric']
Plugin Specific Options
-----------------------
Verisign Issuer Plugin
^^^^^^^^^^^^^^^^^^^^^^
Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur, 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 Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
@ -260,87 +614,84 @@ for those plugins.
This is the root to be used for your CA chain This is the root to be used for your CA chain
Authentication Digicert Issuer Plugin
-------------- ~~~~~~~~~~~~~~~~~~~~~~
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>`_ The following configuration properties are required to use the Digicert issuer plugin.
.. data:: ACTIVE_PROVIDERS
.. data:: DIGICERT_URL
:noindex: :noindex:
:: This is the url for the Digicert API (e.g. https://www.digicert.com)
ACTIVE_PROVIDERS = ["ping", "google"]
.. data:: PING_SECRET .. data:: DIGICERT_ORDER_TYPE
:noindex: :noindex:
:: This is the type of certificate to order. (e.g. ssl_plus, ssl_ev_plus see: https://www.digicert.com/services/v2/documentation/order/overview-submit)
PING_SECRET = 'somethingsecret'
.. data:: PING_ACCESS_TOKEN_URL .. data:: DIGICERT_API_KEY
:noindex: :noindex:
:: This is the Digicert API key
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
.. data:: PING_USER_API_URL .. data:: DIGICERT_ORG_ID
:noindex: :noindex:
:: This is the Digicert organization ID tied to your API key
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
.. data:: PING_JWKS_URL .. data:: DIGICERT_ROOT
:noindex: :noindex:
:: This is the root to be used for your CA chain
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
.. data:: PING_NAME .. data:: DIGICERT_DEFAULT_VALIDITY
:noindex: :noindex:
:: This is the default validity (in years), if no end date is specified. (Default: 1)
PING_NAME = "Example Oauth2 Provider"
.. data:: PING_CLIENT_ID .. data:: DIGICERT_PRIVATE
:noindex: :noindex:
:: This is whether or not to issue a private certificate. (Default: False)
PING_CLIENT_ID = "client-id"
.. data:: GOOGLE_CLIENT_ID CFSSL Issuer Plugin
^^^^^^^^^^^^^^^^^^^
The following configuration properties are required to use the CFSSL issuer plugin.
.. data:: CFSSL_URL
:noindex: :noindex:
:: This is the URL for the CFSSL API
GOOGLE_CLIENT_ID = "client-id" .. data:: CFSSL_ROOT
.. data:: GOOGLE_SECRET
:noindex: :noindex:
:: This is the root to be used for your CA chain
GOOGLE_SECRET = "somethingsecret" .. data:: CFSSL_INTERMEDIATE
:noindex:
This is the intermediate to be used for your CA chain
AWS Plugin Configuration AWS Source/Destination Plugin
======================== ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions. In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions.
.. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so. .. 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 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.
@ -388,7 +739,7 @@ STS-AssumeRole
Next we will create the the Lemur IAM role. Next we will create the Lemur IAM role.
.. note:: .. note::
@ -445,7 +796,8 @@ IAM-ServerCertificate
Setting up STS access 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. 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 In the AWS console select the Lemur IAM role and select the Trust Relationships tab and click Edit Trust Relationship
@ -472,7 +824,7 @@ Below is an example policy:
Adding N+1 accounts 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. To add another account we go to the new account and create a new Lemur IAM role with the same policy as above.
@ -500,7 +852,7 @@ An example policy:
} }
Setting up SES Setting up SES
-------------- """"""""""""""
Lemur has built in support for sending it's certificate notifications via Amazon's simple email service (SES). To force 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 Lemur to use SES ensure you are the running as the IAM role defined above and that you have followed the steps outlined
@ -515,23 +867,6 @@ Will be the sender of all notifications, so ensure that it is verified with AWS.
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
settings. settings.
Upgrading Lemur
===============
Lemur provides an easy way to upgrade between versions. Simply download the newest
version of Lemur from pypi and then apply any schema changes with the following command.
.. code-block:: bash
$ lemur db upgrade
.. note:: Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
.. note:: By default Alembic looks for the `migrations` folder in the current working directory.
The migrations folder is located under `<LEMUR_HOME>/lemur/migrations` if you are running the lemur command from any
location besides `<LEMUR_HOME>/lemur` you will need to pass the `-d` flag to specify the absolute file path to the
`migrations` folder.
.. _CommandLineInterface: .. _CommandLineInterface:
Command Line Interface Command Line Interface
@ -601,24 +936,33 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
Traverses every certificate that Lemur is aware of and attempts to understand its validity. 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 It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates
validity its status is marked 'unknown' validity its status is marked 'unknown'.
.. data:: sync .. data:: sync
Sync attempts to discover certificates in the environment that were not created by Lemur. If you wish to only 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 Sub-commands
@ -641,37 +985,11 @@ and to get help on sub-commands
lemur certificates --help lemur certificates --help
Identity and Access Management
==============================
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
meaning.
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
as ViewPrivateKeyPermission are compositions of these three main Permissions.
Lets take a look at how these permissions are used:
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
private key information.
These permissions are applied to the user upon login and refreshed on every request.
.. seealso::
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
Upgrading Lemur Upgrading Lemur
=============== ===============
To upgrade Lemur to the newest release you will need to ensure you have the lastest code and have run any needed To upgrade Lemur to the newest release you will need to ensure you have the latest code and have run any needed
database migrations. database migrations.
To get the latest code from github run To get the latest code from github run
@ -697,3 +1015,231 @@ After you have the latest version of the Lemur code base you must run any needed
This will ensure that any needed tables or columns are created or destroyed. This will ensure that any needed tables or columns are created or destroyed.
.. note::
Internally, this uses `Alembic <http://alembic.zzzcomputing.com/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.
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec.
Verisign/Symantec
-----------------
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Issuer
:Description:
Basic support for the VICE 2.0 API
Cryptography
------------
:Authors:
Kevin Glisson <kglisson@netflix.com>,
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
:Type:
Issuer
:Description:
Toy certificate authority that creates self-signed certificate authorities.
Allows for the creation of arbitrary authorities and end-entity certificates.
This is *not* recommended for production use.
Acme
----
:Authors:
Kevin Glisson <kglisson@netflix.com>,
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
:Type:
Issuer
:Description:
Adds support for the ACME protocol (including LetsEncrypt) with domain validation being handled Route53.
Atlas
-----
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Metric
:Description:
Adds basic support for the `Atlas <https://github.com/Netflix/atlas/wiki>`_ telemetry system.
Email
-----
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Notification
:Description:
Adds support for basic email notifications via SES.
Slack
-----
:Authors:
Harm Weites <harm@weites.com>
:Type:
Notification
:Description:
Adds support for slack notifications.
AWS
----
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Source
:Description:
Uses AWS IAM as a source of certificates to manage. Supports a multi-account deployment.
AWS
----
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Destination
:Description:
Uses AWS IAM as a destination for Lemur generated certificates. Support a multi-account deployment.
Kubernetes
----------
:Authors:
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
:Type:
Destination
:Description:
Allows Lemur to upload generated certificates to the Kubernetes certificate store.
Java
----
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Export
:Description:
Generates java compatible .jks keystores and truststores from Lemur managed certificates.
Openssl
-------
:Authors:
Kevin Glisson <kglisson@netflix.com>
:Type:
Export
:Description:
Leverages Openssl to support additional export formats (pkcs12)
CFSSL
-----
:Authors:
Charles Hendrie <chad.hendrie@thomsonreuters.com>
:Type:
Issuer
:Description:
Basic support for generating certificates from the private certificate authority CFSSL
3rd Party Plugins
=================
The following plugins are available and maintained by members of the Lemur community:
Digicert
--------
:Authors:
Chris Dorros
:Type:
Issuer
:Description:
Adds support for basic Digicert
:Links:
https://github.com/opendns/lemur-digicert
InfluxDB
--------
:Authors:
Titouan Christophe
:Type:
Metric
:Description:
Sends key metrics to InfluxDB
:Links:
https://github.com/titouanc/lemur-influxdb
Hashicorp Vault
---------------
:Authors:
Ron Cohen
:Type:
Issuer
:Description:
Adds support for basic Vault PKI secret backend.
:Links:
https://github.com/RcRonco/lemur_vault
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
get it added.
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
Identity and Access Management
==============================
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
depending on an external identity provider (Google, LDAP, etc.), or are hardcoded within Lemur and associated with special
meaning.
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
as ViewPrivateKeyPermission are compositions of these three main Permissions.
Lets take a look at how these permissions are used:
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
private key information.
These permissions are applied to the user upon login and refreshed on every request.
.. seealso::
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_

View File

@ -13,12 +13,24 @@
# serve to show the default. # serve to show the default.
import sys import sys
import os import os
from unittest.mock import MagicMock
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
# Mock packages that cannot be installed on rtd
on_rtd = os.environ.get('READTHEDOCS') == 'True'
if on_rtd:
class Mock(MagicMock):
@classmethod
def __getattr__(cls, name):
return MagicMock()
MOCK_MODULES = ['ldap']
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
@ -47,7 +59,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'lemur' project = u'lemur'
copyright = u'2015, Netflix Inc.' copyright = u'2018, Netflix Inc.'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -101,9 +113,13 @@ pygments_style = 'sphinx'
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
# a list of builtin themes. on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
html_theme = 'default'
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 # 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 # further. For a list of options available for each theme, see the

View File

@ -48,7 +48,7 @@ of Lemur. You'll want to make sure you have a few things on your local system fi
* pip * pip
* virtualenv (ideally virtualenvwrapper) * virtualenv (ideally virtualenvwrapper)
* node.js (for npm and building css/javascript) * node.js (for npm and building css/javascript)
* (Optional) Potgresql +* `PostgreSQL <https://lemur.readthedocs.io/en/latest/quickstart/index.html#setup-postgres>`_
Once you've got all that, the rest is simple: Once you've got all that, the rest is simple:
@ -77,6 +77,7 @@ Create a default Lemur configuration just as if this were a production instance:
:: ::
lemur create_config
lemur init lemur init
You'll likely want to make some changes to the default configuration (we recommend developing against Postgres, for example). Once done, migrate your database using the following command: You'll likely want to make some changes to the default configuration (we recommend developing against Postgres, for example). Once done, migrate your database using the following command:
@ -86,7 +87,13 @@ You'll likely want to make some changes to the default configuration (we recomme
lemur upgrade lemur upgrade
.. note:: The ``upgrade`` shortcut is simply a shorcut to Alembic's upgrade command. .. note:: The ``upgrade`` shortcut is simply a shortcut to Alembic's upgrade command.
Running tests with Docker and docker-compose
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Alternatively you can use Docker and docker-compose for running the tests with ``docker-compose run test``.
Coding Standards Coding Standards
@ -113,6 +120,12 @@ HTML:
2 Spaces 2 Spaces
Git hooks
~~~~~~~~~
To help developers maintain the above standards, Lemur includes a configuration file for Yelp's `pre-commit <http://pre-commit.com/>`_. This is an optional dependency and is not required in order to contribute to Lemur.
Running the Test Suite Running the Test Suite
---------------------- ----------------------
@ -144,8 +157,19 @@ 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. 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 Developing with Flask
---------------------- ---------------------
Because Lemur is just Flask, you can use all of the standard Flask functionality. The only difference is you'll be accessing commands that would normally go through manage.py using the ``lemur`` CLI helper instead. Because Lemur is just Flask, you can use all of the standard Flask functionality. The only difference is you'll be accessing commands that would normally go through manage.py using the ``lemur`` CLI helper instead.
@ -164,7 +188,7 @@ Schema changes should always introduce the new schema in a commit, and then intr
Removing columns and tables requires a slightly more painful flow, and should resemble the follow multi-commit flow: Removing columns and tables requires a slightly more painful flow, and should resemble the follow multi-commit flow:
- Remove all references to the column or table (but dont remove the Model itself) - Remove all references to the column or table (but don't remove the Model itself)
- Remove the model code - Remove the model code
- Remove the table or column - Remove the table or column
@ -180,19 +204,116 @@ 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 Pull requests should be against **master** and pass all TravisCI checks
Plugins
======= Writing a Plugin
================
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 2
plugins/index 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
API. 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:
Endpoints
---------
.. automodule:: lemur.endpoints.views
:members:
:undoc-members:
:show-inheritance:
Logs
----
.. automodule:: lemur.logs.views
:members:
:undoc-members:
:show-inheritance:
Sources
-------
.. automodule:: lemur.sources.views
:members:
:undoc-members:
:show-inheritance:
Internals Internals
========= =========
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 2
internals/lemur internals/lemur

View File

@ -1,15 +1,6 @@
certificates Package certificates Package
==================== ====================
:mod:`exceptions` Module
------------------------
.. automodule:: lemur.certificates.exceptions
:noindex:
:members:
:undoc-members:
:show-inheritance:
:mod:`models` Module :mod:`models` Module
-------------------- --------------------

View File

@ -0,0 +1,20 @@
lemur_cfssl Package
===================
:mod:`lemur_cfssl` Package
--------------------------
.. automodule:: lemur.plugins.lemur_cfssl
:noindex:
:members:
:undoc-members:
:show-inheritance:
:mod:`plugin` Module
--------------------
.. automodule:: lemur.plugins.lemur_cfssl.plugin
:noindex:
:members:
:undoc-members:
:show-inheritance:

View File

@ -10,15 +10,6 @@ lemur_verisign Package
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`constants` Module
-----------------------
.. automodule:: lemur.plugins.lemur_verisign.constants
:noindex:
:members:
:undoc-members:
:show-inheritance:
:mod:`plugin` Module :mod:`plugin` Module
-------------------- --------------------

View File

@ -27,6 +27,6 @@ Subpackages
lemur.plugins.base lemur.plugins.base
lemur.plugins.bases lemur.plugins.bases
lemur.plugins.lemur_aws lemur.plugins.lemur_aws
lemur.plugins.lemur_cloudca lemur.plugins.lemur_cfssl
lemur.plugins.lemur_email lemur.plugins.lemur_email
lemur.plugins.lemur_verisign lemur.plugins.lemur_verisign

View File

@ -96,5 +96,19 @@ Subpackages
lemur.notifications lemur.notifications
lemur.plugins lemur.plugins
lemur.roles lemur.roles
lemur.status
lemur.users lemur.users
lemur.sources
lemur.logs
lemur.reporting
lemur.tests
lemur.deployment
lemur.endpoints
lemur.defaults
lemur.plugins.lemur_acme
lemur.plugins.lemur_atlas
lemur.plugins.lemur_cryptography
lemur.plugins.lemur_digicert
lemur.plugins.lemur_java
lemur.plugins.lemur_kubernetes
lemur.plugins.lemur_openssl
lemur.plugins.lemur_slack

View File

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

View File

@ -1,6 +1,3 @@
Writing a Plugin
================
Several interfaces exist for extending Lemur: Several interfaces exist for extending Lemur:
* Issuer (lemur.plugins.base.issuer) * Issuer (lemur.plugins.base.issuer)
@ -28,7 +25,7 @@ if you want to pull the version using pkg_resources (which is what we recommend)
try: try:
VERSION = __import__('pkg_resources') \ VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version .get_distribution(__name__).version
except Exception, e: except Exception as e:
VERSION = 'unknown' VERSION = 'unknown'
Inside of ``plugin.py``, you'll declare your Plugin class:: Inside of ``plugin.py``, you'll declare your Plugin class::
@ -73,10 +70,18 @@ at multiple plugins within your package::
}, },
) )
Once your plugin files are in place and the ``/www/lemur/setup.py`` file has been modified, you can load your plugin into your instance by reinstalling lemur:
::
(lemur)$cd /www/lemur
(lemur)$pip install -e .
That's it! Users will be able to install your plugin via ``pip install <package name>``. That's it! Users will be able to install your plugin via ``pip install <package name>``.
.. SeeAlso:: For more information about python packages see `Python Packaging <https://packaging.python.org/en/latest/distributing.html>`_ .. SeeAlso:: For more information about python packages see `Python Packaging <https://packaging.python.org/en/latest/distributing.html>`_
.. SeeAlso:: For an example of a plugin operation outside of Lemur's core, see `lemur-digicert <https://github.com/opendns/lemur-digicert>`_
.. _PluginInterfaces: .. _PluginInterfaces:
Plugin Interfaces Plugin Interfaces
@ -95,10 +100,16 @@ If you have a third party or internal service that creates authorities (EJBCA, e
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities. it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
The `IssuerPlugin` exposes two functions:: The `IssuerPlugin` exposes four functions functions::
def create_certificate(self, options): def create_certificate(self, csr, issuer_options):
# requests.get('a third party') # requests.get('a third party')
def revoke_certificate(self, certificate, comments):
# requests.put('a third party')
def get_ordered_certificate(self, order_id):
# requests.get('already existing certificate')
def canceled_ordered_certificate(self, pending_cert, **kwargs):
# requests.put('cancel an order that has yet to be issued')
Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request. Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request.
@ -134,15 +145,34 @@ The `IssuerPlugin` doesn't have any options like Destination, Source, and Notifi
any fields you might need to submit a request to a third party. If there are additional options you need any fields you might need to submit a request to a third party. If there are additional options you need
in your plugin feel free to open an issue, or look into adding additional options to issuers yourself. in your plugin feel free to open an issue, or look into adding additional options to issuers yourself.
Asynchronous Certificates
^^^^^^^^^^^^^^^^^^^^^^^^^
An issuer may take some time to actually issue a certificate for an order. In this case, a `PendingCertificate` is returned, which holds information to recreate a `Certificate` object at a later time. Then, `get_ordered_certificate()` should be run periodically via `python manage.py pending_certs fetch -i all` to attempt to retrieve an ordered certificate::
def get_ordered_ceriticate(self, order_id):
# order_id is the external id of the order, not the external_id of the certificate
# retrieve an order, and check if there is an issued certificate attached to it
`cancel_ordered_certificate()` should be implemented to allow an ordered certificate to be canceled before it is issued::
def cancel_ordered_certificate(self, pending_cert, **kwargs):
# pending_cert should contain the necessary information to match an order
# kwargs can be given to provide information to the issuer for canceling
Destination Destination
----------- -----------
Destination plugins allow you to propagate certificates managed by Lemur to additional third parties. This provides flexibility when Destination plugins allow you to propagate certificates managed by Lemur to additional third parties. This provides flexibility when
different orchestration systems have their own way of manage certificates or there is an existing system you wish to integrate with Lemur. different orchestration systems have their own way of manage certificates or there is an existing system you wish to integrate with Lemur.
By default destination plugins have a private key requirement. If your plugin does not require a certificates private key mark `requires_key = False`
in the plugins base class like so::
class MyDestinationPlugin(DestinationPlugin):
requires_key = False
The DestinationPlugin requires only one function to be implemented:: The DestinationPlugin requires only one function to be implemented::
def upload(self, cert, private_key, cert_chain, options, **kwargs): def upload(self, name, body, private_key, cert_chain, options, **kwargs):
# request.post('a third party') # request.post('a third party')
Additionally the DestinationPlugin allows the plugin author to add additional options Additionally the DestinationPlugin allows the plugin author to add additional options
@ -151,25 +181,25 @@ that can be used to help define sub-destinations.
For example, if we look at the aws-destination plugin we can see that it defines an `accountNumber` option:: For example, if we look at the aws-destination plugin we can see that it defines an `accountNumber` option::
options = [ options = [
{ {
'name': 'accountNumber', 'name': 'accountNumber',
'type': 'int', 'type': 'int',
'required': True, 'required': True,
'validation': '/^[0-9]{12,12}$/', 'validation': '/^[0-9]{12,12}$/',
'helpMessage': 'Must be a valid AWS account number!', 'helpMessage': 'Must be a valid AWS account number!',
} }
] ]
By defining an `accountNumber` we can make this plugin handle many N number of AWS accounts instead of just one. By defining an `accountNumber` we can make this plugin handle many N number of AWS accounts instead of just one.
The schema for defining plugin options are pretty straightforward: The schema for defining plugin options are pretty straightforward:
- **Name**: name of the variable you wish to present the user, snake case (snakeCase) is preferrred as Lemur - **Name**: name of the variable you wish to present the user, snake case (snakeCase) is preferred as Lemur
will parse these and create pretty variable titles will parse these and create pretty variable titles
- **Type** there are currently four supported variable types - **Type** there are currently four supported variable types
- **Int** creates an html integer box for the user to enter integers into - **Int** creates an html integer box for the user to enter integers into
- **Str** creates a html text input box - **Str** creates a html text input box
- **Boolean** creates a checkbox for the user to signify truithyness - **Boolean** creates a checkbox for the user to signify truthiness
- **Select** creates a select box that gives the user a list of options - **Select** creates a select box that gives the user a list of options
- When used a `available` key must be provided with a list of selectable options - When used a `available` key must be provided with a list of selectable options
- **Required** determines if this option is required, this **must be a boolean value** - **Required** determines if this option is required, this **must be a boolean value**
@ -185,7 +215,7 @@ Notification
------------ ------------
Lemur includes the ability to create Email notifications by **default**. These notifications Lemur includes the ability to create Email notifications by **default**. These notifications
currently come in the form of expiration noticies. Lemur periodically checks certifications expiration dates and currently come in the form of expiration notices. Lemur periodically checks certifications expiration dates and
determines if a given certificate is eligible for notification. There are currently only two parameters used to determines if a given certificate is eligible for notification. There are currently only two parameters used to
determine if a certificate is eligible; validity expiration (date the certificate is no longer valid) and the number determine if a certificate is eligible; validity expiration (date the certificate is no longer valid) and the number
of days the current date (UTC) is from that expiration date. of days the current date (UTC) is from that expiration date.
@ -196,10 +226,10 @@ are trying to create a new notification type (audit, failed logins, etc.) this w
You would also then need to build additional code to trigger the new notification type. You would also then need to build additional code to trigger the new notification type.
The second is `ExpirationNotificationPlugin`, this object inherits from `NotificationPlugin` object. The second is `ExpirationNotificationPlugin`, this object inherits from `NotificationPlugin` object.
You will most likely want to base your plugin on, if you want to add new channels for expiration notices (Slack, Hipcat, Jira, etc.). It adds default options that are required by You will most likely want to base your plugin on, if you want to add new channels for expiration notices (Slack, HipChat, Jira, etc.). It adds default options that are required by
by all expiration notifications (interval, unit). This interface expects for the child to define the following function:: all expiration notifications (interval, unit). This interface expects for the child to define the following function::
def send(self): def send(self, notification_type, message, targets, options, **kwargs):
# request.post("some alerting infrastructure") # request.post("some alerting infrastructure")
@ -207,27 +237,27 @@ Source
------ ------
When building Lemur we realized that although it would be nice if every certificate went through Lemur to get issued, but this is not When building Lemur we realized that although it would be nice if every certificate went through Lemur to get issued, but this is not
always be the case. Often times there are third parties that will issue certificates on your behalf and these can get deployed always be the case. Oftentimes there are third parties that will issue certificates on your behalf and these can get deployed
to infrastructure without any interaction with Lemur. In an attempt to combat this and try to track every certificate, Lemur has a notion of to infrastructure without any interaction with Lemur. In an attempt to combat this and try to track every certificate, Lemur has a notion of
certificate **Sources**. Lemur will contact the source at periodic intervals and attempt to **sync** against the source. This means downloading or discovering any certificate **Sources**. Lemur will contact the source at periodic intervals and attempt to **sync** against the source. This means downloading or discovering any
certificate Lemur does not know about and adding the certificate to it's inventory to be tracked and alerted on. certificate Lemur does not know about and adding the certificate to its inventory to be tracked and alerted on.
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates. The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
.. warning:: .. warning::
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron 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. job if you need a higher resolution sync job.
The `SourcePlugin` object requires implementation of one function:: The `SourcePlugin` object requires implementation of one function::
def get_certificates(self, **kwargs): def get_certificates(self, options, **kwargs):
# request.get("some source of certificates") # 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. Oftentimes to facilitate code re-use it makes sense put source and destination plugins into one package.
Export Export
@ -247,9 +277,8 @@ The `ExportPlugin` object requires the implementation of one function::
# return "extension", passphrase, raw # return "extension", passphrase, raw
.. Note:: .. note::
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to these calls.
these calls.
Testing Testing
@ -268,9 +297,9 @@ Augment your setup.py to ensure at least the following:
setup( setup(
# ... # ...
install_requires=[ install_requires=[
'lemur', 'lemur',
] ]
) )
@ -281,11 +310,7 @@ The ``conftest.py`` file is our main entry-point for py.test. We need to configu
.. code-block:: python .. code-block:: python
from __future__ import absolute_import from lemur.tests.conftest import * # noqa
pytest_plugins = [
'lemur.utils.pytest'
]
Test Cases Test Cases
@ -295,14 +320,18 @@ You can now inherit from Lemur's core test classes. These are Django-based and e
.. code-block:: python .. code-block:: python
# test_myextension.py import pytest
from __future__ import absolute_import from lemur.tests.vectors import INTERNAL_CERTIFICATE_A_STR, INTERNAL_PRIVATE_KEY_A_STR
from lemur.testutils import TestCase def test_export_keystore(app):
from lemur.plugins.base import plugins
p = plugins.get('java-keystore-jks')
options = [{'name': 'passphrase', 'value': 'test1234'}]
with pytest.raises(Exception):
p.export(INTERNAL_CERTIFICATE_A_STR, "", "", options)
class MyExtensionTest(TestCase): raw = p.export(INTERNAL_CERTIFICATE_A_STR, "", INTERNAL_PRIVATE_KEY_A_STR, options)
def test_simple(self): assert raw != b""
assert 1 != 2
Running Tests Running Tests
@ -314,13 +343,14 @@ Running tests follows the py.test standard. As long as your test files and metho
$ py.test -v $ py.test -v
============================== test session starts ============================== ============================== test session starts ==============================
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4/python2.7 platform darwin -- Python 2.7.10, pytest-2.8.5, py-1.4.30, pluggy-0.3.1
plugins: django cachedir: .cache
collected 1 items plugins: flask-0.10.0
collected 346 items
tests/test_myextension.py::MyExtensionTest::test_simple PASSED lemur/plugins/lemur_acme/tests/test_acme.py::test_get_certificates PASSED
=========================== 1 passed in 0.35 seconds ============================ =========================== 1 passed in 0.35 seconds ============================
.. SeeAlso:: Lemur bundles several plugins that use the same interfaces mentioned above. View the source: # TODO .. SeeAlso:: Lemur bundles several plugins that use the same interfaces mentioned above.

View File

@ -1,66 +0,0 @@
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
UI. The following is documents and provides examples on how to make requests to the Lemur API.
Authentication
--------------
.. automodule:: lemur.auth.views
:members:
:undoc-members:
:show-inheritance:
Destinations
------------
.. automodule:: lemur.destinations.views
:members:
:undoc-members:
:show-inheritance:
Notifications
-------------
.. automodule:: lemur.notifications.views
:members:
:undoc-members:
:show-inheritance:
Users
-----
.. automodule:: lemur.users.views
:members:
:undoc-members:
:show-inheritance:
Roles
-----
.. automodule:: lemur.roles.views
:members:
:undoc-members:
:show-inheritance:
Certificates
------------
.. automodule:: lemur.certificates.views
:members:
:undoc-members:
:show-inheritance:
Authorities
-----------
.. automodule:: lemur.authorities.views
:members:
:undoc-members:
:show-inheritance:
Domains
-------
.. automodule:: lemur.domains.views
:members:
:undoc-members:
:show-inheritance:

View File

@ -6,7 +6,7 @@ Common Problems
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'* 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 You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
:doc:`administration/index` for more information. :doc:`administration` for more information.
I am seeing Lemur's javascript load in my browser but not the CSS. I am seeing Lemur's javascript load in my browser but not the CSS.

View File

@ -18,7 +18,7 @@ that Lemur can then manage.
.. figure:: create_authority.png .. figure:: create_authority.png
Enter a authority name and short description about the authority. Enter an owner, Enter an authority name and short description about the authority. Enter an owner,
and certificate common name. Depending on the authority and the authority/issuer plugin and certificate common name. Depending on the authority and the authority/issuer plugin
these values may or may not be used. these values may or may not be used.
@ -56,7 +56,7 @@ Import an Existing Certificate
.. figure:: upload_certificate.png .. figure:: upload_certificate.png
Enter a owner, short description and public certificate. If there are intermediates and private keys Enter an 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 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. a certificate name but you can override that by passing a value to the `Custom Name` field.

View File

@ -27,8 +27,7 @@ Administration
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
administration/index administration
plugins/index
Developers Developers
---------- ----------
@ -38,17 +37,24 @@ Developers
developer/index developer/index
Security
REST API
-------- --------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
developer/rest security
Doing a Release
---------------
.. toctree::
:maxdepth: 1
doing-a-release
FAQ FAQ
---- ---
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1

View File

@ -1,20 +0,0 @@
Plugins
=======
There are several interfaces currently available to extend Lemur. These are a work in
progress and the API is not frozen.
Bundled Plugins
---------------
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
3rd Party Extensions
--------------------
The following extensions are available and maintained by members of the Lemur community:
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
get it added.
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.

View File

@ -37,20 +37,20 @@ Entropy
------- -------
Lemur generates private keys for the certificates it creates. This means that it is vitally important that Lemur has enough entropy to draw from. To generate private keys Lemur uses the python library `Cryptography <https://cryptography.io>`_. In turn Cryptography uses OpenSSL bindings to generate Lemur generates private keys for the certificates it creates. This means that it is vitally important that Lemur has enough entropy to draw from. To generate private keys Lemur uses the python library `Cryptography <https://cryptography.io>`_. In turn Cryptography uses OpenSSL bindings to generate
keys just like you might from the OpenSSL command line. OpenSSL draws it's initial entropy from system during startup and uses PRNGs to generate a stream of random bytes (as output by /dev/urandom) whenever it needs to do a cryptographic operation. keys just like you might from the OpenSSL command line. OpenSSL draws its initial entropy from system during startup and uses PRNGs to generate a stream of random bytes (as output by /dev/urandom) whenever it needs to do a cryptographic operation.
What does all this mean? Well in order for the keys What does all this mean? Well in order for the keys
that Lemur generates to be strong, the system needs to interact with the outside world. This is typically accomplished through the systems hardware (thermal, sound, video user-input, etc.) since the physical world is much more "random" than the computer world. that Lemur generates to be strong, the system needs to interact with the outside world. This is typically accomplished through the systems hardware (thermal, sound, video user-input, etc.) since the physical world is much more "random" than the computer world.
If you are running Lemur on its own server with its own hardware "bare metal" then the entropy of the system is typically "good enough" for generating keys. If however you are using an VM on shared hardware there is a potential that your initial seed data (data that was initially If you are running Lemur on its own server with its own hardware "bare metal" then the entropy of the system is typically "good enough" for generating keys. If however you are using a VM on shared hardware there is a potential that your initial seed data (data that was initially
fed to the PRNG) is not very good. What's more VMs have been known to be unable to inject more entropy into the system once it has been started. This is because there is typically very little interaction with the server once it has been started. fed to the PRNG) is not very good. What's more, VMs have been known to be unable to inject more entropy into the system once it has been started. This is because there is typically very little interaction with the server once it has been started.
The amount of effort you wish to expend ensuring that Lemur has good entropy to draw from is up to your specific risk tolerance and how Lemur is configured. The amount of effort you wish to expend ensuring that Lemur has good entropy to draw from is up to your specific risk tolerance and how Lemur is configured.
If you wish to generate more entropy for your system we would suggest you take a look at the following resources: If you wish to generate more entropy for your system we would suggest you take a look at the following resources:
- `WES-entropy-client <https://github.com/WhitewoodCrypto/WES-entropy-client>`_ - `WES-entropy-client <https://github.com/WhitewoodCrypto/WES-entropy-client>`_
- `haveaged <http://www.issihosts.com/haveged/>`_ - `haveged <http://www.issihosts.com/haveged/>`_
For additional information about OpenSSL entropy issues: For additional information about OpenSSL entropy issues:
@ -72,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 Nginx doesn't run any Python process, it only serves requests from outside to
the Python server. the Python server.
Therefore there are two steps: Therefore, there are two steps:
- Run the Python process. - Run the Python process.
- Run Nginx. - Run Nginx.
@ -110,7 +110,7 @@ You can make some adjustments to get a better user experience::
error_log /var/log/nginx/log/lemur.error.log; error_log /var/log/nginx/log/lemur.error.log;
location /api { 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_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off; proxy_redirect off;
proxy_buffering off; proxy_buffering off;
@ -176,7 +176,7 @@ sensitive nature of Lemur and what it controls makes this essential. This is a s
resolver <IP DNS resolver>; resolver <IP DNS resolver>;
location /api { 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_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off; proxy_redirect off;
proxy_buffering off; proxy_buffering off;
@ -217,13 +217,30 @@ An example apache config::
# HSTS (mod_headers is required) (15768000 seconds = 6 months) # HSTS (mod_headers is required) (15768000 seconds = 6 months)
Header always set Strict-Transport-Security "max-age=15768000" Header always set Strict-Transport-Security "max-age=15768000"
... ...
# Set the lemur DocumentRoot to static/dist
DocumentRoot /www/lemur/lemur/static/dist
# Uncomment to force http 1.0 connections to proxy
# SetEnv force-proxy-request-1.0 1
#Don't keep proxy connections alive
SetEnv proxy-nokeepalive 1
# Only need to do reverse proxy
ProxyRequests Off
# Proxy requests to the api to the lemur service (and sanitize redirects from it)
ProxyPass "/api" "http://127.0.0.1:8000/api"
ProxyPassReverse "/api" "http://127.0.0.1:8000/api"
</VirtualHost> </VirtualHost>
Also included in the configurations above are several best practices when it comes to deploying TLS. 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. HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment.
.. note:: .. note::
This is a rather incomplete apache config for running Lemur (needs mod_wsgi etc.,), if you have a working apache config please let us know! This is a rather incomplete apache config for running Lemur (needs mod_wsgi etc.), if you have a working apache config please let us know!
.. seealso:: .. seealso::
`Mozilla SSL Configuration Generator <https://mozilla.github.io/server-side-tls/ssl-config-generator/>`_ `Mozilla SSL Configuration Generator <https://mozilla.github.io/server-side-tls/ssl-config-generator/>`_
@ -240,10 +257,10 @@ most of the time), but here is a quick overview on how to use it.
Create a configuration file named supervisor.ini:: Create a configuration file named supervisor.ini::
[unix_http_server] [unix_http_server]
file=/tmp/supervisor.sock; file=/tmp/supervisor.sock
[supervisorctl] [supervisorctl]
serverurl=unix:///tmp/supervisor.sock; serverurl=unix:///tmp/supervisor.sock
[rpcinterface:supervisor] [rpcinterface:supervisor]
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
@ -295,3 +312,25 @@ Then you can manage the process by running::
It will start a shell from which 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 occur 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 expirations
*/15 * * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur source sync -s all
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur certificate check_revoked

View File

@ -12,11 +12,11 @@ Dependencies
Some basic prerequisites which you'll need in order to run Lemur: 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 * Python 3.5 or greater
* PostgreSQL * PostgreSQL 9.4 or greater
* Nginx * 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. .. 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 as generic as possible and are not intended to document every step of launching Lemur into a given environment.
Installing Build Dependencies Installing Build Dependencies
@ -27,10 +27,13 @@ If installing Lemur on a bare Ubuntu OS you will need to grab the following pack
.. code-block:: bash .. code-block:: bash
$ sudo apt-get update $ sudo apt-get update
$ sudo apt-get install install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql $ sudo apt-get install nodejs nodejs-legacy python-pip python-dev python3-dev libpq-dev build-essential libssl-dev libffi-dev libsasl2-dev libldap2-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). .. 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).
.. note:: Installing node from a package manager may creat the nodejs bin at /usr/bin/nodejs instead of /usr/bin/node If that is the case run the following
$ sudo ln -s /user/bin/nodejs /usr/bin/node
Now, install Python ``virtualenv`` package: Now, install Python ``virtualenv`` package:
.. code-block:: bash .. code-block:: bash
@ -52,6 +55,10 @@ Clone Lemur inside the just created directory and give yourself write permission
.. code-block:: bash .. code-block:: bash
$ sudo useradd lemur
$ sudo passwd lemur
$ sudo mkdir /home/lemur
$ sudo chown lemur:lemur /home/lemur
$ sudo git clone https://github.com/Netflix/lemur $ sudo git clone https://github.com/Netflix/lemur
$ sudo chown -R lemur lemur/ $ sudo chown -R lemur lemur/
@ -59,7 +66,8 @@ Create the virtual environment, activate it and enter the Lemur's directory:
.. code-block:: bash .. code-block:: bash
$ virtualenv lemur $ su lemur
$ virtualenv -p python3 lemur
$ source /www/lemur/bin/activate $ source /www/lemur/bin/activate
$ cd lemur $ cd lemur
@ -79,11 +87,23 @@ And then run:
.. code-block:: bash .. code-block:: bash
$ make develop $ make release
.. 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.
You may also run with the urlContextPath variable set. If this is set it will add the desired context path for subsequent calls back to lemur. This will only edit the front end code for calls back to the server, you will have to make sure the server knows about these routes.
::
Example:
urlContextPath=lemur
/api/1/auth/providers -> /lemur/api/1/auth/providers
.. code-block:: bash
$ make release urlContextPath={desired context path}
Creating a configuration Creating a configuration
------------------------ ------------------------
@ -105,9 +125,24 @@ 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 stored 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: .. code-block:: bash
$ vi ~/.lemur/lemur.conf.py
.. note:: If you are unfamiliar with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
``postgresql://userame:password@<database-fqdn>:<database-port>/<database-name>`` ``postgresql://userame:password@<database-fqdn>:<database-port>/<database-name>``
Before Lemur will run you need to fill in a few required variables in the configuration file:
.. code-block:: bash
LEMUR_SECURITY_TEAM_EMAIL
#/the e-mail address needs to be enclosed in quotes
LEMUR_DEFAULT_COUNTRY
LEMUR_DEFAULT_STATE
LEMUR_DEFAULT_LOCATION
LEMUR_DEFAULT_ORGANIZATION
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT
Setup Postgres Setup Postgres
-------------- --------------
@ -118,10 +153,9 @@ First, set a password for the postgres user. For this guide, we will use ``lemu
.. code-block:: bash .. code-block:: bash
$ sudo -u postgres psql postgres $ sudo -u postgres -i
# \password postgres $ psql
Enter new password: lemur postgres=# CREATE USER lemur WITH PASSWORD 'lemur';
Enter it again: lemur
Once successful, type CTRL-D to exit the Postgres shell. Once successful, type CTRL-D to exit the Postgres shell.
@ -133,17 +167,11 @@ Next, we will create our new database:
.. _InitializingLemur: .. _InitializingLemur:
Set a password for lemur user inside Postgres: .. 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 its own user.
.. code-block:: bash
$ sudo -u postgres psql postgres
\password lemur
Enter new password: lemur
Enter it again: lemur
Again, enter CTRL-D to exit the Postgres shell.
.. note::
Postgres 9.4 or greater is required as Lemur relies advanced data columns (e.g. JSON Column type)
Initializing Lemur Initializing Lemur
------------------ ------------------
@ -158,8 +186,10 @@ Additional notifications can be created through the UI or API. See :ref:`Creati
.. code-block:: bash .. code-block:: bash
$ cd /www/lemur/lemur
$ lemur init $ 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.
@ -177,7 +207,7 @@ You'll use the builtin ``HttpProxyModule`` within Nginx to handle proxying. Edi
:: ::
location /api { 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_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off; proxy_redirect off;
proxy_buffering off; proxy_buffering off;
@ -219,7 +249,7 @@ you can pass that via the ``--config`` option.
# the correct host and port! # the correct host and port!
lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:8000 lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:8000
You should now be able to test the web service by visiting ``http://localhost:5000/``. You should now be able to test the web service by visiting ``http://localhost:8000/``.
Running Lemur as a Service Running Lemur as a Service
@ -241,8 +271,8 @@ Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` e
autostart=true autostart=true
autorestart=true autorestart=true
redirect_stderr=true redirect_stderr=true
stdout_logfile syslog stdout_logfile=syslog
stderr_logfile syslog stderr_logfile=syslog
See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor. See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor.
@ -250,13 +280,14 @@ See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervis
Syncing Syncing
------- -------
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. As always, things can change outside of Lemur, but we do our best to reconcile those changes, for example, using Cron: Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. Things change outside of Lemur we do our best to reconcile those changes. The recommended method is to use CRON:
.. code-block:: bash .. code-block:: bash
$ crontab -e $ crontab -e
* 3 * * * lemur sync --all */15 * * * * lemur sync -s all
* 3 * * * lemur check_revoked 0 22 * * * lemur check_revoked
0 22 * * * lemur notify
Additional Utilities Additional Utilities

View File

@ -1,29 +0,0 @@
Jinja2>=2.3
Pygments>=1.2
Sphinx>=1.3
docutils>=0.7
markupsafe
sphinxcontrib-httpdomain
Flask==0.10.1
Flask-RESTful==0.3.3
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
Flask-Migrate==1.6.0
Flask-Bcrypt==0.7.1
Flask-Principal==0.4.0
Flask-Mail==0.9.1
SQLAlchemy-Utils==0.31.3
BeautifulSoup4
requests==2.8.1
psycopg2==2.6.1
arrow==0.7.0
boto==2.38.0 # we might make this optional
six==1.10.0
gunicorn==19.4.1
pycrypto==2.6.1
cryptography==1.1.1
pyopenssl==0.15.1
pyjwt==1.4.0
xmltodict==0.9.2
lockfile==0.12.2
future==0.15.2

View File

@ -60,7 +60,7 @@ and public disclosure may be shortened considerably.
The list of people and organizations who receives advanced notification of The list of people and organizations who receives advanced notification of
security issues is not, and will not, be made public. This list generally security issues is not, and will not, be made public. This list generally
consists of high profile downstream distributors and is entirely at the consists of high-profile downstream distributors and is entirely at the
discretion of the ``lemur`` team. discretion of the ``lemur`` team.
.. _`master`: https://github.com/Netflix/lemur .. _`master`: https://github.com/Netflix/lemur

View File

@ -1,13 +1,12 @@
'use strict';
var gulp = require('gulp'), var gulp = require('gulp'),
minifycss = require('gulp-minify-css'), minifycss = require('gulp-minify-css'),
concat = require('gulp-concat'), concat = require('gulp-concat'),
less = require('gulp-less'), less = require('gulp-less'),
gulpif = require('gulp-if'), gulpif = require('gulp-if'),
order = require('gulp-order'),
gutil = require('gulp-util'), gutil = require('gulp-util'),
rename = require('gulp-rename'),
foreach = require('gulp-foreach'), foreach = require('gulp-foreach'),
debug = require('gulp-debug'),
path =require('path'), path =require('path'),
merge = require('merge-stream'), merge = require('merge-stream'),
del = require('del'), del = require('del'),
@ -27,7 +26,8 @@ var gulp = require('gulp'),
minifyHtml = require('gulp-minify-html'), minifyHtml = require('gulp-minify-html'),
bowerFiles = require('main-bower-files'), bowerFiles = require('main-bower-files'),
karma = require('karma'), karma = require('karma'),
replace = require('gulp-replace'); replace = require('gulp-replace'),
argv = require('yargs').argv;
gulp.task('default', ['clean'], function () { gulp.task('default', ['clean'], function () {
gulp.start('fonts', 'styles'); gulp.start('fonts', 'styles');
@ -79,8 +79,9 @@ gulp.task('dev:styles', function () {
'bower_components/angular-loading-bar/src/loading-bar.css', 'bower_components/angular-loading-bar/src/loading-bar.css',
'bower_components/angular-ui-switch/angular-ui-switch.css', 'bower_components/angular-ui-switch/angular-ui-switch.css',
'bower_components/angular-wizard/dist/angular-wizard.css', 'bower_components/angular-wizard/dist/angular-wizard.css',
'bower_components/ng-table/ng-table.css', 'bower_components/ng-table/dist/ng-table.css',
'bower_components/angularjs-toaster/toaster.css', 'bower_components/angularjs-toaster/toaster.css',
'bower_components/angular-ui-select/dist/select.css',
'lemur/static/app/styles/lemur.css' 'lemur/static/app/styles/lemur.css'
]; ];
@ -88,9 +89,9 @@ gulp.task('dev:styles', function () {
.pipe(gulpif(isBootswatchFile, foreach(function (stream, file) { .pipe(gulpif(isBootswatchFile, foreach(function (stream, file) {
var themeName = path.basename(path.dirname(file.path)), var themeName = path.basename(path.dirname(file.path)),
content = replaceAll(baseContent, '$theme$', themeName), content = replaceAll(baseContent, '$theme$', themeName),
file = string_src('bootstrap-' + themeName + '.less', content); file2 = string_src('bootstrap-' + themeName + '.less', content);
return file; return file2;
}))) })))
.pipe(less()) .pipe(less())
.pipe(gulpif(isBootstrapFile, foreach(function (stream, file) { .pipe(gulpif(isBootstrapFile, foreach(function (stream, file) {
@ -100,7 +101,7 @@ gulp.task('dev:styles', function () {
// http://stackoverflow.com/questions/21719833/gulp-how-to-add-src-files-in-the-middle-of-a-pipe // http://stackoverflow.com/questions/21719833/gulp-how-to-add-src-files-in-the-middle-of-a-pipe
// https://github.com/gulpjs/gulp/blob/master/docs/recipes/using-multiple-sources-in-one-task.md // https://github.com/gulpjs/gulp/blob/master/docs/recipes/using-multiple-sources-in-one-task.md
return merge(stream, gulp.src(['.tmp/styles/font-awesome.css', '.tmp/styles/lemur.css'])) return merge(stream, gulp.src(['.tmp/styles/font-awesome.css', '.tmp/styles/lemur.css']))
.pipe(concat('style-' + themeName + ".css")); .pipe(concat('style-' + themeName + '.css'));
}))) })))
.pipe(plumber()) .pipe(plumber())
.pipe(concat('styles.css')) .pipe(concat('styles.css'))
@ -112,7 +113,7 @@ gulp.task('dev:styles', function () {
// http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript // http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript
function escapeRegExp(string) { function escapeRegExp(string) {
return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
} }
function replaceAll(string, find, replace) { function replaceAll(string, find, replace) {
@ -122,7 +123,7 @@ function replaceAll(string, find, replace) {
function string_src(filename, string) { function string_src(filename, string) {
var src = require('stream').Readable({ objectMode: true }); var src = require('stream').Readable({ objectMode: true });
src._read = function () { src._read = function () {
this.push(new gutil.File({ cwd: "", base: "", path: filename, contents: new Buffer(string) })); this.push(new gutil.File({ cwd: '', base: '', path: filename, contents: new Buffer(string) }));
this.push(null); this.push(null);
}; };
return src; return src;
@ -143,26 +144,18 @@ gulp.task('build:extras', function () {
function injectHtml(isDev) { function injectHtml(isDev) {
return gulp.src('lemur/static/app/index.html') return gulp.src('lemur/static/app/index.html')
.pipe( .pipe(
inject(gulp.src(bowerFiles({ base: 'app' }), { inject(gulp.src(bowerFiles({ base: 'app' })), {
read: false
}), {
starttag: '<!-- inject:bower:{{ext}} -->', starttag: '<!-- inject:bower:{{ext}} -->',
addRootSlash: false, addRootSlash: false,
ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null
}) })
) )
.pipe(inject(gulp.src(['lemur/static/app/angular/**/*.js'], { .pipe(inject(gulp.src(['lemur/static/app/angular/**/*.js']), {
read: false
}), {
read: false,
starttag: '<!-- inject:{{ext}} -->', starttag: '<!-- inject:{{ext}} -->',
addRootSlash: false, addRootSlash: false,
ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null
})) }))
.pipe(inject(gulp.src(['.tmp/styles/**/*.css'], { .pipe(inject(gulp.src(['.tmp/styles/**/*.css']), {
read: false
}), {
read: false,
starttag: '<!-- inject:{{ext}} -->', starttag: '<!-- inject:{{ext}} -->',
addRootSlash: false, addRootSlash: false,
ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null ignorePath: isDev ? ['lemur/static/app/', '.tmp/'] : null
@ -170,13 +163,11 @@ function injectHtml(isDev) {
.pipe( .pipe(
gulpif(!isDev, gulpif(!isDev,
inject(gulp.src('lemur/static/dist/ngviews/ngviews.min.js'), { inject(gulp.src('lemur/static/dist/ngviews/ngviews.min.js'), {
read: false,
starttag: '<!-- inject:ngviews -->', starttag: '<!-- inject:ngviews -->',
addRootSlash: false addRootSlash: false
}) })
) )
) ).pipe(gulp.dest('.tmp/'));
.pipe(gulp.dest('.tmp/'));
} }
gulp.task('dev:inject', ['dev:styles', 'dev:scripts'], function () { gulp.task('dev:inject', ['dev:styles', 'dev:scripts'], function () {
@ -199,23 +190,17 @@ gulp.task('build:ngviews', function () {
}); });
gulp.task('build:html', ['dev:styles', 'dev:scripts', 'build:ngviews', 'build:inject'], function () { gulp.task('build:html', ['dev:styles', 'dev:scripts', 'build:ngviews', 'build:inject'], function () {
var jsFilter = filter('**/*.js'); var jsFilter = filter(['**/*.js'], {'restore': true});
var cssFilter = filter('**/*.css'); var cssFilter = filter(['**/*.css'], {'restore': true});
var assets = useref.assets();
return gulp.src('.tmp/index.html') return gulp.src('.tmp/index.html')
.pipe(assets)
.pipe(rev())
.pipe(jsFilter) .pipe(jsFilter)
.pipe(ngAnnotate()) .pipe(ngAnnotate())
.pipe(jsFilter.restore()) .pipe(jsFilter.restore)
.pipe(cssFilter) .pipe(cssFilter)
.pipe(csso()) .pipe(csso())
.pipe(cssFilter.restore()) .pipe(cssFilter.restore)
.pipe(assets.restore())
.pipe(useref()) .pipe(useref())
.pipe(revReplace())
.pipe(gulp.dest('lemur/static/dist')) .pipe(gulp.dest('lemur/static/dist'))
.pipe(size()); .pipe(size());
}); });
@ -241,10 +226,40 @@ gulp.task('package:strip', function () {
.pipe(replace('http:\/\/localhost:3000', '')) .pipe(replace('http:\/\/localhost:3000', ''))
.pipe(replace('http:\/\/localhost:8000', '')) .pipe(replace('http:\/\/localhost:8000', ''))
.pipe(useref()) .pipe(useref())
.pipe(revReplace())
.pipe(gulp.dest('lemur/static/dist/scripts')) .pipe(gulp.dest('lemur/static/dist/scripts'))
.pipe(size()); .pipe(size());
}); });
gulp.task('addUrlContextPath',['addUrlContextPath:revreplace'], function(){
var urlContextPathExists = argv.urlContextPath ? true : false;
['lemur/static/dist/scripts/main*.js',
'lemur/static/dist/angular/**/*.html']
.forEach(function(file){
return gulp.src(file)
.pipe(gulpif(urlContextPathExists, replace('api/', argv.urlContextPath + '/api/')))
.pipe(gulpif(urlContextPathExists, replace('angular/', argv.urlContextPath + '/angular/')))
.pipe(gulp.dest(function(file){
return file.base;
}))
})
});
gulp.task('addUrlContextPath:revision', function(){
return gulp.src(['lemur/static/dist/**/*.css','lemur/static/dist/**/*.js'])
.pipe(rev())
.pipe(gulp.dest('lemur/static/dist'))
.pipe(rev.manifest())
.pipe(gulp.dest('lemur/static/dist'))
})
gulp.task('addUrlContextPath:revreplace', ['addUrlContextPath:revision'], function(){
var manifest = gulp.src("lemur/static/dist/rev-manifest.json");
var urlContextPathExists = argv.urlContextPath ? true : false;
return gulp.src( "lemur/static/dist/index.html")
.pipe(gulpif(urlContextPathExists, revReplace({prefix: argv.urlContextPath + '/', manifest: manifest}, revReplace({manifest: manifest}))))
.pipe(gulp.dest('lemur/static/dist'));
})
gulp.task('build', ['build:ngviews', 'build:inject', 'build:images', 'build:fonts', 'build:html', 'build:extras']); gulp.task('build', ['build:ngviews', 'build:inject', 'build:images', 'build:fonts', 'build:html', 'build:extras']);
gulp.task('package', ['package:strip']); gulp.task('package', ['addUrlContextPath', 'package:strip']);

View File

@ -1,46 +0,0 @@
#!/usr/bin/env python
import glob
import os
import sys
os.environ['PYFLAKES_NODOCTEST'] = '1'
# pep8.py uses sys.argv to find setup.cfg
sys.argv = [os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)]
# git usurbs your bin path for hooks and will always run system python
if 'VIRTUAL_ENV' in os.environ:
site_packages = glob.glob(
'%s/lib/*/site-packages' % os.environ['VIRTUAL_ENV'])[0]
sys.path.insert(0, site_packages)
def py_lint(files_modified):
from flake8.main import DEFAULT_CONFIG
from flake8.engine import get_style_guide
# remove non-py files and files which no longer exist
files_modified = filter(lambda x: x.endswith('.py'), files_modified)
flake8_style = get_style_guide(parse_argv=True, config_file=DEFAULT_CONFIG)
report = flake8_style.check_files(files_modified)
return report.total_errors != 0
def main():
from flake8.hooks import run
gitcmd = "git diff-index --cached --name-only HEAD"
_, files_modified, _ = run(gitcmd)
files_modified = filter(lambda x: os.path.exists(x), files_modified)
if py_lint(files_modified):
return 1
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -9,10 +9,10 @@ __title__ = "lemur"
__summary__ = ("Certificate management and orchestration service") __summary__ = ("Certificate management and orchestration service")
__uri__ = "https://github.com/Netflix/lemur" __uri__ = "https://github.com/Netflix/lemur"
__version__ = "0.2.1" __version__ = "0.7.0"
__author__ = "The Lemur developers" __author__ = "The Lemur developers"
__email__ = "security@netflix.com" __email__ = "security@netflix.com"
__license__ = "Apache License, Version 2.0" __license__ = "Apache License, Version 2.0"
__copyright__ = "Copyright 2015 {0}".format(__author__) __copyright__ = "Copyright 2018 {0}".format(__author__)

View File

@ -1,16 +1,18 @@
""" """
.. module: lemur .. module: lemur
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from __future__ import absolute_import, division, print_function import time
from flask import g, request
from lemur import factory from lemur import factory
from lemur.extensions import metrics
from lemur.users.views import mod as users_bp from lemur.users.views import mod as users_bp
from lemur.roles.views import mod as roles_bp from lemur.roles.views import mod as roles_bp
@ -23,6 +25,11 @@ from lemur.defaults.views import mod as defaults_bp
from lemur.plugins.views import mod as plugins_bp from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp from lemur.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp from lemur.sources.views import mod as sources_bp
from lemur.endpoints.views import mod as endpoints_bp
from lemur.logs.views import mod as logs_bp
from lemur.api_keys.views import mod as api_key_bp
from lemur.pending_certificates.views import mod as pending_certificates_bp
from lemur.dns_providers.views import mod as dns_providers_bp
from lemur.__about__ import ( from lemur.__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__, __author__, __copyright__, __email__, __license__, __summary__, __title__,
@ -46,7 +53,12 @@ LEMUR_BLUEPRINTS = (
defaults_bp, defaults_bp,
plugins_bp, plugins_bp,
notifications_bp, notifications_bp,
sources_bp sources_bp,
endpoints_bp,
logs_bp,
api_key_bp,
pending_certificates_bp,
dns_providers_bp,
) )
@ -62,16 +74,40 @@ def configure_hook(app):
:param app: :param app:
:return: :return:
""" """
from flask.ext.principal import PermissionDenied from flask import jsonify
from lemur.decorators import crossdomain from werkzeug.exceptions import HTTPException
if app.config.get('CORS'):
@app.after_request @app.errorhandler(Exception)
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE']) def handle_error(e):
def after(response): code = 500
if isinstance(e, HTTPException):
code = e.code
app.logger.exception(e)
return jsonify(error=str(e)), code
@app.before_request
def before_request():
g.request_start_time = time.time()
@app.after_request
def after_request(response):
# Return early if we don't have the start time
if not hasattr(g, 'request_start_time'):
return response return response
@app.errorhandler(PermissionDenied) # Get elapsed time in milliseconds
def handle_invalid_usage(error): elapsed = time.time() - g.request_start_time
response = {'message': 'You are not allow to access this resource'} elapsed = int(round(1000 * elapsed))
response.status_code = 403
# Collect request/response tags
tags = {
'endpoint': request.endpoint,
'request_method': request.method.lower(),
'status_code': response.status_code
}
# Record our response time metric
metrics.send('response_time', 'TIMER', elapsed, metric_tags=tags)
metrics.send('status_code_{}'.format(response.status_code), 'counter', 1)
return response return response

View File

@ -1,64 +0,0 @@
"""
.. module: lemur.analyze.service
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
# def analyze(endpoints, truststores):
# results = {"headings": ["Endpoint"],
# "results": [],
# "time": datetime.now().strftime("#Y%m%d %H:%M:%S")}
#
# for store in truststores:
# results['headings'].append(os.path.basename(store))
#
# for endpoint in endpoints:
# result_row = [endpoint]
# for store in truststores:
# result = {'details': []}
#
# tests = []
# for region, ip in REGIONS.items():
# try:
# domain = dns.name.from_text(endpoint)
# if not domain.is_absolute():
# domain = domain.concatenate(dns.name.root)
#
# my_resolver = dns.resolver.Resolver()
# my_resolver.nameservers = [ip]
# answer = my_resolver.query(domain)
#
# #force the testing of regional enpoints by changing the dns server
# response = requests.get('https://' + str(answer[0]), verify=store)
# tests.append('pass')
# result['details'].append("{}: SSL testing completed without errors".format(region))
#
# except SSLError as e:
# log.debug(e)
# if 'hostname' in str(e):
# tests.append('pass')
# result['details'].append(
# "{}: This test passed ssl negotiation but failed hostname verification because \
# the hostname is not included in the certificate".format(region))
# elif 'certificate verify failed' in str(e):
# tests.append('fail')
# result['details'].append("{}: This test failed to verify the SSL certificate".format(region))
# else:
# tests.append('fail')
# result['details'].append("{}: {}".format(region, str(e)))
#
# except Exception as e:
# log.debug(e)
# tests.append('fail')
# result['details'].append("{}: {}".format(region, str(e)))
#
# #any failing tests fails the whole endpoint
# if 'fail' in tests:
# result['test'] = 'fail'
# else:
# result['test'] = 'pass'
#
# result_row.append(result)
# results['results'].append(result_row)
# return results
#

41
lemur/api_keys/cli.py Normal file
View File

@ -0,0 +1,41 @@
"""
.. module: lemur.api_keys.cli
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""
from flask_script import Manager
from lemur.api_keys import service as api_key_service
from lemur.auth.service import create_token
from datetime import datetime
manager = Manager(usage="Handles all api key related tasks.")
@manager.option('-u', '--user-id', dest='uid', help='The User ID this access key belongs too.')
@manager.option('-n', '--name', dest='name', help='The name of this API Key.')
@manager.option('-t', '--ttl', dest='ttl', help='The TTL of this API Key. -1 for forever.')
def create(uid, name, ttl):
"""
Create a new api key for a user.
:return:
"""
print("[+] Creating a new api key.")
key = api_key_service.create(user_id=uid, name=name,
ttl=ttl, issued_at=int(datetime.utcnow().timestamp()), revoked=False)
print("[+] Successfully created a new api key. Generating a JWT...")
jwt = create_token(uid, key.id, key.ttl)
print("[+] Your JWT is: {jwt}".format(jwt=jwt))
@manager.option('-a', '--api-key-id', dest='aid', help='The API Key ID to revoke.')
def revoke(aid):
"""
Revokes an api key for a user.
:return:
"""
print("[-] Revoking the API Key api key.")
api_key_service.revoke(aid=aid)
print("[+] Successfully revoked the api key")

25
lemur/api_keys/models.py Normal file
View File

@ -0,0 +1,25 @@
"""
.. module: lemur.api_keys.models
:platform: Unix
:synopsis: This module contains all of the models need to create an api key within Lemur.
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""
from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, Integer, String
from lemur.database import db
class ApiKey(db.Model):
__tablename__ = 'api_keys'
id = Column(Integer, primary_key=True)
name = Column(String)
user_id = Column(Integer, ForeignKey('users.id'))
ttl = Column(BigInteger)
issued_at = Column(BigInteger)
revoked = Column(Boolean)
def __repr__(self):
return "ApiKey(name={name}, user_id={user_id}, ttl={ttl}, issued_at={iat}, revoked={revoked})".format(
user_id=self.user_id, name=self.name, ttl=self.ttl, iat=self.issued_at, revoked=self.revoked)

57
lemur/api_keys/schemas.py Normal file
View File

@ -0,0 +1,57 @@
"""
.. module: lemur.api_keys.schemas
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""
from flask import g
from marshmallow import fields
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.users.schemas import UserNestedOutputSchema, UserInputSchema
def current_user_id():
return {'id': g.current_user.id, 'email': g.current_user.email, 'username': g.current_user.username}
class ApiKeyInputSchema(LemurInputSchema):
name = fields.String(required=False)
user = fields.Nested(UserInputSchema, missing=current_user_id, default=current_user_id)
ttl = fields.Integer()
class ApiKeyRevokeSchema(LemurInputSchema):
id = fields.Integer(required=True)
name = fields.String()
user = fields.Nested(UserInputSchema, required=True)
revoked = fields.Boolean()
ttl = fields.Integer()
issued_at = fields.Integer(required=False)
class UserApiKeyInputSchema(LemurInputSchema):
name = fields.String(required=False)
ttl = fields.Integer()
class ApiKeyOutputSchema(LemurOutputSchema):
jwt = fields.String()
class ApiKeyDescribedOutputSchema(LemurOutputSchema):
id = fields.Integer()
name = fields.String()
user = fields.Nested(UserNestedOutputSchema)
ttl = fields.Integer()
issued_at = fields.Integer()
revoked = fields.Boolean()
api_key_input_schema = ApiKeyInputSchema()
api_key_revoke_schema = ApiKeyRevokeSchema()
api_key_output_schema = ApiKeyOutputSchema()
api_keys_output_schema = ApiKeyDescribedOutputSchema(many=True)
api_key_described_output_schema = ApiKeyDescribedOutputSchema()
user_api_key_input_schema = UserApiKeyInputSchema()

97
lemur/api_keys/service.py Normal file
View File

@ -0,0 +1,97 @@
"""
.. module: lemur.api_keys.service
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""
from lemur import database
from lemur.api_keys.models import ApiKey
def get(aid):
"""
Retrieves an api key by its ID.
:param aid: The access key id to get.
:return:
"""
return database.get(ApiKey, aid)
def delete(access_key):
"""
Delete an access key. This is one way to remove a key, though you probably should just set revoked.
:param access_key:
:return:
"""
database.delete(access_key)
def revoke(aid):
"""
Revokes an api key.
:param aid:
:return:
"""
api_key = get(aid)
setattr(api_key, 'revoked', False)
return database.update(api_key)
def get_all_api_keys():
"""
Retrieves all Api Keys.
:return:
"""
return ApiKey.query.all()
def create(**kwargs):
"""
Creates a new API Key.
:param kwargs:
:return:
"""
api_key = ApiKey(**kwargs)
database.create(api_key)
return api_key
def update(api_key, **kwargs):
"""
Updates an api key.
:param api_key:
:param kwargs:
:return:
"""
for key, value in kwargs.items():
setattr(api_key, key, value)
return database.update(api_key)
def render(args):
"""
Helper to parse REST Api requests
:param args:
:return:
"""
query = database.session_query(ApiKey)
user_id = args.pop('user_id', None)
aid = args.pop('id', None)
has_permission = args.pop('has_permission', False)
requesting_user_id = args.pop('requesting_user_id')
if user_id:
query = query.filter(ApiKey.user_id == user_id)
if aid:
query = query.filter(ApiKey.id == aid)
if not has_permission:
query = query.filter(ApiKey.user_id == requesting_user_id)
return database.sort_and_page(query, ApiKey, args)

579
lemur/api_keys/views.py Normal file
View File

@ -0,0 +1,579 @@
"""
.. module: lemur.api_keys.views
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""
from datetime import datetime
from flask import Blueprint, g
from flask_restful import reqparse, Api
from lemur.api_keys import service
from lemur.auth.service import AuthenticatedResource, create_token
from lemur.auth.permissions import ApiKeyCreatorPermission
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
from lemur.api_keys.schemas import api_key_input_schema, api_key_revoke_schema, api_key_output_schema, \
api_keys_output_schema, api_key_described_output_schema, user_api_key_input_schema
mod = Blueprint('api_keys', __name__)
api = Api(mod)
class ApiKeyList(AuthenticatedResource):
""" Defines the 'api_keys' endpoint """
def __init__(self):
super(ApiKeyList, self).__init__()
@validate_schema(None, api_keys_output_schema)
def get(self):
"""
.. http:get:: /keys
The current list of api keys, that you can see.
**Example request**:
.. sourcecode:: http
GET /keys 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": [
{
"id": 1,
"name": "custom name",
"user_id": 1,
"ttl": -1,
"issued_at": 12,
"revoked": false
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc or desc
:query page: int default is 1
:query count: count number. default is 10
:query user_id: a user to filter by.
:query id: an access key to filter by.
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
args = parser.parse_args()
args['has_permission'] = ApiKeyCreatorPermission().can()
args['requesting_user_id'] = g.current_user.id
return service.render(args)
@validate_schema(api_key_input_schema, api_key_output_schema)
def post(self, data=None):
"""
.. http:post:: /keys
Creates an API Key.
**Example request**:
.. sourcecode:: http
POST /keys HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"name": "my custom name",
"user_id": 1,
"ttl": -1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"jwt": ""
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
if not ApiKeyCreatorPermission().can():
if data['user']['id'] != g.current_user.id:
return dict(message="You are not authorized to create tokens for: {0}".format(data['user']['username'])), 403
access_token = service.create(name=data['name'], user_id=data['user']['id'], ttl=data['ttl'],
revoked=False, issued_at=int(datetime.utcnow().timestamp()))
return dict(jwt=create_token(access_token.user_id, access_token.id, access_token.ttl))
class ApiKeyUserList(AuthenticatedResource):
""" Defines the 'keys' endpoint on the 'users' endpoint. """
def __init__(self):
super(ApiKeyUserList, self).__init__()
@validate_schema(None, api_keys_output_schema)
def get(self, user_id):
"""
.. http:get:: /users/:user_id/keys
The current list of api keys for a user, that you can see.
**Example request**:
.. sourcecode:: http
GET /users/1/keys 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": [
{
"id": 1,
"name": "custom name",
"user_id": 1,
"ttl": -1,
"issued_at": 12,
"revoked": false
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc or desc
:query page: int default is 1
:query count: count number. default is 10
:query id: an access key to filter by.
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
args = parser.parse_args()
args['has_permission'] = ApiKeyCreatorPermission().can()
args['requesting_user_id'] = g.current_user.id
args['user_id'] = user_id
return service.render(args)
@validate_schema(user_api_key_input_schema, api_key_output_schema)
def post(self, user_id, data=None):
"""
.. http:post:: /users/:user_id/keys
Creates an API Key for a user.
**Example request**:
.. sourcecode:: http
POST /users/1/keys HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"name": "my custom name"
"ttl": -1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"jwt": ""
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
if not ApiKeyCreatorPermission().can():
if user_id != g.current_user.id:
return dict(message="You are not authorized to create tokens for: {0}".format(user_id)), 403
access_token = service.create(name=data['name'], user_id=user_id, ttl=data['ttl'],
revoked=False, issued_at=int(datetime.utcnow().timestamp()))
return dict(jwt=create_token(access_token.user_id, access_token.id, access_token.ttl))
class ApiKeys(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(ApiKeys, self).__init__()
@validate_schema(None, api_key_output_schema)
def get(self, aid):
"""
.. http:get:: /keys/1
Fetch one api key
**Example request**:
.. sourcecode:: http
GET /keys/1 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
{
"jwt": ""
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
@validate_schema(api_key_revoke_schema, api_key_output_schema)
def put(self, aid, data=None):
"""
.. http:put:: /keys/1
update one api key
**Example request**:
.. sourcecode:: http
PUT /keys/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"name": "new_name",
"revoked": false,
"ttl": -1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"jwt": ""
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to update this token!"), 403
service.update(access_key, name=data['name'], revoked=data['revoked'], ttl=data['ttl'])
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
def delete(self, aid):
"""
.. http:delete:: /keys/1
deletes one api key
**Example request**:
.. sourcecode:: http
DELETE /keys/1 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
{
"result": true
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to delete this token!"), 403
service.delete(access_key)
return {'result': True}
class UserApiKeys(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(UserApiKeys, self).__init__()
@validate_schema(None, api_key_output_schema)
def get(self, uid, aid):
"""
.. http:get:: /users/1/keys/1
Fetch one api key
**Example request**:
.. sourcecode:: http
GET /users/1/api_keys/1 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
{
"jwt": ""
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
if uid != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != uid:
return dict(message="You are not authorized to view this token!"), 403
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
@validate_schema(api_key_revoke_schema, api_key_output_schema)
def put(self, uid, aid, data=None):
"""
.. http:put:: /users/1/keys/1
update one api key
**Example request**:
.. sourcecode:: http
PUT /users/1/keys/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"name": "new_name",
"revoked": false,
"ttl": -1
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"jwt": ""
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
if uid != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != uid:
return dict(message="You are not authorized to update this token!"), 403
service.update(access_key, name=data['name'], revoked=data['revoked'], ttl=data['ttl'])
return dict(jwt=create_token(access_key.user_id, access_key.id, access_key.ttl))
def delete(self, uid, aid):
"""
.. http:delete:: /users/1/keys/1
deletes one api key
**Example request**:
.. sourcecode:: http
DELETE /users/1/keys/1 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
{
"result": true
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
if uid != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != uid:
return dict(message="You are not authorized to delete this token!"), 403
service.delete(access_key)
return {'result': True}
class ApiKeysDescribed(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(ApiKeysDescribed, self).__init__()
@validate_schema(None, api_key_described_output_schema)
def get(self, aid):
"""
.. http:get:: /keys/1/described
Fetch one api key
**Example request**:
.. sourcecode:: http
GET /keys/1 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
{
"id": 2,
"name": "hoi",
"user_id": 2,
"ttl": -1,
"issued_at": 1222222,
"revoked": false
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
access_key = service.get(aid)
if access_key is None:
return dict(message="This token does not exist!"), 404
if access_key.user_id != g.current_user.id:
if not ApiKeyCreatorPermission().can():
return dict(message="You are not authorized to view this token!"), 403
return access_key
api.add_resource(ApiKeyList, '/keys', endpoint='api_keys')
api.add_resource(ApiKeys, '/keys/<int:aid>', endpoint='api_key')
api.add_resource(ApiKeysDescribed, '/keys/<int:aid>/described', endpoint='api_key_described')
api.add_resource(ApiKeyUserList, '/users/<int:user_id>/keys', endpoint='user_api_keys')
api.add_resource(UserApiKeys, '/users/<int:uid>/keys/<int:aid>', endpoint='user_api_key')

187
lemur/auth/ldap.py Normal file
View File

@ -0,0 +1,187 @@
"""
.. module: lemur.auth.ldap
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Ian Stahnke <ian.stahnke@myob.com>
"""
import ldap
from flask import current_app
from lemur.users import service as user_service
from lemur.roles import service as role_service
from lemur.common.utils import validate_conf, get_psuedo_random_string
class LdapPrincipal():
"""
Provides methods for authenticating against an LDAP server.
"""
def __init__(self, args):
self._ldap_validate_conf()
# setup ldap config
if not args['username']:
raise Exception("missing ldap username")
if not args['password']:
self.error_message = "missing ldap password"
raise Exception("missing ldap password")
self.ldap_principal = args['username']
self.ldap_email_domain = current_app.config.get("LDAP_EMAIL_DOMAIN", None)
if '@' not in self.ldap_principal:
self.ldap_principal = '%s@%s' % (self.ldap_principal, self.ldap_email_domain)
self.ldap_username = args['username']
if '@' in self.ldap_username:
self.ldap_username = args['username'].split("@")[0]
self.ldap_password = args['password']
self.ldap_server = current_app.config.get('LDAP_BIND_URI', None)
self.ldap_base_dn = current_app.config.get("LDAP_BASE_DN", None)
self.ldap_use_tls = current_app.config.get("LDAP_USE_TLS", False)
self.ldap_cacert_file = current_app.config.get("LDAP_CACERT_FILE", None)
self.ldap_default_role = current_app.config.get("LEMUR_DEFAULT_ROLE", None)
self.ldap_required_group = current_app.config.get("LDAP_REQUIRED_GROUP", None)
self.ldap_groups_to_roles = current_app.config.get("LDAP_GROUPS_TO_ROLES", None)
self.ldap_attrs = ['memberOf']
self.ldap_client = None
self.ldap_groups = None
def _update_user(self, roles):
"""
create or update a local user instance.
"""
# try to get user from local database
user = user_service.get_by_email(self.ldap_principal)
# create them a local account
if not user:
user = user_service.create(
self.ldap_username,
get_psuedo_random_string(),
self.ldap_principal,
True,
'', # thumbnailPhotoUrl
list(roles)
)
else:
# we add 'lemur' specific roles, so they do not get marked as removed
for ur in user.roles:
if not ur.third_party:
roles.add(ur)
# update any changes to the user
user_service.update(
user.id,
self.ldap_username,
self.ldap_principal,
user.active,
user.profile_picture,
list(roles)
)
return user
def _authorize(self):
"""
check groups and roles to confirm access.
return a list of roles if ok.
raise an exception on error.
"""
if not self.ldap_principal:
return None
if self.ldap_required_group:
# ensure the user has the required group in their group list
if self.ldap_required_group not in self.ldap_groups:
return None
roles = set()
if self.ldap_default_role:
role = role_service.get_by_name(self.ldap_default_role)
if role:
if not role.third_party:
role = role.set_third_party(role.id, third_party_status=True)
roles.add(role)
# update their 'roles'
role = role_service.get_by_name(self.ldap_principal)
if not role:
description = "auto generated role based on owner: {0}".format(self.ldap_principal)
role = role_service.create(self.ldap_principal, description=description,
third_party=True)
if not role.third_party:
role = role_service.set_third_party(role.id, third_party_status=True)
roles.add(role)
if not self.ldap_groups_to_roles:
return roles
for ldap_group_name, role_name in self.ldap_groups_to_roles.items():
role = role_service.get_by_name(role_name)
if role:
if ldap_group_name in self.ldap_groups:
current_app.logger.debug("assigning role {0} to ldap user {1}".format(self.ldap_principal, role))
if not role.third_party:
role = role_service.set_third_party(role.id, third_party_status=True)
roles.add(role)
return roles
def authenticate(self):
"""
orchestrate the ldap login.
raise an exception on error.
"""
self._bind()
roles = self._authorize()
if not roles:
raise Exception('ldap authorization failed')
return self._update_user(roles)
def _bind(self):
"""
authenticate an ldap user.
list groups for a user.
raise an exception on error.
"""
if '@' not in self.ldap_principal:
self.ldap_principal = '%s@%s' % (self.ldap_principal, self.ldap_email_domain)
ldap_filter = 'userPrincipalName=%s' % self.ldap_principal
# query ldap for auth
try:
# build a client
if not self.ldap_client:
self.ldap_client = ldap.initialize(self.ldap_server)
# perform a synchronous bind
self.ldap_client.set_option(ldap.OPT_REFERRALS, 0)
if self.ldap_use_tls:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
self.ldap_client.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
self.ldap_client.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
self.ldap_client.set_option(ldap.OPT_X_TLS_DEMAND, True)
self.ldap_client.set_option(ldap.OPT_DEBUG_LEVEL, 255)
if self.ldap_cacert_file:
self.ldap_client.set_option(ldap.OPT_X_TLS_CACERTFILE, self.ldap_cacert_file)
self.ldap_client.simple_bind_s(self.ldap_principal, self.ldap_password)
except ldap.INVALID_CREDENTIALS:
self.ldap_client.unbind()
raise Exception('The supplied ldap credentials are invalid')
except ldap.SERVER_DOWN:
raise Exception('ldap server unavailable')
except ldap.LDAPError as e:
raise Exception("ldap error: {0}".format(e))
lgroups = self.ldap_client.search_s(self.ldap_base_dn,
ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs)[0][1]['memberOf']
# lgroups is a list of utf-8 encoded strings
# convert to a single string of groups to allow matching
self.ldap_groups = b''.join(lgroups).decode('ascii')
self.ldap_client.unbind()
def _ldap_validate_conf(self):
"""
Confirms required ldap config settings exist.
"""
required_vars = [
'LDAP_BIND_URI',
'LDAP_BASE_DN',
'LDAP_EMAIL_DOMAIN',
]
validate_conf(current_app, required_vars)

View File

@ -2,43 +2,50 @@
.. module: lemur.auth.permissions .. module: lemur.auth.permissions
:platform: Unix :platform: Unix
:synopsis: This module defines all the permission used within Lemur :synopsis: This module defines all the permission used within Lemur
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from functools import partial from functools import partial
from collections import namedtuple from collections import namedtuple
from flask.ext.principal import Permission, RoleNeed from flask_principal import Permission, RoleNeed
# Permissions # Permissions
operator_permission = Permission(RoleNeed('operator')) operator_permission = Permission(RoleNeed('operator'))
admin_permission = Permission(RoleNeed('admin')) admin_permission = Permission(RoleNeed('admin'))
CertificateCreator = namedtuple('certificate', ['method', 'value']) CertificateOwner = namedtuple('certificate', ['method', 'value'])
CertificateCreatorNeed = partial(CertificateCreator, 'key') CertificateOwnerNeed = partial(CertificateOwner, 'role')
class ViewKeyPermission(Permission): class SensitiveDomainPermission(Permission):
def __init__(self, certificate_id, owner): def __init__(self):
c_need = CertificateCreatorNeed(certificate_id) super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
super(ViewKeyPermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
class UpdateCertificatePermission(Permission): class CertificatePermission(Permission):
def __init__(self, certificate_id, owner): def __init__(self, owner, roles):
c_need = CertificateCreatorNeed(certificate_id) needs = [RoleNeed('admin'), RoleNeed(owner), RoleNeed('creator')]
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin')) for r in roles:
needs.append(CertificateOwnerNeed(str(r)))
super(CertificatePermission, self).__init__(*needs)
RoleUser = namedtuple('role', ['method', 'value']) class ApiKeyCreatorPermission(Permission):
ViewRoleCredentialsNeed = partial(RoleUser, 'roleView') def __init__(self):
super(ApiKeyCreatorPermission, self).__init__(RoleNeed('admin'))
class ViewRoleCredentialsPermission(Permission): RoleMember = namedtuple('role', ['method', 'value'])
RoleMemberNeed = partial(RoleMember, 'member')
class RoleMemberPermission(Permission):
def __init__(self, role_id): def __init__(self, role_id):
need = ViewRoleCredentialsNeed(role_id) needs = [RoleNeed('admin'), RoleMemberNeed(role_id)]
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin')) super(RoleMemberPermission, self).__init__(*needs)
AuthorityCreator = namedtuple('authority', ['method', 'value']) AuthorityCreator = namedtuple('authority', ['method', 'value'])

View File

@ -3,16 +3,13 @@
:platform: Unix :platform: Unix
:synopsis: This module contains all of the authentication duties for :synopsis: This module contains all of the authentication duties for
lemur lemur
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from __future__ import unicode_literals
from builtins import bytes
import jwt import jwt
import json import json
import base64
import binascii import binascii
from functools import wraps from functools import wraps
@ -20,31 +17,18 @@ from datetime import datetime, timedelta
from flask import g, current_app, jsonify, request from flask import g, current_app, jsonify, request
from flask.ext.restful import Resource from flask_restful import Resource
from flask.ext.principal import identity_loaded, RoleNeed, UserNeed from flask_principal import identity_loaded, RoleNeed, UserNeed
from flask.ext.principal import Identity, identity_changed from flask_principal import Identity, identity_changed
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from lemur.users import service as user_service from lemur.users import service as user_service
from lemur.auth.permissions import CertificateCreatorNeed, \ from lemur.api_keys import service as api_key_service
AuthorityCreatorNeed, ViewRoleCredentialsNeed from lemur.auth.permissions import AuthorityCreatorNeed, RoleMemberNeed
def base64url_decode(data):
rem = len(data) % 4
if rem > 0:
data += '=' * (4 - rem)
return base64.urlsafe_b64decode(bytes(data.encode('latin-1')))
def base64url_encode(data):
return base64.urlsafe_b64encode(data).replace('=', '')
def get_rsa_public_key(n, e): def get_rsa_public_key(n, e):
@ -55,8 +39,9 @@ def get_rsa_public_key(n, e):
:param e: :param e:
:return: a RSA Public Key in PEM format :return: a RSA Public Key in PEM format
""" """
n = int(binascii.hexlify(base64url_decode(n)), 16) n = int(binascii.hexlify(jwt.utils.base64url_decode(bytes(n, 'utf-8'))), 16)
e = int(binascii.hexlify(base64url_decode(e)), 16) e = int(binascii.hexlify(jwt.utils.base64url_decode(bytes(e, 'utf-8'))), 16)
pub = RSAPublicNumbers(e, n).public_key(default_backend()) pub = RSAPublicNumbers(e, n).public_key(default_backend())
return pub.public_bytes( return pub.public_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
@ -64,9 +49,9 @@ def get_rsa_public_key(n, e):
) )
def create_token(user): def create_token(user, aid=None, ttl=None):
""" """
Create a valid JWT for a given user, this token is then used to authenticate Create a valid JWT for a given user/api key, this token is then used to authenticate
sessions until the token expires. sessions until the token expires.
:param user: :param user:
@ -74,17 +59,31 @@ def create_token(user):
""" """
expiration_delta = timedelta(days=int(current_app.config.get('LEMUR_TOKEN_EXPIRATION', 1))) expiration_delta = timedelta(days=int(current_app.config.get('LEMUR_TOKEN_EXPIRATION', 1)))
payload = { payload = {
'sub': user.id, 'iat': datetime.utcnow(),
'iat': datetime.now(), 'exp': datetime.utcnow() + expiration_delta
'exp': datetime.now() + expiration_delta
} }
# Handle Just a User ID & User Object.
if isinstance(user, int):
payload['sub'] = user
else:
payload['sub'] = user.id
if aid is not None:
payload['aid'] = aid
# Custom TTLs are only supported on Access Keys.
if ttl is not None and aid is not None:
# Tokens that are forever until revoked.
if ttl == -1:
del payload['exp']
else:
payload['exp'] = ttl
token = jwt.encode(payload, current_app.config['LEMUR_TOKEN_SECRET']) token = jwt.encode(payload, current_app.config['LEMUR_TOKEN_SECRET'])
return token.decode('unicode_escape') return token.decode('unicode_escape')
def login_required(f): def login_required(f):
""" """
Validates the JWT and ensures that is has not expired. Validates the JWT and ensures that is has not expired and the user is still active.
:param f: :param f:
:return: :return:
@ -110,7 +109,22 @@ def login_required(f):
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
return dict(message='Token is invalid'), 403 return dict(message='Token is invalid'), 403
g.current_user = user_service.get(payload['sub']) if 'aid' in payload:
access_key = api_key_service.get(payload['aid'])
if access_key.revoked:
return dict(message='Token has been revoked'), 403
if access_key.ttl != -1:
current_time = datetime.utcnow()
expired_time = datetime.fromtimestamp(access_key.issued_at + access_key.ttl)
if current_time >= expired_time:
return dict(message='Token has expired'), 403
user = user_service.get(payload['sub'])
if not user.active:
return dict(message='User is not currently active'), 403
g.current_user = user
if not g.current_user: if not g.current_user:
return dict(message='You are not logged in'), 403 return dict(message='You are not logged in'), 403
@ -138,13 +152,10 @@ def fetch_token_header(token):
raise jwt.DecodeError('Not enough segments') raise jwt.DecodeError('Not enough segments')
try: try:
return json.loads(base64url_decode(header_segment)) return json.loads(jwt.utils.base64url_decode(header_segment).decode('utf-8'))
except TypeError as e: except TypeError as e:
current_app.logger.exception(e) current_app.logger.exception(e)
raise jwt.DecodeError('Invalid header padding') raise jwt.DecodeError('Invalid header padding')
except binascii.Error as e:
current_app.logger.exception(e)
raise jwt.DecodeError('Invalid header padding')
@identity_loaded.connect @identity_loaded.connect
@ -165,19 +176,14 @@ def on_identity_loaded(sender, identity):
# identity with the roles that the user provides # identity with the roles that the user provides
if hasattr(user, 'roles'): if hasattr(user, 'roles'):
for role in user.roles: for role in user.roles:
identity.provides.add(ViewRoleCredentialsNeed(role.id))
identity.provides.add(RoleNeed(role.name)) identity.provides.add(RoleNeed(role.name))
identity.provides.add(RoleMemberNeed(role.id))
# apply ownership for authorities # apply ownership for authorities
if hasattr(user, 'authorities'): if hasattr(user, 'authorities'):
for authority in user.authorities: for authority in user.authorities:
identity.provides.add(AuthorityCreatorNeed(authority.id)) identity.provides.add(AuthorityCreatorNeed(authority.id))
# apply ownership of certificates
if hasattr(user, 'certificates'):
for certificate in user.certificates:
identity.provides.add(CertificateCreatorNeed(certificate.id))
g.user = user g.user = user

View File

@ -1,7 +1,7 @@
""" """
.. module: lemur.auth.views .. module: lemur.auth.views
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
@ -9,22 +9,191 @@ import jwt
import base64 import base64
import requests 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_restful import reqparse, Resource, Api
from flask.ext.principal import Identity, identity_changed from flask_principal import Identity, identity_changed
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
from lemur.extensions import metrics
from lemur.common.utils import get_psuedo_random_string from lemur.common.utils import get_psuedo_random_string
from lemur.users import service as user_service from lemur.users import service as user_service
from lemur.roles import service as role_service from lemur.roles import service as role_service
from lemur.auth.service import create_token, fetch_token_header, get_rsa_public_key from lemur.auth.service import create_token, fetch_token_header, get_rsa_public_key
import lemur.auth.ldap as ldap
mod = Blueprint('auth', __name__) mod = Blueprint('auth', __name__)
api = Api(mod) api = Api(mod)
def exchange_for_access_token(code, redirect_uri, client_id, secret, access_token_url=None, verify_cert=True):
"""
Exchanges authorization code for access token.
:param code:
:param redirect_uri:
:param client_id:
:param secret:
:param access_token_url:
:param verify_cert:
:return:
:return:
"""
# take the information we have received from the provider to create a new request
params = {
'grant_type': 'authorization_code',
'scope': 'openid email profile address',
'code': code,
'redirect_uri': redirect_uri,
'client_id': client_id
}
# the secret and cliendId will be given to you when you signup for the provider
token = '{0}:{1}'.format(client_id, secret)
basic = base64.b64encode(bytes(token, 'utf-8'))
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'authorization': 'basic {0}'.format(basic.decode('utf-8'))
}
# exchange authorization code for access token.
r = requests.post(access_token_url, headers=headers, params=params, verify=verify_cert)
if r.status_code == 400:
r = requests.post(access_token_url, headers=headers, data=params, verify=verify_cert)
id_token = r.json()['id_token']
access_token = r.json()['access_token']
return id_token, access_token
def validate_id_token(id_token, client_id, jwks_url):
"""
Ensures that the token we receive is valid.
:param id_token:
:param client_id:
:param jwks_url:
:return:
"""
# fetch token public key
header_data = fetch_token_header(id_token)
# retrieve the key material as specified by the token header
r = requests.get(jwks_url)
for key in r.json()['keys']:
if key['kid'] == header_data['kid']:
secret = get_rsa_public_key(key['n'], key['e'])
algo = header_data['alg']
break
else:
return dict(message='Key not found'), 401
# validate your token based on the key it was signed with
try:
jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=client_id)
except jwt.DecodeError:
return dict(message='Token is invalid'), 401
except jwt.ExpiredSignatureError:
return dict(message='Token has expired'), 401
except jwt.InvalidTokenError:
return dict(message='Token is invalid'), 401
def retrieve_user(user_api_url, access_token):
"""
Fetch user information from provided user api_url.
:param user_api_url:
:param access_token:
:return:
"""
user_params = dict(access_token=access_token, schema='profile')
# retrieve information about the current user.
r = requests.get(user_api_url, params=user_params)
profile = r.json()
user = user_service.get_by_email(profile['email'])
return user, profile
def create_user_roles(profile):
"""Creates new roles based on profile information.
:param profile:
:return:
"""
roles = []
# update their google 'roles'
for group in profile['googleGroups']:
role = role_service.get_by_name(group)
if not role:
role = role_service.create(group, description='This is a google group based role created by Lemur', third_party=True)
if not role.third_party:
role = role_service.set_third_party(role.id, third_party_status=True)
roles.append(role)
role = role_service.get_by_name(profile['email'])
if not role:
role = role_service.create(profile['email'], description='This is a user specific role', third_party=True)
if not role.third_party:
role = role_service.set_third_party(role.id, third_party_status=True)
roles.append(role)
# every user is an operator (tied to a default role)
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
default = role_service.get_by_name(current_app.config['LEMUR_DEFAULT_ROLE'])
if not default:
default = role_service.create(current_app.config['LEMUR_DEFAULT_ROLE'], description='This is the default Lemur role.')
if not default.third_party:
role_service.set_third_party(default.id, third_party_status=True)
roles.append(default)
return roles
def update_user(user, profile, roles):
"""Updates user with current profile information and associated roles.
:param user:
:param profile:
:param roles:
"""
# if we get an sso user create them an account
if not user:
user = user_service.create(
profile['email'],
get_psuedo_random_string(),
profile['email'],
True,
profile.get('thumbnailPhotoUrl'),
roles
)
else:
# we add 'lemur' specific roles, so they do not get marked as removed
for ur in user.roles:
if not ur.third_party:
roles.append(ur)
# update any changes to the user
user_service.update(
user.id,
profile['email'],
profile['email'],
True,
profile.get('thumbnailPhotoUrl'), # profile isn't google+ enabled
roles
)
class Login(Resource): class Login(Resource):
""" """
Provides an endpoint for Lemur's basic authentication. It takes a username and password Provides an endpoint for Lemur's basic authentication. It takes a username and password
@ -92,32 +261,54 @@ class Login(Resource):
else: else:
user = user_service.get_by_username(args['username']) user = user_service.get_by_username(args['username'])
if user and user.check_password(args['password']): # default to local authentication
if user and user.check_password(args['password']) and user.active:
# Tell Flask-Principal the identity changed # Tell Flask-Principal the identity changed
identity_changed.send(current_app._get_current_object(), identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id)) identity=Identity(user.id))
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user)) return dict(token=create_token(user))
return dict(message='The supplied credentials are invalid'), 401 # try ldap login
if current_app.config.get("LDAP_AUTH"):
try:
ldap_principal = ldap.LdapPrincipal(args)
user = ldap_principal.authenticate()
if user and user.active:
# Tell Flask-Principal the identity changed
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user))
except Exception as e:
current_app.logger.error("ldap error: {0}".format(e))
ldap_message = 'ldap error: %s' % e
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
return dict(message=ldap_message), 403
def get(self): # if not valid user - no certificates for you
return {'username': g.current_user.username, 'roles': [r.name for r in g.current_user.roles]} metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
return dict(message='The supplied credentials are invalid'), 403
class Ping(Resource): class Ping(Resource):
""" """
This class serves as an example of how one might implement an SSO provider for use with Lemur. In This class serves as an example of how one might implement an SSO provider for use with Lemur. In
this example we use a OpenIDConnect authentication flow, that is essentially OAuth2 underneath. If you have an this example we use an OpenIDConnect authentication flow, that is essentially OAuth2 underneath. If you have an
OAuth2 provider you want to use Lemur there would be two steps: OAuth2 provider you want to use Lemur there would be two steps:
1. Define your own class that inherits from :class:`flask.ext.restful.Resource` and create the HTTP methods the \ 1. Define your own class that inherits from :class:`flask_restful.Resource` and create the HTTP methods the \
provider uses for it's callbacks. provider uses for its callbacks.
2. Add or change the Lemur AngularJS Configuration to point to your new provider 2. Add or change the Lemur AngularJS Configuration to point to your new provider
""" """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(Ping, self).__init__() super(Ping, self).__init__()
def get(self):
return 'Redirecting...'
def post(self): def post(self):
self.reqparse.add_argument('clientId', type=str, required=True, location='json') 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('redirectUri', type=str, required=True, location='json')
@ -125,108 +316,85 @@ class Ping(Resource):
args = self.reqparse.parse_args() args = self.reqparse.parse_args()
# take the information we have received from the provider to create a new request
params = {
'client_id': args['clientId'],
'grant_type': 'authorization_code',
'scope': 'openid email profile address',
'redirect_uri': args['redirectUri'],
'code': args['code']
}
# you can either discover these dynamically or simply configure them # you can either discover these dynamically or simply configure them
access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL') access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL')
user_api_url = current_app.config.get('PING_USER_API_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 the provider secret = current_app.config.get('PING_SECRET')
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
headers = {'Authorization': 'Basic {0}'.format(basic)}
# exchange authorization code for access token. id_token, access_token = exchange_for_access_token(
args['code'],
args['redirectUri'],
args['clientId'],
secret,
access_token_url=access_token_url
)
r = requests.post(access_token_url, headers=headers, params=params)
id_token = r.json()['id_token']
access_token = r.json()['access_token']
# fetch token public key
header_data = fetch_token_header(id_token)
jwks_url = current_app.config.get('PING_JWKS_URL') jwks_url = current_app.config.get('PING_JWKS_URL')
validate_id_token(id_token, args['clientId'], jwks_url)
# retrieve the key material as specified by the token header user, profile = retrieve_user(user_api_url, access_token)
r = requests.get(jwks_url) roles = create_user_roles(profile)
for key in r.json()['keys']: update_user(user, profile, roles)
if key['kid'] == header_data['kid']:
secret = get_rsa_public_key(key['n'], key['e'])
algo = header_data['alg']
break
else:
return dict(message='Key not found'), 403
# validate your token based on the key it was signed with if not user or not user.active:
try: metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
jwt.decode(id_token, secret, algorithms=[algo], audience=args['clientId']) return dict(message='The supplied credentials are invalid'), 403
except jwt.DecodeError:
return dict(message='Token is invalid'), 403
except jwt.ExpiredSignatureError:
return dict(message='Token has expired'), 403
except jwt.InvalidTokenError:
return dict(message='Token is invalid'), 403
user_params = dict(access_token=access_token, schema='profile')
# retrieve information about the current user.
r = requests.get(user_api_url, params=user_params)
profile = r.json()
user = user_service.get_by_email(profile['email'])
# update their google 'roles'
roles = []
for group in profile['googleGroups']:
role = role_service.get_by_name(group)
if not role:
role = role_service.create(group, description='This is a google group based role created by Lemur')
roles.append(role)
# 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 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'],
get_psuedo_random_string(),
profile['email'],
True,
profile.get('thumbnailPhotoUrl'),
roles
)
else:
# we add 'lemur' specific roles, so they do not get marked as removed
for ur in user.roles:
if ur.authority_id:
roles.append(ur)
# update any changes to the user
user_service.update(
user.id,
profile['email'],
profile['email'],
True,
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
roles
)
# Tell Flask-Principal the identity changed # Tell Flask-Principal the identity changed
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user))
class OAuth2(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(OAuth2, self).__init__()
def get(self):
return 'Redirecting...'
def post(self):
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()
# you can either discover these dynamically or simply configure them
access_token_url = current_app.config.get('OAUTH2_ACCESS_TOKEN_URL')
user_api_url = current_app.config.get('OAUTH2_USER_API_URL')
verify_cert = current_app.config.get('OAUTH2_VERIFY_CERT')
secret = current_app.config.get('OAUTH2_SECRET')
id_token, access_token = exchange_for_access_token(
args['code'],
args['redirectUri'],
args['clientId'],
secret,
access_token_url=access_token_url,
verify_cert=verify_cert
)
jwks_url = current_app.config.get('PING_JWKS_URL')
validate_id_token(id_token, args['clientId'], jwks_url)
user, profile = retrieve_user(user_api_url, access_token)
roles = create_user_roles(profile)
update_user(user, profile, roles)
if not user.active:
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
return dict(message='The supplied credentials are invalid'), 403
# Tell Flask-Principal the identity changed
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user)) return dict(token=create_token(user))
@ -265,15 +433,22 @@ class Google(Resource):
user = user_service.get_by_email(profile['email']) user = user_service.get_by_email(profile['email'])
if not (user and user.active):
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
return dict(message='The supplied credentials are invalid.'), 403
if user: if user:
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user)) return dict(token=create_token(user))
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
class Providers(Resource): class Providers(Resource):
def get(self): def get(self):
active_providers = [] active_providers = []
for provider in current_app.config.get("ACTIVE_PROVIDERS"): for provider in current_app.config.get("ACTIVE_PROVIDERS", []):
provider = provider.lower() provider = provider.lower()
if provider == "google": if provider == "google":
@ -291,16 +466,33 @@ class Providers(Resource):
'clientId': current_app.config.get("PING_CLIENT_ID"), 'clientId': current_app.config.get("PING_CLIENT_ID"),
'responseType': 'code', 'responseType': 'code',
'scope': ['openid', 'email', 'profile', 'address'], 'scope': ['openid', 'email', 'profile', 'address'],
'scopeDelimeter': ' ', 'scopeDelimiter': ' ',
'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"), 'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"),
'requiredUrlParams': ['scope'], 'requiredUrlParams': ['scope'],
'type': '2.0' 'type': '2.0'
}) })
elif provider == "oauth2":
active_providers.append({
'name': current_app.config.get("OAUTH2_NAME"),
'url': current_app.config.get('OAUTH2_REDIRECT_URI'),
'redirectUri': current_app.config.get("OAUTH2_REDIRECT_URI"),
'clientId': current_app.config.get("OAUTH2_CLIENT_ID"),
'responseType': 'code',
'scope': ['openid', 'email', 'profile', 'groups'],
'scopeDelimiter': ' ',
'authorizationEndpoint': current_app.config.get("OAUTH2_AUTH_ENDPOINT"),
'requiredUrlParams': ['scope', 'state', 'nonce'],
'state': 'STATE',
'nonce': get_psuedo_random_string(),
'type': '2.0'
})
return active_providers return active_providers
api.add_resource(Login, '/auth/login', endpoint='login') api.add_resource(Login, '/auth/login', endpoint='login')
api.add_resource(Ping, '/auth/ping', endpoint='ping') api.add_resource(Ping, '/auth/ping', endpoint='ping')
api.add_resource(Google, '/auth/google', endpoint='google') api.add_resource(Google, '/auth/google', endpoint='google')
api.add_resource(OAuth2, '/auth/oauth2', endpoint='oauth2')
api.add_resource(Providers, '/auth/providers', endpoint='providers') api.add_resource(Providers, '/auth/providers', endpoint='providers')

View File

@ -1,58 +1,52 @@
""" """
.. module: lemur.authorities.models .. module: lemur.authorities.models
:platform: unix :platform: unix
:synopsis: This module contains all of the models need to create a authority within Lemur. :synopsis: This module contains all of the models need to create an authority within Lemur.
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean
from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSON
from lemur.database import db from lemur.database import db
from lemur.certificates.models import get_cn, get_not_after, get_not_before from lemur.plugins.base import plugins
from lemur.models import roles_authorities
class Authority(db.Model): class Authority(db.Model):
__tablename__ = 'authorities' __tablename__ = 'authorities'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
owner = Column(String(128)) owner = Column(String(128), nullable=False)
name = Column(String(128), unique=True) name = Column(String(128), unique=True)
body = Column(Text()) body = Column(Text())
chain = 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) active = Column(Boolean, default=True)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
plugin_name = Column(String(64)) plugin_name = Column(String(64))
description = Column(Text) description = Column(Text)
options = Column(JSON) 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')) 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): authority_pending_certificate = relationship("PendingCertificate", backref='root_authority', uselist=False, foreign_keys='PendingCertificate.root_authority_id')
self.name = name pending_certificates = relationship('PendingCertificate', backref='authority', foreign_keys='PendingCertificate.authority_id')
self.body = body
self.chain = chain
self.owner = owner
self.plugin_name = plugin_name
cert = x509.load_pem_x509_certificate(str(body), default_backend())
self.cn = get_cn(cert)
self.not_before = get_not_before(cert)
self.not_after = get_not_after(cert)
self.roles = roles
self.description = description
def as_dict(self): def __init__(self, **kwargs):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} 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']
self.options = kwargs.get('options')
def serialize(self): @property
blob = self.as_dict() def plugin(self):
return blob return plugins.get(self.plugin_name)
def __repr__(self):
return "Authority(name={name})".format(name=self.name)

View File

@ -0,0 +1,119 @@
"""
.. module: lemur.authorities.schemas
:platform: unix
:copyright: (c) 2018 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, pre_load
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, missing
from lemur.common.fields import ArrowDateTime
class AuthorityInputSchema(LemurInputSchema):
name = fields.String(required=True)
owner = fields.Email(required=True)
description = fields.String()
common_name = fields.String(required=True, validate=validators.common_name)
validity_start = ArrowDateTime()
validity_end = ArrowDateTime()
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.")
@pre_load
def ensure_dates(self, data):
return missing.convert_validity_years(data)
class AuthorityUpdateSchema(LemurInputSchema):
owner = fields.Email(required=True)
description = fields.String()
active = fields.Boolean(missing=True)
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):
__envelope__ = False
id = fields.Integer()
description = fields.String()
name = fields.String()
owner = fields.Email()
plugin = fields.Nested(PluginOutputSchema)
active = fields.Boolean()
authority_update_schema = AuthorityUpdateSchema()
authority_input_schema = AuthorityInputSchema()
authority_output_schema = AuthorityOutputSchema()
authorities_output_schema = AuthorityOutputSchema(many=True)

View File

@ -3,108 +3,121 @@
:platform: Unix :platform: Unix
:synopsis: This module contains all of the services level functions used to :synopsis: This module contains all of the services level functions used to
administer authorities in Lemur administer authorities in Lemur
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import g
from flask import current_app import json
from lemur import database from lemur import database
from lemur.common.utils import truthiness
from lemur.extensions import metrics
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.roles import service as role_service 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.service import upload
from lemur.certificates.models import Certificate
from lemur.plugins.base import plugins
def update(authority_id, description=None, owner=None, active=None, roles=None): def update(authority_id, description, owner, active, roles):
""" """
Update a an authority with new values. Update an authority with new values.
:param authority_id: :param authority_id:
:param roles: roles that are allowed to use this authority :param roles: roles that are allowed to use this authority
:rtype : Authority
:return: :return:
""" """
authority = get(authority_id) authority = get(authority_id)
if roles:
authority = database.update_list(authority, 'roles', Role, roles)
if active:
authority.active = active
authority.roles = roles
authority.active = active
authority.description = description authority.description = description
authority.owner = owner authority.owner = owner
return database.update(authority) 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']
values = issuer.create_authority(kwargs)
:rtype : Authority # support older plugins
if len(values) == 3:
body, chain, roles = values
private_key = None
elif len(values) == 4:
body, private_key, chain, roles = values
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title, kwargs['creator'])
return body, private_key, chain, roles
def create_authority_roles(roles, owner, plugin_title, creator):
"""
Creates all of the necessary authority roles.
:param creator:
:param roles:
:return: :return:
""" """
issuer = plugins.get(kwargs.get('pluginName'))
kwargs['creator'] = g.current_user.email
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
cert = Certificate(cert_body, chain=intermediate)
cert.owner = kwargs['ownerEmail']
if kwargs['caType'] == 'subca':
cert.description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
authority is {1}.".format(kwargs.get('caName'), kwargs.get('caParent'))
else:
cert.description = "This is the ROOT certificate for the {0} certificate authority.".format(
kwargs.get('caName')
)
cert.user = g.current_user
cert.notifications = notification_service.create_default_expiration_notifications(
'DEFAULT_SECURITY',
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
)
# we create and attach any roles that the issuer gives us
role_objs = [] role_objs = []
for r in issuer_roles: for r in roles:
role = role_service.get_by_name(r['name'])
role = role_service.create( if not role:
r['name'], role = role_service.create(
password=r['password'], r['name'],
description="{0} auto generated role".format(kwargs.get('pluginName')), password=r['password'],
username=r['username']) description="Auto generated role for {0}".format(plugin_title),
username=r['username'])
# the user creating the authority should be able to administer it # the user creating the authority should be able to administer it
if role.username == 'admin': if role.username == 'admin':
g.current_user.roles.append(role) creator.roles.append(role)
role_objs.append(role) role_objs.append(role)
authority = Authority( # create an role for the owner and assign it
kwargs.get('caName'), owner_role = role_service.get_by_name(owner)
kwargs['ownerEmail'], if not owner_role:
kwargs['pluginName'], owner_role = role_service.create(
cert_body, owner,
description=kwargs['caDescription'], description="Auto generated role based on owner: {0}".format(owner)
chain=intermediate, )
roles=role_objs
)
database.update(cert) role_objs.append(owner_role)
return role_objs
def create(**kwargs):
"""
Creates a new authority.
"""
body, private_key, chain, roles = mint(**kwargs)
kwargs['creator'].roles = list(set(list(kwargs['creator'].roles) + roles))
kwargs['body'] = body
kwargs['private_key'] = private_key
kwargs['chain'] = chain
if kwargs.get('roles'):
kwargs['roles'] += roles
else:
kwargs['roles'] = roles
cert = upload(**kwargs)
kwargs['authority_certificate'] = cert
if kwargs.get('plugin', {}).get('plugin_options', []):
kwargs['options'] = json.dumps(kwargs['plugin']['plugin_options'])
authority = Authority(**kwargs)
authority = database.create(authority) authority = database.create(authority)
kwargs['creator'].authorities.append(authority)
g.current_user.authorities.append(authority) metrics.send('authority_created', 'counter', 1, metric_tags=dict(owner=authority.owner))
return authority return authority
@ -123,7 +136,6 @@ def get(authority_id):
""" """
Retrieves an authority given it's ID Retrieves an authority given it's ID
:rtype : Authority
:param authority_id: :param authority_id:
:return: :return:
""" """
@ -135,28 +147,22 @@ def get_by_name(authority_name):
Retrieves an authority given it's name. Retrieves an authority given it's name.
:param authority_name: :param authority_name:
:rtype : Authority
:return: :return:
""" """
return database.get(Authority, authority_name, field='name') return database.get(Authority, authority_name, field='name')
def get_authority_role(ca_name): def get_authority_role(ca_name, creator=None):
""" """
Attempts to get the authority role for a given ca uses current_user Attempts to get the authority role for a given ca uses current_user
as a basis for accomplishing that. as a basis for accomplishing that.
:param ca_name: :param ca_name:
""" """
if g.current_user.is_admin: if creator:
authority = get_by_name(ca_name) if creator.is_admin:
# TODO we should pick admin ca roles for admin return role_service.get_by_name("{0}_admin".format(ca_name))
return authority.roles[0] return role_service.get_by_name("{0}_operator".format(ca_name))
else:
for role in g.current_user.roles:
if role.authority:
if role.authority.name == ca_name:
return role
def render(args): def render(args):
@ -166,30 +172,24 @@ def render(args):
:return: :return:
""" """
query = database.session_query(Authority) 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') filt = args.pop('filter')
if filt: if filt:
terms = filt.split(';') terms = filt.split(';')
if 'active' in filt: # this is really weird but strcmp seems to not work here?? if 'active' in filt:
query = query.filter(Authority.active == terms[1]) query = query.filter(Authority.active == truthiness(terms[1]))
else: else:
query = database.filter(query, Authority, terms) query = database.filter(query, Authority, terms)
# we make sure that a user can only use an authority they either own are are a member of - admins can see all # we make sure that a user can only use an authority they either own are a member of - admins can see all
if not g.current_user.is_admin: if not args['user'].is_admin:
authority_ids = [] authority_ids = []
for role in g.current_user.roles: for authority in args['user'].authorities:
if role.authority: authority_ids.append(authority.id)
authority_ids.append(role.authority.id)
for role in args['user'].roles:
for authority in role.authorities:
authority_ids.append(authority.id)
query = query.filter(Authority.id.in_(authority_ids)) query = query.filter(Authority.id.in_(authority_ids))
query = database.find_all(query, Authority, args) return database.sort_and_page(query, Authority, args)
if sort_by and sort_dir:
query = database.sort(query, Authority, sort_by, sort_dir)
return database.paginate(query, page, count)

View File

@ -1,36 +1,23 @@
""" """
.. module: lemur.authorities.views .. module: lemur.authorities.views
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import Blueprint, g from flask import Blueprint, g
from flask.ext.restful import reqparse, fields, Api from flask_restful import reqparse, Api
from lemur.authorities import service from lemur.common.utils import paginated_parser
from lemur.roles import service as role_service from lemur.common.schema import validate_schema
from lemur.certificates import service as certificate_service
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import AuthorityPermission from lemur.auth.permissions import AuthorityPermission
from lemur.common.utils import paginated_parser, marshal_items from lemur.certificates import service as certificate_service
from lemur.authorities import service
from lemur.authorities.schemas import authority_input_schema, authority_output_schema, authorities_output_schema, authority_update_schema
FIELDS = {
'name': fields.String,
'owner': fields.String,
'description': fields.String,
'options': fields.Raw,
'pluginName': fields.String,
'body': fields.String,
'chain': fields.String,
'active': fields.Boolean,
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
'id': fields.Integer,
}
mod = Blueprint('authorities', __name__) mod = Blueprint('authorities', __name__)
api = Api(mod) api = Api(mod)
@ -42,7 +29,7 @@ class AuthoritiesList(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(AuthoritiesList, self).__init__() super(AuthoritiesList, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, authorities_output_schema)
def get(self): def get(self):
""" """
.. http:get:: /authorities .. http:get:: /authorities
@ -66,28 +53,52 @@ class AuthoritiesList(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"items": [ "items": [{
{ "name": "TestAuthority",
"id": 1, "roles": [{
"name": "authority1", "id": 123,
"description": "this is authority1", "name": "secure@example.com"
"pluginName": null, }, {
"chain": "-----Begin ...", "id": 564,
"body": "-----Begin ...", "name": "TestAuthority_admin"
"active": true, }, {
"notBefore": "2015-06-05T17:09:39", "id": 565,
"notAfter": "2015-06-10T17:09:39" "name": "TestAuthority_operator"
"options": null }],
} "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 "total": 1
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair. format is k;v
:query limit: limit number. default is 10 :query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
@ -96,10 +107,11 @@ class AuthoritiesList(AuthenticatedResource):
""" """
parser = paginated_parser.copy() parser = paginated_parser.copy()
args = parser.parse_args() args = parser.parse_args()
args['user'] = g.current_user
return service.render(args) return service.render(args)
@marshal_items(FIELDS) @validate_schema(authority_input_schema, authority_output_schema)
def post(self): def post(self, data=None):
""" """
.. http:post:: /authorities .. http:post:: /authorities
@ -113,31 +125,30 @@ class AuthoritiesList(AuthenticatedResource):
Host: example.com Host: example.com
Accept: application/json, text/javascript Accept: application/json, text/javascript
{ {
"caDN": { "country": "US",
"country": "US", "state": "California",
"state": "CA", "location": "Los Gatos",
"location": "A Location", "organization": "Netflix",
"organization": "ExampleInc", "organizationalUnit": "Operations",
"organizationalUnit": "Operations", "type": "root",
"commonName": "a common name" "signingAlgorithm": "sha256WithRSA",
}, "sensitivity": "medium",
"caType": "root",
"caSigningAlgo": "sha256WithRSA",
"caSensitivity": "medium",
"keyType": "RSA2048", "keyType": "RSA2048",
"pluginName": "cloudca", "plugin": {
"validityStart": "2015-06-11T07:00:00.000Z", "slug": "cloudca-issuer",
"validityEnd": "2015-06-13T07:00:00.000Z",
"caName": "DoctestCA",
"ownerEmail": "jimbob@example.com",
"caDescription": "Example CA",
"extensions": {
"subAltNames": {
"names": []
}
}, },
} "name": "TimeTestAuthority5",
"owner": "secure@example.com",
"description": "test",
"commonName": "AcommonName",
"validityYears": "20",
"extensions": {
"subAltNames": {
"names": []
},
"custom": []
}
**Example response**: **Example response**:
@ -148,57 +159,68 @@ class AuthoritiesList(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "name": "TestAuthority",
"name": "authority1", "roles": [{
"description": "this is authority1", "id": 123,
"pluginName": null, "name": "secure@example.com"
"chain": "-----Begin ...", }, {
"body": "-----Begin ...", "id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"options": null,
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "authorityCertificate": {
"notAfter": "2015-06-10T17:09:39" "body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
"options": null "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 name: authority's name
:arg ownerEmail: the team or person who 'owns' this 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 validityStart: when this authority should start issuing certificates :arg validityStart: when this authority should start issuing certificates
:arg validityEnd: when this authority should stop 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 extensions: certificate extensions
:arg pluginName: name of the plugin to create the authority :arg plugin: name of the plugin to create the authority
:arg caType: the type of authority (root/subca) :arg type: the type of authority (root/subca)
:arg caParent: the parent authority if this is to be a subca :arg parent: the parent authority if this is to be a subca
:arg caSigningAlgo: algorithm used to sign the authority :arg signingAlgorithm: algorithm used to sign the authority
:arg keyType: key type :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 in an HSM
:arg caKeyName: name of the key to store in the HSM (CloudCA) :arg keyName: name of the key to store in the HSM (CloudCA)
:arg caSerialNumber: serial number of the authority :arg serialNumber: serial number of the authority
:arg caFirstSerial: specifies the starting serial number for certificates issued off of this authority :arg firstSerial: specifies the starting serial number for certificates issued off of this authority
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
:statuscode 200: no error :statuscode 200: no error
""" """
self.reqparse.add_argument('caName', type=str, location='json', required=True) data['creator'] = g.current_user
self.reqparse.add_argument('caDescription', type=str, location='json', required=False) return service.create(**data)
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)
class Authorities(AuthenticatedResource): class Authorities(AuthenticatedResource):
@ -206,7 +228,7 @@ class Authorities(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(Authorities, self).__init__() super(Authorities, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, authority_output_schema)
def get(self, authority_id): def get(self, authority_id):
""" """
.. http:get:: /authorities/1 .. http:get:: /authorities/1
@ -230,30 +252,40 @@ class Authorities(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "roles": [{
"name": "authority1", "id": 123,
"description": "this is authority1", "name": "secure@example.com"
"pluginName": null, }, {
"chain": "-----Begin ...", "id": 564,
"body": "-----Begin ...", "name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "owner": "secure@example.com",
"notAfter": "2015-06-10T17:09:39" "id": 43,
"options": null "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 :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
return service.get(authority_id) return service.get(authority_id)
@marshal_items(FIELDS) @validate_schema(authority_update_schema, authority_output_schema)
def put(self, authority_id): def put(self, authority_id, data=None):
""" """
.. http:put:: /authorities/1 .. http:put:: /authorities/1
Update a authority Update an authority
**Example request**: **Example request**:
@ -264,11 +296,42 @@ class Authorities(AuthenticatedResource):
Accept: application/json, text/javascript Accept: application/json, text/javascript
{ {
"roles": [], "name": "TestAuthority5",
"active": false, "roles": [{
"owner": "bob@example.com", "id": 566,
"description": "this is authority1" "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**: **Example response**:
@ -279,64 +342,74 @@ class Authorities(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "name": "TestAuthority",
"name": "authority1", "roles": [{
"description": "this is authority1", "id": 123,
"pluginName": null, "name": "secure@example.com"
"chain": "-----begin ...", }, {
"body": "-----begin ...", "id": 564,
"active": false, "name": "TestAuthority_admin"
"notBefore": "2015-06-05t17:09:39", }, {
"notAfter": "2015-06-10t17:09:39" "id": 565,
"options": null "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 :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
self.reqparse.add_argument('roles', type=list, default=[], location='json')
self.reqparse.add_argument('active', type=str, location='json', required=True)
self.reqparse.add_argument('owner', type=str, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json', required=True)
args = self.reqparse.parse_args()
authority = service.get(authority_id) 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 # all the authority role members should be allowed
roles = [x.name for x in authority.roles] roles = [x.name for x in authority.roles]
# allow "owner" roles by team DL
roles.append(role)
permission = AuthorityPermission(authority_id, roles) 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(): if permission.can():
return service.update( return service.update(
authority_id, authority_id,
owner=args['owner'], owner=data['owner'],
description=args['description'], description=data['description'],
active=args['active'], active=data['active'],
roles=args['roles'] 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): class CertificateAuthority(AuthenticatedResource):
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificateAuthority, self).__init__() super(CertificateAuthority, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, authority_output_schema)
def get(self, certificate_id): def get(self, certificate_id):
""" """
.. http:get:: /certificates/1/authority .. http:get:: /certificates/1/authority
@ -360,16 +433,42 @@ class CertificateAuthority(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "name": "TestAuthority",
"name": "authority1", "roles": [{
"description": "this is authority1", "id": 123,
"pluginName": null, "name": "secure@example.com"
"chain": "-----Begin ...", }, {
"body": "-----Begin ...", "id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"options": null,
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "authorityCertificate": {
"notAfter": "2015-06-10T17:09:39" "body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
"options": null "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 :reqheader Authorization: OAuth token to authenticate
@ -378,10 +477,36 @@ class CertificateAuthority(AuthenticatedResource):
""" """
cert = certificate_service.get(certificate_id) cert = certificate_service.get(certificate_id)
if not cert: if not cert:
return dict(message="Certificate not found"), 404 return dict(message="Certificate not found."), 404
return cert.authority 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(AuthoritiesList, '/authorities', endpoint='authorities')
api.add_resource(Authorities, '/authorities/<int:authority_id>', endpoint='authority') 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') api.add_resource(CertificateAuthority, '/certificates/<int:certificate_id>/authority', endpoint='certificateAuthority')

View File

View File

@ -0,0 +1,34 @@
"""
.. module: lemur.authorizations.models
:platform: unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Netflix Secops <secops@netflix.com>
"""
from sqlalchemy import Column, Integer, String
from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
class Authorization(db.Model):
__tablename__ = 'pending_dns_authorizations'
id = Column(Integer, primary_key=True, autoincrement=True)
account_number = Column(String(128))
domains = Column(JSONType)
dns_provider_type = Column(String(128))
options = Column(JSONType)
@property
def plugin(self):
return plugins.get(self.plugin_name)
def __repr__(self):
return "Authorization(id={id})".format(label=self.id)
def __init__(self, account_number, domains, dns_provider_type, options=None):
self.account_number = account_number
self.domains = domains
self.dns_provider_type = dns_provider_type
self.options = options

View File

@ -0,0 +1,24 @@
"""
.. module: lemur.pending_certificates.service
Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved.
.. moduleauthor:: Secops <secops@netflix.com>
"""
from lemur import database
from lemur.authorizations.models import Authorization
def get(authorization_id):
"""
Retrieve dns authorization by ID
"""
return database.get(Authorization, authorization_id)
def create(account_number, domains, dns_provider_type, options=None):
"""
Creates a new dns authorization.
"""
authorization = Authorization(account_number, domains, dns_provider_type, options)
return database.create(authorization)

373
lemur/certificates/cli.py Normal file
View File

@ -0,0 +1,373 @@
"""
.. module: lemur.certificate.cli
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import sys
import multiprocessing
from tabulate import tabulate
from sqlalchemy import or_
from flask import current_app
from flask_script import Manager
from flask_principal import Identity, identity_changed
from lemur import database
from lemur.extensions import sentry
from lemur.extensions import metrics
from lemur.plugins.base import plugins
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
from lemur.deployment import service as deployment_service
from lemur.endpoints import service as endpoint_service
from lemur.notifications.messaging import send_rotation_notification
from lemur.domains.models import Domain
from lemur.authorities.models import Authority
from lemur.certificates.schemas import CertificateOutputSchema
from lemur.certificates.models import Certificate
from lemur.certificates.service import (
reissue_certificate,
get_certificate_primitives,
get_all_pending_reissue,
get_by_name,
get_all_certs,
get
)
from lemur.certificates.verify import verify_string
manager = Manager(usage="Handles all certificate related tasks.")
def print_certificate_details(details):
"""
Print the certificate details with formatting.
:param details:
:return:
"""
details, errors = CertificateOutputSchema().dump(details)
print("[+] Re-issuing certificate with the following details: ")
print(
"\t[+] Common Name: {common_name}\n"
"\t[+] Subject Alternate Names: {sans}\n"
"\t[+] Authority: {authority_name}\n"
"\t[+] Validity Start: {validity_start}\n"
"\t[+] Validity End: {validity_end}\n".format(
common_name=details['commonName'],
sans=",".join(x['value'] for x in details['extensions']['subAltNames']['names']) or None,
authority_name=details['authority']['name'],
validity_start=details['validityStart'],
validity_end=details['validityEnd']
)
)
def validate_certificate(certificate_name):
"""
Ensuring that the specified certificate exists.
:param certificate_name:
:return:
"""
if certificate_name:
cert = get_by_name(certificate_name)
if not cert:
print("[-] No certificate found with name: {0}".format(certificate_name))
sys.exit(1)
return cert
def validate_endpoint(endpoint_name):
"""
Ensuring that the specified endpoint exists.
:param endpoint_name:
:return:
"""
if endpoint_name:
endpoint = endpoint_service.get_by_name(endpoint_name)
if not endpoint:
print("[-] No endpoint found with name: {0}".format(endpoint_name))
sys.exit(1)
return endpoint
def request_rotation(endpoint, certificate, message, commit):
"""
Rotates a certificate and handles any exceptions during
execution.
:param endpoint:
:param certificate:
:param message:
:param commit:
:return:
"""
status = FAILURE_METRIC_STATUS
if commit:
try:
deployment_service.rotate_certificate(endpoint, certificate)
if message:
send_rotation_notification(certificate)
status = SUCCESS_METRIC_STATUS
except Exception as e:
print(
"[!] Failed to rotate endpoint {0} to certificate {1} reason: {2}".format(
endpoint.name,
certificate.name,
e
)
)
metrics.send('endpoint_rotation', 'counter', 1, metric_tags={'status': status})
def request_reissue(certificate, commit):
"""
Reissuing certificate and handles any exceptions.
:param certificate:
:param commit:
:return:
"""
status = FAILURE_METRIC_STATUS
try:
print("[+] {0} is eligible for re-issuance".format(certificate.name))
# set the lemur identity for all cli commands
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
details = get_certificate_primitives(certificate)
print_certificate_details(details)
if commit:
new_cert = reissue_certificate(certificate, replace=True)
print("[+] New certificate named: {0}".format(new_cert.name))
status = SUCCESS_METRIC_STATUS
except Exception as e:
sentry.captureException()
current_app.logger.exception("Error reissuing certificate.", exc_info=True)
print(
"[!] Failed to reissue certificates. Reason: {}".format(
e
)
)
metrics.send('certificate_reissue', 'counter', 1, metric_tags={'status': status})
@manager.option('-e', '--endpoint', dest='endpoint_name', help='Name of the endpoint you wish to rotate.')
@manager.option('-n', '--new-certificate', dest='new_certificate_name', help='Name of the certificate you wish to rotate to.')
@manager.option('-o', '--old-certificate', dest='old_certificate_name', help='Name of the certificate you wish to rotate.')
@manager.option('-a', '--notify', dest='message', action='store_true', help='Send a rotation notification to the certificates owner.')
@manager.option('-c', '--commit', dest='commit', action='store_true', default=False, help='Persist changes.')
def rotate(endpoint_name, new_certificate_name, old_certificate_name, message, commit):
"""
Rotates an endpoint and reissues it if it has not already been replaced. If it has
been replaced, will use the replacement certificate for the rotation.
"""
if commit:
print("[!] Running in COMMIT mode.")
print("[+] Starting endpoint rotation.")
status = FAILURE_METRIC_STATUS
try:
old_cert = validate_certificate(old_certificate_name)
new_cert = validate_certificate(new_certificate_name)
endpoint = validate_endpoint(endpoint_name)
if endpoint and new_cert:
print("[+] Rotating endpoint: {0} to certificate {1}".format(endpoint.name, new_cert.name))
request_rotation(endpoint, new_cert, message, commit)
elif old_cert and new_cert:
print("[+] Rotating all endpoints from {0} to {1}".format(old_cert.name, new_cert.name))
for endpoint in old_cert.endpoints:
print("[+] Rotating {0}".format(endpoint.name))
request_rotation(endpoint, new_cert, message, commit)
else:
print("[+] Rotating all endpoints that have new certificates available")
for endpoint in endpoint_service.get_all_pending_rotation():
if len(endpoint.certificate.replaced) == 1:
print("[+] Rotating {0} to {1}".format(endpoint.name, endpoint.certificate.replaced[0].name))
request_rotation(endpoint, endpoint.certificate.replaced[0], message, commit)
else:
metrics.send('endpoint_rotation', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
print("[!] Failed to rotate endpoint {0} reason: Multiple replacement certificates found.".format(
endpoint.name
))
status = SUCCESS_METRIC_STATUS
print("[+] Done!")
except Exception as e:
sentry.captureException()
metrics.send('endpoint_rotation_job', 'counter', 1, metric_tags={'status': status})
@manager.option('-o', '--old-certificate', dest='old_certificate_name', help='Name of the certificate you wish to reissue.')
@manager.option('-c', '--commit', dest='commit', action='store_true', default=False, help='Persist changes.')
def reissue(old_certificate_name, commit):
"""
Reissues certificate with the same parameters as it was originally issued with.
If not time period is provided, reissues certificate as valid from today to
today + length of original.
"""
if commit:
print("[!] Running in COMMIT mode.")
print("[+] Starting certificate re-issuance.")
status = FAILURE_METRIC_STATUS
try:
old_cert = validate_certificate(old_certificate_name)
if not old_cert:
for certificate in get_all_pending_reissue():
request_reissue(certificate, commit)
else:
request_reissue(old_cert, commit)
status = SUCCESS_METRIC_STATUS
print("[+] Done!")
except Exception as e:
sentry.captureException()
current_app.logger.exception("Error reissuing certificate.", exc_info=True)
print(
"[!] Failed to reissue certificates. Reason: {}".format(
e
)
)
metrics.send('certificate_reissue_job', 'counter', 1, metric_tags={'status': status})
@manager.option('-f', '--fqdns', dest='fqdns', help='FQDNs to query. Multiple fqdns specified via comma.')
@manager.option('-i', '--issuer', dest='issuer', help='Issuer to query for.')
@manager.option('-o', '--owner', dest='owner', help='Owner to query for.')
@manager.option('-e', '--expired', dest='expired', type=bool, default=False, help='Include expired certificates.')
def query(fqdns, issuer, owner, expired):
"""Prints certificates that match the query params."""
table = []
q = database.session_query(Certificate)
sub_query = database.session_query(Authority.id) \
.filter(Authority.name.ilike('%{0}%'.format(issuer))) \
.subquery()
q = q.filter(
or_(
Certificate.issuer.ilike('%{0}%'.format(issuer)),
Certificate.authority_id.in_(sub_query)
)
)
q = q.filter(Certificate.owner.ilike('%{0}%'.format(owner)))
if not expired:
q = q.filter(Certificate.expired == False) # noqa
for f in fqdns.split(','):
q = q.filter(
or_(
Certificate.cn.ilike('%{0}%'.format(f)),
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(f)))
)
)
for c in q.all():
table.append([c.id, c.name, c.owner, c.issuer])
print(tabulate(table, headers=['Id', 'Name', 'Owner', 'Issuer'], tablefmt='csv'))
def worker(data, commit, reason):
parts = [x for x in data.split(' ') if x]
try:
cert = get(int(parts[0].strip()))
plugin = plugins.get(cert.authority.plugin_name)
print('[+] Revoking certificate. Id: {0} Name: {1}'.format(cert.id, cert.name))
if commit:
plugin.revoke_certificate(cert, reason)
metrics.send('certificate_revoke', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
except Exception as e:
sentry.captureException()
metrics.send('certificate_revoke', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
print(
"[!] Failed to revoke certificates. Reason: {}".format(
e
)
)
@manager.command
def clear_pending():
"""
Function clears all pending certificates.
:return:
"""
v = plugins.get('verisign-issuer')
v.clear_pending_certificates()
@manager.option('-p', '--path', dest='path', help='Absolute file path to a Lemur query csv.')
@manager.option('-r', '--reason', dest='reason', help='Reason to revoke certificate.')
@manager.option('-c', '--commit', dest='commit', action='store_true', default=False, help='Persist changes.')
def revoke(path, reason, commit):
"""
Revokes given certificate.
"""
if commit:
print("[!] Running in COMMIT mode.")
print("[+] Starting certificate revocation.")
with open(path, 'r') as f:
args = [[x, commit, reason] for x in f.readlines()[2:]]
with multiprocessing.Pool(processes=3) as pool:
pool.starmap(worker, args)
@manager.command
def check_revoked():
"""
Function attempts to update Lemur's internal cache with revoked
certificates. This is called periodically by Lemur. It checks both
CRLs and OCSP to see if a certificate is revoked. If Lemur is unable
encounters an issue with verification it marks the certificate status
as `unknown`.
"""
for cert in get_all_certs():
try:
if cert.chain:
status = verify_string(cert.body, cert.chain)
else:
status = verify_string(cert.body, "")
cert.status = 'valid' if status else 'revoked'
except Exception as e:
sentry.captureException()
current_app.logger.exception(e)
cert.status = 'unknown'
database.update(cert)

View File

@ -1,88 +0,0 @@
"""
.. module: lemur.certificates.exceptions
:synopsis: Defines all monterey specific exceptions
: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 lemur.exceptions import LemurException
class UnknownAuthority(LemurException):
def __init__(self, authority):
self.code = 404
self.authority = authority
self.data = {"message": "The authority specified '{}' is not a valid authority".format(self.authority)}
current_app.logger.warning(self)
def __str__(self):
return repr(self.data['message'])
class InsufficientDomains(LemurException):
def __init__(self):
self.code = 400
self.data = {"message": "Need at least one domain specified in order create a certificate"}
current_app.logger.warning(self)
def __str__(self):
return repr(self.data['message'])
class InvalidCertificate(LemurException):
def __init__(self):
self.code = 400
self.data = {"message": "Need at least one domain specified in order create a certificate"}
current_app.logger.warning(self)
def __str__(self):
return repr(self.data['message'])
class UnableToCreateCSR(LemurException):
def __init__(self):
self.code = 500
self.data = {"message": "Unable to generate CSR"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])
class UnableToCreatePrivateKey(LemurException):
def __init__(self):
self.code = 500
self.data = {"message": "Unable to generate Private Key"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])
class MissingFiles(LemurException):
def __init__(self, path):
self.code = 500
self.path = path
self.data = {"path": self.path, "message": "Expecting missing files"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])
class NoPersistanceFound(LemurException):
def __init__(self):
self.code = 500
self.data = {"code": 500, "message": "No peristence method found, Lemur cannot persist sensitive information"}
current_app.logger.error(self)
def __str__(self):
return repr(self.data['message'])

View File

@ -0,0 +1,38 @@
"""
Debugging hooks for dumping imported or generated CSR and certificate details to stdout via OpenSSL.
.. module: lemur.certificates.hooks
:platform: Unix
:copyright: (c) 2018 by Marti Raudsepp, see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Marti Raudsepp <marti@juffo.org>
"""
import subprocess
from flask import current_app
from lemur.certificates.service import csr_created, csr_imported, certificate_issued, certificate_imported
def csr_dump_handler(sender, csr, **kwargs):
try:
subprocess.run(['openssl', 'req', '-text', '-noout', '-reqopt', 'no_sigdump,no_pubkey'],
input=csr.encode('utf8'))
except Exception as err:
current_app.logger.warning("Error inspecting CSR: %s", err)
def cert_dump_handler(sender, certificate, **kwargs):
try:
subprocess.run(['openssl', 'x509', '-text', '-noout', '-certopt', 'no_sigdump,no_pubkey'],
input=certificate.body.encode('utf8'))
except Exception as err:
current_app.logger.warning("Error inspecting certificate: %s", err)
def activate_debug_dump():
csr_created.connect(csr_dump_handler)
csr_imported.connect(csr_dump_handler)
certificate_issued.connect(cert_dump_handler)
certificate_imported.connect(cert_dump_handler)

View File

@ -1,301 +1,364 @@
""" """
.. module: lemur.certificates.models .. module: lemur.certificates.models
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import datetime import arrow
from datetime import timedelta
from flask import current_app from flask import current_app
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa
from idna.core import InvalidCodepoint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean from sqlalchemy.sql.expression import case, extract
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import event, Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils.types.arrow import ArrowType
import lemur.common.utils
from lemur.database import db
from lemur.extensions import sentry
from lemur.utils import Vault from lemur.utils import Vault
from lemur.database import db from lemur.common import defaults
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
from lemur.domains.models import Domain from lemur.extensions import metrics
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
from lemur.models import certificate_associations, certificate_source_associations, \ from lemur.models import certificate_associations, certificate_source_associations, \
certificate_destination_associations, certificate_notification_associations, \ certificate_destination_associations, certificate_notification_associations, \
certificate_replacement_associations certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations
from lemur.domains.models import Domain
from lemur.policies.models import RotationPolicy
def create_name(issuer, not_before, not_after, subject, san): def get_sequence(name):
""" if '-' not in name:
Create a name for our certificate. A naming standard return name, None
is based on a series of templates. The name includes
useful information such as Common Name, Validation dates,
and Issuer.
:param san: parts = name.split('-')
:param subject:
: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( # see if we have an int at the end of our name
subject=subject,
issuer=issuer,
not_before=not_before.strftime('%Y%m%d'),
not_after=not_after.strftime('%Y%m%d')
)
# NOTE we may want to give more control over naming
# aws doesn't allow special chars except '-'
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
disallowed_chars = disallowed_chars.replace("-", "")
disallowed_chars = disallowed_chars.replace(".", "")
temp = temp.replace('*', "WILDCARD")
for c in disallowed_chars:
temp = temp.replace(c, "")
# white space is silly too
return temp.replace(" ", "-")
def get_signing_algorithm(cert):
return cert.signature_hash_algorithm.name
def get_cn(cert):
"""
Attempts to get a sane common name from a given certificate.
:param cert:
:return: Common name or None
"""
return cert.subject.get_attributes_for_oid(
x509.OID_COMMON_NAME
)[0].value.strip()
def get_domains(cert):
"""
Attempts to get an domains listed in a certificate.
If 'subjectAltName' extension is not available we simply
return the common name.
:param cert:
:return: List of domains
"""
domains = []
try: try:
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME) seq = int(parts[-1])
entries = ext.value.get_values_for_type(x509.DNSName) except ValueError:
for entry in entries: return name, None
domains.append(entry)
except Exception as e:
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
return domains # we might have a date at the end of our name
if len(parts[-1]) == 8:
return name, None
root = '-'.join(parts[:-1])
return root, seq
def get_serial(cert): def get_or_increase_name(name, serial):
""" certificates = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).all()
Fetch the serial number from the certificate.
:param cert: if not certificates:
:return: serial number return name
"""
return cert.serial
serial_name = '{0}-{1}'.format(name, hex(int(serial))[2:].upper())
certificates = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(serial_name))).all()
def is_san(cert): if not certificates:
""" return serial_name
Determines if a given certificate is a SAN certificate.
SAN certificates are simply certificates that cover multiple domains.
:param cert: ends = [0]
:return: Bool root, end = get_sequence(serial_name)
""" for cert in certificates:
if len(get_domains(cert)) > 1: root, end = get_sequence(cert.name)
return True if end:
ends.append(end)
return '{0}-{1}'.format(root, max(ends) + 1)
def is_wildcard(cert):
"""
Determines if certificate is a wildcard certificate.
:param cert:
:return: Bool
"""
domains = get_domains(cert)
if len(domains) == 1 and domains[0][0:1] == "*":
return True
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
return True
def get_bitstrength(cert):
"""
Calculates a certificates public key bit length.
:param cert:
:return: Integer
"""
return cert.public_key().key_size
def get_issuer(cert):
"""
Gets a sane issuer from a given certificate.
:param cert:
:return: Issuer
"""
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
try:
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
for c in delchars:
issuer = issuer.replace(c, "")
return issuer
except Exception as e:
current_app.logger.error("Unable to get issuer! {0}".format(e))
def get_not_before(cert):
"""
Gets the naive datetime of the certificates 'not_before' field.
This field denotes the first date in time which the given certificate
is valid.
:param cert:
:return: Datetime
"""
return cert.not_valid_before
def get_not_after(cert):
"""
Gets the naive datetime of the certificates 'not_after' field.
This field denotes the last date in time which the given certificate
is valid.
:param cert:
:return: Datetime
"""
return cert.not_valid_after
def get_name_from_arn(arn):
"""
Extract the certificate name from an arn.
:param arn: IAM SSL arn
:return: name of the certificate as uploaded to AWS
"""
return arn.split("/", 1)[1]
def get_account_number(arn):
"""
Extract the account number from an arn.
:param arn: IAM SSL arn
:return: account number associated with ARN
"""
return arn.split(":")[4]
class Certificate(db.Model): class Certificate(db.Model):
__tablename__ = 'certificates' __tablename__ = 'certificates'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
owner = Column(String(128)) external_id = Column(String(128))
body = Column(Text()) owner = Column(String(128), nullable=False)
private_key = Column(Vault) name = Column(String(256), unique=True)
status = Column(String(128)) description = Column(String(1024))
deleted = Column(Boolean, index=True) notify = Column(Boolean, default=True)
name = Column(String(128))
body = Column(Text(), nullable=False)
chain = Column(Text()) chain = Column(Text())
bits = Column(Integer()) private_key = Column(Vault)
issuer = Column(String(128)) issuer = Column(String(128))
serial = Column(String(128)) serial = Column(String(128))
cn = Column(String(128)) cn = Column(String(128))
description = Column(String(1024)) deleted = Column(Boolean, index=True)
active = Column(Boolean, default=True) dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True)
san = Column(String(1024))
not_before = Column(DateTime) not_before = Column(ArrowType)
not_after = Column(DateTime) not_after = Column(ArrowType)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)
signing_algorithm = Column(String(128)) signing_algorithm = Column(String(128))
status = Column(String(128))
bits = Column(Integer())
san = Column(String(1024)) # TODO this should be migrated to boolean
rotation = Column(Boolean, default=False)
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id')) authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate') root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate') rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))
replaces = relationship("Certificate",
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, secondary=certificate_replacement_associations,
primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
backref='replaced') backref='replaced')
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
def __init__(self, body, private_key=None, chain=None): replaced_by_pending = relationship('PendingCertificate',
self.body = body secondary=pending_cert_replacement_associations,
# We encrypt the private_key on creation backref='pending_replace',
self.private_key = private_key viewonly=True)
self.chain = chain
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
self.signing_algorithm = get_signing_algorithm(cert)
self.bits = get_bitstrength(cert)
self.issuer = get_issuer(cert)
self.serial = get_serial(cert)
self.cn = get_cn(cert)
self.san = is_san(cert)
self.not_before = get_not_before(cert)
self.not_after = get_not_after(cert)
self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san)
for domain in get_domains(cert): logs = relationship('Log', backref='certificate')
endpoints = relationship('Endpoint', backref='certificate')
rotation_policy = relationship("RotationPolicy")
sensitive_fields = ('private_key',)
def __init__(self, **kwargs):
cert = lemur.common.utils.parse_certificate(kwargs['body'])
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)
self.serial = defaults.serial(cert)
# when destinations are appended they require a valid name.
if kwargs.get('name'):
self.name = get_or_increase_name(defaults.text_to_slug(kwargs['name']), self.serial)
else:
self.name = get_or_increase_name(
defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san), self.serial)
self.owner = kwargs['owner']
self.body = kwargs['body'].strip()
if kwargs.get('private_key'):
self.private_key = kwargs['private_key'].strip()
if kwargs.get('chain'):
self.chain = kwargs['chain'].strip()
self.notify = kwargs.get('notify', True)
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('replaces', [])
self.rotation = kwargs.get('rotation')
self.rotation_policy = kwargs.get('rotation_policy')
self.signing_algorithm = defaults.signing_algorithm(cert)
self.bits = defaults.bitstrength(cert)
self.external_id = kwargs.get('external_id')
self.authority_id = kwargs.get('authority_id')
self.dns_provider_id = kwargs.get('dns_provider_id')
for domain in defaults.domains(cert):
self.domains.append(Domain(name=domain)) self.domains.append(Domain(name=domain))
@property @property
def is_expired(self): def active(self):
if self.not_after < datetime.datetime.now(): return self.notify
return True
@property @property
def is_unused(self): def organization(self):
if self.elb_listeners.count() == 0: cert = lemur.common.utils.parse_certificate(self.body)
return True return defaults.organization(cert)
@property @property
def is_revoked(self): def organizational_unit(self):
# we might not yet know the condition of the cert cert = lemur.common.utils.parse_certificate(self.body)
if self.status: return defaults.organizational_unit(cert)
if 'revoked' in self.status:
return True
def get_arn(self, account_number): @property
def country(self):
cert = lemur.common.utils.parse_certificate(self.body)
return defaults.country(cert)
@property
def state(self):
cert = lemur.common.utils.parse_certificate(self.body)
return defaults.state(cert)
@property
def location(self):
cert = lemur.common.utils.parse_certificate(self.body)
return defaults.location(cert)
@property
def key_type(self):
cert = lemur.common.utils.parse_certificate(self.body)
if isinstance(cert.public_key(), rsa.RSAPublicKey):
return 'RSA{key_size}'.format(key_size=cert.public_key().key_size)
@property
def validity_remaining(self):
return abs(self.not_after - arrow.utcnow())
@property
def validity_range(self):
return self.not_after - self.not_before
@property
def subject(self):
cert = lemur.common.utils.parse_certificate(self.body)
return cert.subject
@property
def public_key(self):
cert = lemur.common.utils.parse_certificate(self.body)
return cert.public_key()
@hybrid_property
def expired(self):
if self.not_after <= arrow.utcnow():
return True
@expired.expression
def expired(cls):
return case(
[
(cls.not_after <= arrow.utcnow(), True)
],
else_=False
)
@hybrid_property
def revoked(self):
if 'revoked' == self.status:
return True
@revoked.expression
def revoked(cls):
return case(
[
(cls.status == 'revoked', True)
],
else_=False
)
@hybrid_property
def in_rotation_window(self):
""" """
Generate a valid AWS IAM arn Determines if a certificate is available for rotation based
on the rotation policy associated.
:rtype : str
:param account_number:
:return: :return:
""" """
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name) now = arrow.utcnow()
end = now + timedelta(days=self.rotation_policy.days)
if self.not_after <= end:
return True
@in_rotation_window.expression
def in_rotation_window(cls):
"""
Determines if a certificate is available for rotation based
on the rotation policy associated.
:return:
"""
return case(
[
(extract('day', cls.not_after - func.now()) <= RotationPolicy.days, True)
],
else_=False
)
@property
def extensions(self):
# setup default values
return_extensions = {
'sub_alt_names': {'names': []}
}
try:
cert = lemur.common.utils.parse_certificate(self.body)
for extension in cert.extensions:
value = extension.value
if isinstance(value, x509.BasicConstraints):
return_extensions['basic_constraints'] = value
elif isinstance(value, x509.SubjectAlternativeName):
return_extensions['sub_alt_names']['names'] = value
elif isinstance(value, x509.ExtendedKeyUsage):
return_extensions['extended_key_usage'] = value
elif isinstance(value, x509.KeyUsage):
return_extensions['key_usage'] = value
elif isinstance(value, x509.SubjectKeyIdentifier):
return_extensions['subject_key_identifier'] = {'include_ski': True}
elif isinstance(value, x509.AuthorityInformationAccess):
return_extensions['certificate_info_access'] = {'include_aia': True}
elif isinstance(value, x509.AuthorityKeyIdentifier):
aki = {
'use_key_identifier': False,
'use_authority_cert': False
}
if value.key_identifier:
aki['use_key_identifier'] = True
if value.authority_cert_issuer:
aki['use_authority_cert'] = True
return_extensions['authority_key_identifier'] = aki
elif isinstance(value, x509.CRLDistributionPoints):
return_extensions['crl_distribution_points'] = {'include_crl_dp': value}
# TODO: Not supporting custom OIDs yet. https://github.com/Netflix/lemur/issues/665
else:
current_app.logger.warning('Custom OIDs not yet supported for clone operation.')
except InvalidCodepoint as e:
sentry.captureException()
current_app.logger.warning('Unable to parse extensions due to underscore in dns name')
except ValueError as e:
sentry.captureException()
current_app.logger.warning('Unable to parse')
current_app.logger.exception(e)
return return_extensions
def __repr__(self):
return "Certificate(name={name})".format(name=self.name)
@event.listens_for(Certificate.destinations, 'append') @event.listens_for(Certificate.destinations, 'append')
def update_destinations(target, value, initiator): def update_destinations(target, value, initiator):
""" """
Attempt to upload the new certificate to the new destination Attempt to upload certificate to the new destination
:param target: :param target:
:param value: :param value:
@ -303,32 +366,26 @@ def update_destinations(target, value, initiator):
:return: :return:
""" """
destination_plugin = plugins.get(value.plugin_name) destination_plugin = plugins.get(value.plugin_name)
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options) status = FAILURE_METRIC_STATUS
try:
if target.private_key:
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
status = SUCCESS_METRIC_STATUS
except Exception as e:
sentry.captureException()
metrics.send('destination_upload', 'counter', 1,
metric_tags={'status': status, 'certificate': target.name, 'destination': value.label})
@event.listens_for(Certificate.replaces, 'append') @event.listens_for(Certificate.replaces, 'append')
def update_replacement(target, value, initiator): def update_replacement(target, value, initiator):
""" """
When a certificate is marked as 'replaced' it is then marked as in-active When a certificate is marked as 'replaced' we should not notify.
:param target: :param target:
:param value: :param value:
:param initiator: :param initiator:
:return: :return:
""" """
value.active = False value.notify = 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.")

View File

@ -0,0 +1,275 @@
"""
.. module: lemur.certificates.schemas
:platform: unix
:copyright: (c) 2018 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, validate, validates_schema, post_load, pre_load
from marshmallow.exceptions import ValidationError
from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.common import validators, missing
from lemur.common.fields import ArrowDateTime, Hex
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.constants import CERTIFICATE_KEY_TYPES
from lemur.destinations.schemas import DestinationNestedOutputSchema
from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.notifications import service as notification_service
from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
from lemur.roles.schemas import RoleNestedOutputSchema
from lemur.schemas import (
AssociatedAuthoritySchema,
AssociatedDestinationSchema,
AssociatedCertificateSchema,
AssociatedNotificationSchema,
AssociatedDnsProviderSchema,
PluginInputSchema,
ExtensionSchema,
AssociatedRoleSchema,
EndpointNestedOutputSchema,
AssociatedRotationPolicySchema,
)
from lemur.users.schemas import UserNestedOutputSchema
class CertificateSchema(LemurInputSchema):
owner = fields.Email(required=True)
description = fields.String(missing='', allow_none=True)
class CertificateCreationSchema(CertificateSchema):
@post_load
def default_notification(self, data):
if not data['notifications']:
data['notifications'] += notification_service.create_default_expiration_notifications(
"DEFAULT_{0}".format(data['owner'].split('@')[0].upper()),
[data['owner']],
)
data['notifications'] += notification_service.create_default_expiration_notifications(
'DEFAULT_SECURITY',
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
)
return data
class CertificateInputSchema(CertificateCreationSchema):
name = fields.String()
common_name = fields.String(required=True, validate=validators.common_name)
authority = fields.Nested(AssociatedAuthoritySchema, required=True)
validity_start = ArrowDateTime()
validity_end = ArrowDateTime()
validity_years = fields.Integer()
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) # deprecated
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
dns_provider = fields.Nested(AssociatedDnsProviderSchema, missing=None, allow_none=True, required=False)
csr = fields.String(validate=validators.csr)
key_type = fields.String(
validate=validate.OneOf(CERTIFICATE_KEY_TYPES),
missing='RSA2048')
notify = fields.Boolean(default=True)
rotation = fields.Boolean()
rotation_policy = fields.Nested(AssociatedRotationPolicySchema, missing={'name': 'default'}, default={'name': 'default'})
# 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_authority(self, data):
if not data['authority'].active:
raise ValidationError("The authority is inactive.", ['authority'])
@validates_schema
def validate_dates(self, data):
validators.dates(data)
@pre_load
def load_data(self, data):
if data.get('replacements'):
data['replaces'] = data['replacements'] # TODO remove when field is deprecated
return missing.convert_validity_years(data)
class CertificateEditInputSchema(CertificateSchema):
owner = fields.String()
notify = fields.Boolean()
rotation = fields.Boolean()
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) # deprecated
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
@pre_load
def load_data(self, data):
if data.get('replacements'):
data['replaces'] = data['replacements'] # TODO remove when field is deprecated
return data
@post_load
def enforce_notifications(self, data):
"""
Ensures that when an owner changes, default notifications are added for the new owner.
Old owner notifications are retained unless explicitly removed.
:param data:
:return:
"""
if data['owner']:
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
return data
class CertificateNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
name = fields.String()
owner = fields.Email()
creator = fields.Nested(UserNestedOutputSchema)
description = fields.String()
status = fields.String()
bits = fields.Integer()
body = fields.String()
chain = fields.String()
active = fields.Boolean()
rotation = fields.Boolean()
notify = fields.Boolean()
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
# Note aliasing is the first step in deprecating these fields.
cn = fields.String() # deprecated
common_name = fields.String(attribute='cn')
not_after = fields.DateTime() # deprecated
validity_end = ArrowDateTime(attribute='not_after')
not_before = fields.DateTime() # deprecated
validity_start = ArrowDateTime(attribute='not_before')
issuer = fields.Nested(AuthorityNestedOutputSchema)
class CertificateCloneSchema(LemurOutputSchema):
__envelope__ = False
description = fields.String()
common_name = fields.String()
class CertificateOutputSchema(LemurOutputSchema):
id = fields.Integer()
external_id = fields.String()
bits = fields.Integer()
body = fields.String()
chain = fields.String()
deleted = fields.Boolean(default=False)
description = fields.String()
issuer = fields.String()
name = fields.String()
dns_provider_id = fields.Integer(required=False, allow_none=True)
rotation = fields.Boolean()
# Note aliasing is the first step in deprecating these fields.
notify = fields.Boolean()
active = fields.Boolean(attribute='notify')
cn = fields.String()
common_name = fields.String(attribute='cn')
not_after = fields.DateTime()
validity_end = ArrowDateTime(attribute='not_after')
not_before = fields.DateTime()
validity_start = ArrowDateTime(attribute='not_before')
owner = fields.Email()
san = fields.Boolean()
serial = fields.String()
serial_hex = Hex(attribute='serial')
signing_algorithm = fields.String()
status = fields.String()
user = fields.Nested(UserNestedOutputSchema)
extensions = fields.Nested(ExtensionSchema)
# associated objects
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.Nested(EndpointNestedOutputSchema, many=True, missing=[])
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
class CertificateUploadInputSchema(CertificateCreationSchema):
name = fields.String()
notify = 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, missing=None, allow_none=True) # TODO this could be multiple certificates
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replaces = 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)
class CertificateNotificationOutputSchema(LemurOutputSchema):
description = fields.String()
issuer = fields.String()
name = fields.String()
owner = fields.Email()
user = fields.Nested(UserNestedOutputSchema)
validity_end = ArrowDateTime(attribute='not_after')
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
class CertificateRevokeSchema(LemurInputSchema):
comments = fields.String()
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()
certificate_notification_output_schema = CertificateNotificationOutputSchema()
certificate_revoke_schema = CertificateRevokeSchema()

View File

@ -1,35 +1,46 @@
""" """
.. module: service .. module: lemur.certificate.service
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import arrow import arrow
from sqlalchemy import func, or_ from flask import current_app
from flask import g, current_app from sqlalchemy import func, or_, not_, cast, Integer
from lemur import database
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 cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from lemur import database
from lemur.extensions import metrics, signals
from lemur.plugins.base import plugins
from lemur.common.utils import generate_private_key, truthiness
from lemur.roles.models import Role
from lemur.domains.models import Domain
from lemur.authorities.models import Authority
from lemur.destinations.models import Destination
from lemur.certificates.models import Certificate
from lemur.notifications.models import Notification
from lemur.pending_certificates.models import PendingCertificate
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
from lemur.roles import service as role_service
csr_created = signals.signal('csr_created', "CSR generated")
csr_imported = signals.signal('csr_imported', "CSR imported from external source")
certificate_issued = signals.signal('certificate_issued', "Authority issued a certificate")
certificate_imported = signals.signal('certificate_imported', "Certificate imported from external source")
def get(cert_id): def get(cert_id):
""" """
Retrieves certificate by it's ID. Retrieves certificate by its ID.
:param cert_id: :param cert_id:
:return: :return:
@ -39,7 +50,7 @@ def get(cert_id):
def get_by_name(name): def get_by_name(name):
""" """
Retrieves certificate by it's Name. Retrieves certificate by its Name.
:param name: :param name:
:return: :return:
@ -47,6 +58,18 @@ def get_by_name(name):
return database.get(Certificate, name, field='name') return database.get(Certificate, name, field='name')
def get_by_serial(serial):
"""
Retrieves certificate by it's Serial.
:param serial:
:return:
"""
if isinstance(serial, int):
# although serial is a number, the DB column is String(128)
serial = str(serial)
return Certificate.query.filter(Certificate.serial == serial).all()
def delete(cert_id): def delete(cert_id):
""" """
Delete's a certificate. Delete's a certificate.
@ -65,16 +88,45 @@ def get_all_certs():
return Certificate.query.all() return Certificate.query.all()
def find_duplicates(cert_body): def get_all_pending_cleaning(source):
"""
Retrieves all certificates that are available for cleaning.
:param source:
:return:
"""
return Certificate.query.filter(Certificate.sources.any(id=source.id))\
.filter(not_(Certificate.endpoints.any())).all()
def get_all_pending_reissue():
"""
Retrieves all certificates that need to be rotated.
Must be X days from expiration, uses the certificates rotation
policy to determine how many days from expiration the certificate must be
for rotation to be pending.
:return:
"""
return Certificate.query.filter(Certificate.rotation == True)\
.filter(not_(Certificate.replaced.any()))\
.filter(Certificate.in_rotation_window == True).all() # noqa
def find_duplicates(cert):
""" """
Finds certificates that already exist within Lemur. We do this by looking for Finds certificates that already exist within Lemur. We do this by looking for
certificate bodies that are the same. This is the most reliable way to determine certificate bodies that are the same. This is the most reliable way to determine
if a certificate is already being tracked by Lemur. if a certificate is already being tracked by Lemur.
:param cert_body: :param cert:
:return: :return:
""" """
return Certificate.query.filter_by(body=cert_body).all() if cert['chain']:
return Certificate.query.filter_by(body=cert['body'].strip(), chain=cert['chain'].strip()).all()
else:
return Certificate.query.filter_by(body=cert['body'].strip(), chain=None).all()
def export(cert, export_plugin): def export(cert, export_plugin):
@ -87,75 +139,62 @@ def export(cert, export_plugin):
:return: :return:
""" """
plugin = plugins.get(export_plugin['slug']) plugin = plugins.get(export_plugin['slug'])
return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions']) return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions'])
def update(cert_id, owner, description, active, destinations, notifications, replaces): def update(cert_id, **kwargs):
""" """
Updates a certificate Updates a certificate
:param cert_id: :param cert_id:
:param owner:
:param description:
:param active:
:param destinations:
:param notifications:
:param replaces:
:return: :return:
""" """
from lemur.notifications import service as notification_service
cert = get(cert_id) cert = get(cert_id)
cert.active = active
cert.description = description
# we might have to create new notifications if the owner changes for key, value in kwargs.items():
new_notifications = [] setattr(cert, key, value)
# get existing names to remove
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
for n in notifications:
if notification_name not in n.label:
new_notifications.append(n)
notification_name = "DEFAULT_{0}".format(owner.split('@')[0].upper())
new_notifications += notification_service.create_default_expiration_notifications(notification_name, owner)
cert.notifications = new_notifications
database.update_list(cert, 'destinations', Destination, destinations)
database.update_list(cert, 'replaces', Certificate, replaces)
cert.owner = owner
return database.update(cert) 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'])
)
# ensure that the authority's owner is also associated with the certificate
if kwargs.get('authority'):
authority_owner_role = role_service.get_by_name(kwargs['authority'].owner)
return [owner_role, authority_owner_role]
return [owner_role]
def mint(**kwargs):
""" """
Minting is slightly different for each authority. Minting is slightly different for each authority.
Support for multiple authorities is handled by individual plugins. 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) issuer = plugins.get(authority.plugin_name)
# allow the CSR to be specified by the user # allow the CSR to be specified by the user
if not issuer_options.get('csr'): if not kwargs.get('csr'):
csr, private_key = create_csr(issuer_options) csr, private_key = create_csr(**kwargs)
csr_created.send(authority=authority, csr=csr)
else: else:
csr = issuer_options.get('csr') csr = str(kwargs.get('csr'))
private_key = None private_key = None
csr_imported.send(authority=authority, csr=csr)
issuer_options['creator'] = g.user.email cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options) return cert_body, private_key, cert_chain, external_id, csr
cert = Certificate(cert_body, private_key, cert_chain)
cert.user = g.user
cert.authority = authority
database.update(cert)
return cert, private_key, cert_chain,
def import_certificate(**kwargs): def import_certificate(**kwargs):
@ -171,68 +210,36 @@ def import_certificate(**kwargs):
:param kwargs: :param kwargs:
""" """
from lemur.users import service as user_service if not kwargs.get('owner'):
from lemur.notifications import service as notification_service kwargs['owner'] = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0]
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 return upload(**kwargs)
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'))
# 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 kwargs.get('user'):
cert.user = kwargs.get('user')
notification_name = 'DEFAULT_SECURITY'
notifications = notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
cert.notifications = notifications
cert = database.create(cert)
return cert
def upload(**kwargs): def upload(**kwargs):
""" """
Allows for pre-made certificates to be imported into Lemur. Allows for pre-made certificates to be imported into Lemur.
""" """
from lemur.notifications import service as notification_service roles = create_certificate_roles(**kwargs)
cert = Certificate(
kwargs.get('public_cert'),
kwargs.get('private_key'),
kwargs.get('intermediate_cert'),
)
# we override the generated name if one is provided if kwargs.get('roles'):
if kwargs.get('name'): kwargs['roles'] += roles
cert.name = kwargs['name'] else:
kwargs['roles'] = roles
cert.description = kwargs.get('description') if kwargs.get('private_key'):
private_key = kwargs['private_key']
if not isinstance(private_key, bytes):
kwargs['private_key'] = private_key.encode('utf-8')
cert = Certificate(**kwargs)
cert.owner = kwargs['owner']
cert = database.create(cert) cert = database.create(cert)
g.user.certificates.append(cert) kwargs['creator'].certificates.append(cert)
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations')) cert = database.update(cert)
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications')) certificate_imported.send(certificate=cert, authority=cert.authority)
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
# create default notifications for this certificate if none are provided
notifications = []
if not kwargs.get('notifications'):
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
notification_name = 'DEFAULT_SECURITY'
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
cert.notifications = notifications
database.update(cert)
return cert return cert
@ -240,34 +247,34 @@ def create(**kwargs):
""" """
Creates a new certificate. Creates a new certificate.
""" """
from lemur.notifications import service as notification_service cert_body, private_key, cert_chain, external_id, csr = mint(**kwargs)
cert, private_key, cert_chain = mint(kwargs) kwargs['body'] = cert_body
kwargs['private_key'] = private_key
kwargs['chain'] = cert_chain
kwargs['external_id'] = external_id
kwargs['csr'] = csr
cert.owner = kwargs['owner'] roles = create_certificate_roles(**kwargs)
database.create(cert) if kwargs.get('roles'):
cert.description = kwargs['description'] kwargs['roles'] += roles
g.user.certificates.append(cert) else:
database.update(g.user) kwargs['roles'] = roles
# do this after the certificate has already been created because if it fails to upload to the third party if cert_body:
# we do not want to lose the certificate information. cert = Certificate(**kwargs)
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations')) kwargs['creator'].certificates.append(cert)
database.update_list(cert, 'replaces', Certificate, kwargs['replacements']) else:
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications')) cert = PendingCertificate(**kwargs)
kwargs['creator'].pending_certificates.append(cert)
# create default notifications for this certificate if none are provided cert.authority = kwargs['authority']
notifications = cert.notifications
if not kwargs.get('notifications'):
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
notification_name = 'DEFAULT_SECURITY' database.commit()
notifications += notification_service.create_default_expiration_notifications(notification_name,
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
cert.notifications = notifications
database.update(cert) if isinstance(cert, Certificate):
certificate_issued.send(certificate=cert, authority=cert.authority)
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
return cert return cert
@ -291,40 +298,55 @@ def render(args):
if filt: if filt:
terms = filt.split(';') terms = filt.split(';')
term = '%{0}%'.format(terms[1])
# Exact matches for quotes. Only applies to name, issuer, and cn
if terms[1].startswith('"') and terms[1].endswith('"'):
term = terms[1][1:-1]
if 'issuer' in terms: if 'issuer' in terms:
# we can't rely on issuer being correct in the cert directly so we combine queries # we can't rely on issuer being correct in the cert directly so we combine queries
sub_query = database.session_query(Authority.id)\ sub_query = database.session_query(Authority.id)\
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\ .filter(Authority.name.ilike(term))\
.subquery() .subquery()
query = query.filter( query = query.filter(
or_( or_(
Certificate.issuer.ilike('%{0}%'.format(terms[1])), Certificate.issuer.ilike(term),
Certificate.authority_id.in_(sub_query) Certificate.authority_id.in_(sub_query)
) )
) )
return database.sort_and_page(query, Certificate, args)
elif 'destination' in terms: elif 'destination' in terms:
query = query.filter(Certificate.destinations.any(Destination.id == terms[1])) 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?? elif 'notify' in filt:
query = query.filter(Certificate.active == terms[1]) query = query.filter(Certificate.notify == truthiness(terms[1]))
elif 'active' in filt:
query = query.filter(Certificate.active == truthiness(terms[1]))
elif 'cn' in terms: elif 'cn' in terms:
query = query.filter( query = query.filter(
or_( or_(
Certificate.cn.ilike('%{0}%'.format(terms[1])), Certificate.cn.ilike(term),
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1]))) Certificate.domains.any(Domain.name.ilike(term))
)
)
elif 'id' in terms:
query = query.filter(Certificate.id == cast(terms[1], Integer))
elif 'name' in terms:
query = query.filter(
or_(
Certificate.name.ilike(term),
Certificate.domains.any(Domain.name.ilike(term)),
Certificate.cn.ilike(term),
) )
) )
else: else:
query = database.filter(query, Certificate, terms) query = database.filter(query, Certificate, terms)
if show: if show:
sub_query = database.session_query(Role.name).filter(Role.user_id == g.user.id).subquery() sub_query = database.session_query(Role.name).filter(Role.user_id == args['user'].id).subquery()
query = query.filter( query = query.filter(
or_( or_(
Certificate.user_id == g.user.id, Certificate.user_id == args['user'].id,
Certificate.owner.in_(sub_query) Certificate.owner.in_(sub_query)
) )
) )
@ -343,106 +365,74 @@ def render(args):
return database.sort_and_page(query, Certificate, 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 Given a list of domains create the appropriate csr
for those domains for those domains
:param csr_config: :param csr_config:
""" """
private_key = rsa.generate_private_key( private_key = generate_private_key(csr_config.get('key_type'))
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# TODO When we figure out a better way to validate these options they should be parsed as str
builder = x509.CertificateSigningRequestBuilder() builder = x509.CertificateSigningRequestBuilder()
builder = builder.subject_name(x509.Name([ name_list = [x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['common_name'])]
x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['commonName']), if current_app.config.get('LEMUR_OWNER_EMAIL_IN_SUBJECT', True):
x509.NameAttribute(x509.OID_ORGANIZATION_NAME, csr_config['organization']), name_list.append(x509.NameAttribute(x509.OID_EMAIL_ADDRESS, csr_config['owner']))
x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizationalUnit']), if 'organization' in csr_config and csr_config['organization'].strip():
x509.NameAttribute(x509.OID_COUNTRY_NAME, csr_config['country']), name_list.append(x509.NameAttribute(x509.OID_ORGANIZATION_NAME, csr_config['organization']))
x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, csr_config['state']), if 'organizational_unit' in csr_config and csr_config['organizational_unit'].strip():
x509.NameAttribute(x509.OID_LOCALITY_NAME, csr_config['location']), name_list.append(x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizational_unit']))
])) if 'country' in csr_config and csr_config['country'].strip():
name_list.append(x509.NameAttribute(x509.OID_COUNTRY_NAME, csr_config['country']))
if 'state' in csr_config and csr_config['state'].strip():
name_list.append(x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, csr_config['state']))
if 'location' in csr_config and csr_config['location'].strip():
name_list.append(x509.NameAttribute(x509.OID_LOCALITY_NAME, csr_config['location']))
builder = builder.subject_name(x509.Name(name_list))
builder = builder.add_extension( extensions = csr_config.get('extensions', {})
x509.BasicConstraints(ca=False, path_length=None), critical=True, critical_extensions = ['basic_constraints', 'sub_alt_names', 'key_usage']
) noncritical_extensions = ['extended_key_usage']
for k, v in extensions.items():
if v:
if k in critical_extensions:
current_app.logger.debug('Adding Critical Extension: {0} {1}'.format(k, v))
if k == 'sub_alt_names':
if v['names']:
builder = builder.add_extension(v['names'], critical=True)
else:
builder = builder.add_extension(v, critical=True)
if csr_config.get('extensions'): if k in noncritical_extensions:
for k, v in csr_config.get('extensions', {}).items(): current_app.logger.debug('Adding Extension: {0} {1}'.format(k, v))
if k == 'subAltNames': builder = builder.add_extension(v, critical=False)
# map types to their x509 objects
general_names = []
for name in v['names']:
if name['nameType'] == 'DNSName':
general_names.append(x509.DNSName(name['value']))
builder = builder.add_extension( ski = extensions.get('subject_key_identifier', {})
x509.SubjectAlternativeName(general_names), critical=True if ski.get('include_ski', False):
) builder = builder.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
# TODO support more CSR options, none of the authority plugins currently support these options critical=False
# builder.add_extension( )
# x509.KeyUsage(
# digital_signature=digital_signature,
# content_commitment=content_commitment,
# key_encipherment=key_enipherment,
# data_encipherment=data_encipherment,
# key_agreement=key_agreement,
# key_cert_sign=key_cert_sign,
# crl_sign=crl_sign,
# encipher_only=enchipher_only,
# decipher_only=decipher_only
# ), critical=True
# )
#
# # we must maintain our own list of OIDs here
# builder.add_extension(
# x509.ExtendedKeyUsage(
# server_authentication=server_authentication,
# email=
# )
# )
#
# builder.add_extension(
# x509.AuthorityInformationAccess()
# )
#
# builder.add_extension(
# x509.AuthorityKeyIdentifier()
# )
#
# builder.add_extension(
# x509.SubjectKeyIdentifier()
# )
#
# builder.add_extension(
# x509.CRLDistributionPoints()
# )
#
# builder.add_extension(
# x509.ObjectIdentifier(oid)
# )
request = builder.sign( request = builder.sign(
private_key, hashes.SHA256(), default_backend() private_key, hashes.SHA256(), default_backend()
) )
# serialize our private key and CSR # serialize our private key and CSR
pem = private_key.private_bytes( private_key = private_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it
encryption_algorithm=serialization.NoEncryption() encryption_algorithm=serialization.NoEncryption()
) )
if isinstance(private_key, bytes):
private_key = private_key.decode('utf-8')
csr = request.public_bytes( csr = request.public_bytes(
encoding=serialization.Encoding.PEM encoding=serialization.Encoding.PEM
) ).decode('utf-8')
return csr, pem return csr, private_key
def stats(**kwargs): def stats(**kwargs):
@ -473,3 +463,88 @@ def stats(**kwargs):
values.append(count) values.append(count)
return {'labels': keys, 'values': values} 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]
def calculate_reissue_range(start, end):
"""
Determine what the new validity_start and validity_end dates should be.
:param start:
:param end:
:return:
"""
span = end - start
new_start = arrow.utcnow()
new_end = new_start + span
return new_start, arrow.get(new_end)
def get_certificate_primitives(certificate):
"""
Retrieve key primitive from a certificate such that the certificate
could be recreated with new expiration or be used to build upon.
:param certificate:
:return: dict of certificate primitives, should be enough to effectively re-issue
certificate via `create`.
"""
start, end = calculate_reissue_range(certificate.not_before, certificate.not_after)
data = CertificateInputSchema().load(CertificateOutputSchema().dump(certificate).data).data
# we can't quite tell if we are using a custom name, as this is an automated process (typically)
# we will rely on the Lemur generated name
data.pop('name', None)
# TODO this can be removed once we migrate away from cn
data['cn'] = data['common_name']
# needed until we move off not_*
data['not_before'] = start
data['not_after'] = end
data['validity_start'] = start
data['validity_end'] = end
return data
def reissue_certificate(certificate, replace=None, user=None):
"""
Reissue certificate with the same properties of the given certificate.
:param certificate:
:param replace:
:param user:
:return:
"""
primitives = get_certificate_primitives(certificate)
if not user:
primitives['creator'] = certificate.user
else:
primitives['creator'] = user
if replace:
primitives['replaces'] = [certificate]
new_cert = create(**primitives)
return new_cert

View File

@ -1,25 +1,25 @@
""" """
.. module: lemur.certificates.verify .. module: lemur.certificates.verify
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import requests import requests
import subprocess import subprocess
from OpenSSL import crypto from requests.exceptions import ConnectionError, InvalidSchema
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from flask import current_app
from lemur.utils import mktempfile from lemur.utils import mktempfile
from lemur.common.utils import parse_certificate
def ocsp_verify(cert_path, issuer_chain_path): def ocsp_verify(cert_path, issuer_chain_path):
""" """
Attempts to verify a certificate via OCSP. OCSP is a more modern version Attempts to verify a certificate via OCSP. OCSP is a more modern version
of CRL in that it will query the OCSP URI in order to determine if the of CRL in that it will query the OCSP URI in order to determine if the
certificate as been revoked certificate has been revoked
:param cert_path: :param cert_path:
:param issuer_chain_path: :param issuer_chain_path:
@ -33,13 +33,16 @@ def ocsp_verify(cert_path, issuer_chain_path):
'-cert', cert_path, "-url", url.strip()], stdout=subprocess.PIPE, stderr=subprocess.PIPE) '-cert', cert_path, "-url", url.strip()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
message, err = p2.communicate() message, err = p2.communicate()
if 'error' in message or 'Error' in message:
p_message = message.decode('utf-8')
if 'error' in p_message or 'Error' in p_message:
raise Exception("Got error when parsing OCSP url") raise Exception("Got error when parsing OCSP url")
elif 'revoked' in message: elif 'revoked' in p_message:
return return
elif 'good' not in message: elif 'good' not in p_message:
raise Exception("Did not receive a valid response") raise Exception("Did not receive a valid response")
return True return True
@ -54,17 +57,39 @@ def crl_verify(cert_path):
:raise Exception: If certificate does not have CRL :raise Exception: If certificate does not have CRL
""" """
with open(cert_path, 'rt') as c: with open(cert_path, 'rt') as c:
cert = x509.load_pem_x509_certificate(c.read(), default_backend()) cert = parse_certificate(c.read())
distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value
for p in distribution_points: for p in distribution_points:
point = p.full_name[0].value 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 try:
revoked = crl.get_revoked() response = requests.get(point)
for r in revoked:
if cert.serial == r.get_serial(): if response.status_code != 200:
raise Exception("Unable to retrieve CRL: {0}".format(point))
except InvalidSchema:
# Unhandled URI scheme (like ldap://); skip this distribution point.
continue
except ConnectionError:
raise Exception("Unable to retrieve CRL: {0}".format(point))
crl = x509.load_der_x509_crl(response.content, backend=default_backend())
for r in crl:
if cert.serial == r.serial_number:
try:
reason = r.extensions.get_extension_for_class(x509.CRLReason).value
# Handle "removeFromCRL" revoke reason as unrevoked; continue with the next distribution point.
# Per RFC 5280 section 6.3.3 (k): https://tools.ietf.org/html/rfc5280#section-6.3.3
if reason == x509.ReasonFlags.remove_from_crl:
break
except x509.ExtensionNotFound:
pass
return return
return True return True
@ -81,13 +106,10 @@ def verify(cert_path, issuer_chain_path):
try: try:
return ocsp_verify(cert_path, issuer_chain_path) return ocsp_verify(cert_path, issuer_chain_path)
except Exception as e: except Exception as e:
current_app.logger.debug("Could not use OCSP: {0}".format(e))
try: try:
return crl_verify(cert_path) return crl_verify(cert_path)
except Exception as e: except Exception as e:
current_app.logger.debug("Could not use CRL: {0}".format(e))
raise Exception("Failed to verify") raise Exception("Failed to verify")
raise Exception("Failed to verify")
def verify_string(cert_string, issuer_string): def verify_string(cert_string, issuer_string):

View File

@ -1,97 +1,41 @@
""" """
.. module: lemur.certificates.views .. module: lemur.certificates.views
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import base64 import base64
from builtins import str from builtins import str
from flask import Blueprint, make_response, jsonify
from flask.ext.restful import reqparse, Api, fields from flask import Blueprint, make_response, jsonify, g
from cryptography import x509 from flask_restful import reqparse, Api
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from lemur.common.schema import validate_schema
from lemur.certificates import service from lemur.common.utils import paginated_parser
from lemur.authorities.models import Authority
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission from lemur.auth.permissions import AuthorityPermission, CertificatePermission
from lemur.certificates import service
from lemur.certificates.models import Certificate
from lemur.plugins.base import plugins
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.roles import service as role_service
from lemur.common.utils import marshal_items, paginated_parser from lemur.logs import service as log_service
from lemur.notifications.views import notification_list
mod = Blueprint('certificates', __name__) mod = Blueprint('certificates', __name__)
api = Api(mod) api = Api(mod)
FIELDS = {
'name': fields.String,
'id': fields.Integer,
'bits': fields.Integer,
'deleted': fields.String,
'issuer': fields.String,
'serial': fields.String,
'owner': fields.String,
'chain': fields.String,
'san': fields.String,
'active': fields.Boolean,
'description': fields.String,
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
'cn': fields.String,
'signingAlgorithm': fields.String(attribute='signing_algorithm'),
'status': fields.String,
'body': fields.String
}
def valid_authority(authority_options):
"""
Defends against invalid authorities
:param authority_options:
:return: :raise ValueError:
"""
name = authority_options['name']
authority = Authority.query.filter(Authority.name == name).one()
if not authority:
raise ValueError("Unable to find authority specified")
if not authority.active:
raise ValueError("Selected authority [{0}] is not currently active".format(name))
return authority
def pem_str(value, name):
"""
Used to validate that the given string is a PEM formatted string
:param value:
:param name:
:return: :raise ValueError:
"""
try:
x509.load_pem_x509_certificate(bytes(value), default_backend())
except Exception:
raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name))
return value
def private_key_str(value, name):
"""
User to validate that a given string is a RSA private key
:param value:
:param name:
:return: :raise ValueError:
"""
try:
serialization.load_pem_private_key(bytes(value), None, backend=default_backend())
except Exception:
raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name))
return value
class CertificatesList(AuthenticatedResource): class CertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """ """ Defines the 'certificates' endpoint """
@ -100,7 +44,7 @@ class CertificatesList(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificatesList, self).__init__() super(CertificatesList, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, certificates_output_schema)
def get(self): def get(self):
""" """
.. http:get:: /certificates .. http:get:: /certificates
@ -124,37 +68,66 @@ class CertificatesList(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"items": [ "items": [{
{ "status": null,
"id": 1, "cn": "*.test.example.net",
"name": "cert1", "chain": "",
"description": "this is cert1", "authority": {
"bits": 2048, "active": true,
"deleted": false, "owner": "secure@example.com",
"issuer": "ExampeInc.", "id": 1,
"serial": "123450", "description": "verisign test authority",
"chain": "-----Begin ...", "name": "verisign"
"body": "-----Begin ...", },
"san": true, "owner": "joe@example.com",
"owner": 'bob@example.com", "serial": "82311058732025924142789179368889309156",
"active": true, "id": 2288,
"notBefore": "2015-06-05T17:09:39", "issuer": "SymantecCorporation",
"notAfter": "2015-06-10T17:09:39", "notBefore": "2016-06-03T00:00:00+00:00",
"cn": "example.com", "notAfter": "2018-01-12T23:59:59+00:00",
"status": "unknown" "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": [],
"replaced": [],
"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 "total": 1
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int. default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair format is k;v
:query limit: limit number. default is 10 :query count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
parser = paginated_parser.copy() parser = paginated_parser.copy()
parser.add_argument('timeRange', type=int, dest='time_range', location='args') parser.add_argument('timeRange', type=int, dest='time_range', location='args')
@ -166,10 +139,11 @@ class CertificatesList(AuthenticatedResource):
parser.add_argument('show', type=str, location='args') parser.add_argument('show', type=str, location='args')
args = parser.parse_args() args = parser.parse_args()
args['user'] = g.user
return service.render(args) return service.render(args)
@marshal_items(FIELDS) @validate_schema(certificate_input_schema, certificate_output_schema)
def post(self): def post(self, data=None):
""" """
.. http:post:: /certificates .. http:post:: /certificates
@ -184,90 +158,38 @@ class CertificatesList(AuthenticatedResource):
Accept: application/json, text/javascript Accept: application/json, text/javascript
{ {
"country": "US", "owner": "secure@example.net",
"state": "CA", "commonName": "test.example.net",
"location": "A Place", "country": "US",
"organization": "ExampleInc.", "extensions": {
"organizationalUnit": "Operations",
"owner": "bob@example.com",
"description": "test",
"selectedAuthority": "timetest2",
"csr",
"authority": {
"body": "-----BEGIN...",
"name": "timetest2",
"chain": "",
"notBefore": "2015-06-05T15:20:59",
"active": true,
"id": 50,
"notAfter": "2015-06-17T15:21:08",
"description": "dsfdsf"
},
"notifications": [
{
"description": "Default 30 day expiration notification",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 30,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "days",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "bob@example.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "DEFAULT_KGLISSON_30_DAY",
"pluginName": "email-notification",
"active": true,
"id": 7
}
],
"extensions": {
"basicConstraints": {},
"keyUsage": {
"isCritical": true,
"useKeyEncipherment": true,
"useDigitalSignature": true
},
"extendedKeyUsage": {
"isCritical": true,
"useServerAuthentication": true
},
"subjectKeyIdentifier": {
"includeSKI": true
},
"subAltNames": { "subAltNames": {
"names": [] "names": [
{
"nameType": "DNSName",
"value": "*.test.example.net"
},
{
"nameType": "DNSName",
"value": "www.test.example.net"
}
]
} }
}, },
"commonName": "test", "replacements": [{
"validityStart": "2015-06-05T07:00:00.000Z", "id": 1
"validityEnd": "2015-06-16T07:00:00.000Z", }],
"replacements": [ "notify": true,
{'id': 123} "validityEnd": "2026-01-01T08:00:00.000Z",
] "authority": {
} "name": "verisign"
},
"organization": "Netflix, Inc.",
"location": "Los Gatos",
"state": "California",
"validityStart": "2016-11-11T04:19:48.000Z",
"organizationalUnit": "Operations"
}
**Example response**: **Example response**:
@ -278,72 +200,80 @@ class CertificatesList(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "status": null,
"name": "cert1", "cn": "*.test.example.net",
"description": "this is cert1", "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, "bits": 2048,
"deleted": false, "body": "-----BEGIN CERTIFICATE-----...",
"issuer": "ExampeInc.", "description": null,
"serial": "123450", "deleted": null,
"chain": "-----Begin ...", "notifications": [{
"body": "-----Begin ...", "id": 1
"san": true, }],
"owner": "jimbob@example.com", "signingAlgorithm": "sha256",
"active": false, "user": {
"notBefore": "2015-06-05T17:09:39", "username": "jane",
"notAfter": "2015-06-10T17:09:39", "active": true,
"cn": "example.com", "email": "jane@example.com",
"status": "unknown" "id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [{
"id": 1
}],
"rotation": true,
"rotationPolicy": {"name": "default"},
"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
:arg validityStart: when the certificate should start being valid
:arg validityEnd: when the certificate should expire
:arg authority: authority that should issue the certificate
:arg country: country for the CSR
:arg state: state for the CSR
:arg location: location for the CSR
:arg organization: organization for CSR
:arg commonName: certiifcate common name
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
self.reqparse.add_argument('extensions', type=dict, location='json') role = role_service.get_by_name(data['authority'].owner)
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
self.reqparse.add_argument('authority', type=valid_authority, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('country', type=str, location='json', required=True)
self.reqparse.add_argument('state', type=str, location='json', required=True)
self.reqparse.add_argument('location', type=str, location='json', required=True)
self.reqparse.add_argument('organization', type=str, location='json', required=True)
self.reqparse.add_argument('organizationalUnit', type=str, location='json', required=True)
self.reqparse.add_argument('owner', type=str, location='json', required=True)
self.reqparse.add_argument('commonName', type=str, location='json', required=True)
self.reqparse.add_argument('csr', type=str, location='json')
args = self.reqparse.parse_args()
authority = args['authority']
role = role_service.get_by_name(authority.owner)
# all the authority role members should be allowed # 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 # allow "owner" roles by team DL
roles.append(role) roles.append(role)
permission = AuthorityPermission(authority.id, roles) authority_permission = AuthorityPermission(data['authority'].id, roles)
if permission.can(): if authority_permission.can():
return service.create(**args) data['creator'] = g.user
cert = service.create(**data)
if isinstance(cert, Certificate):
# only log if created, not pending
log_service.create(g.user, 'create_cert', certificate=cert)
return cert
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403 return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
class CertificatesUpload(AuthenticatedResource): class CertificatesUpload(AuthenticatedResource):
@ -353,8 +283,8 @@ class CertificatesUpload(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificatesUpload, self).__init__() super(CertificatesUpload, self).__init__()
@marshal_items(FIELDS) @validate_schema(certificate_upload_input_schema, certificate_output_schema)
def post(self): def post(self, data=None):
""" """
.. http:post:: /certificates/upload .. http:post:: /certificates/upload
@ -369,13 +299,15 @@ class CertificatesUpload(AuthenticatedResource):
Accept: application/json, text/javascript Accept: application/json, text/javascript
{ {
"owner": "joe@exmaple.com", "owner": "joe@example.com",
"publicCert": "---Begin Public...", "body": "-----BEGIN CERTIFICATE-----...",
"intermediateCert": "---Begin Public...", "chain": "-----BEGIN CERTIFICATE-----...",
"privateKey": "---Begin Private..." "privateKey": "-----BEGIN RSA PRIVATE KEY-----..."
"destinations": [], "destinations": [],
"notifications": [], "notifications": [],
"replacements": [], "replacements": [],
"roles": [],
"notify": true,
"name": "cert1" "name": "cert1"
} }
@ -388,51 +320,67 @@ class CertificatesUpload(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "status": null,
"name": "cert1", "cn": "*.test.example.net",
"description": "this is cert1", "chain": "",
"bits": 2048, "authority": {
"deleted": false, "active": true,
"issuer": "ExampeInc.", "owner": "secure@example.com",
"serial": "123450", "id": 1,
"chain": "-----Begin ...", "description": "verisign test authority",
"body": "-----Begin ...", "name": "verisign"
"san": true, },
"owner": "joe@example.com", "owner": "joe@example.com",
"active": true, "serial": "82311058732025924142789179368889309156",
"notBefore": "2015-06-05T17:09:39", "id": 2288,
"notAfter": "2015-06-10T17:09:39", "issuer": "SymantecCorporation",
"signingAlgorithm": "sha2" "notBefore": "2016-06-03T00:00:00+00:00",
"cn": "example.com", "notAfter": "2018-01-12T23:59:59+00:00",
"status": "unknown" "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": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"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
:arg publicCert: valid PEM public key for certificate
:arg intermediateCert valid PEM intermediate key for certificate
:arg privateKey: valid PEM private key for certificate
:arg destinations: list of aws destinations to upload the certificate to
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
:statuscode 200: no error :statuscode 200: no error
"""
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('owner', type=str, required=True, location='json')
self.reqparse.add_argument('name', type=str, location='json')
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
args = self.reqparse.parse_args() """
if args.get('destinations'): data['creator'] = g.user
if args.get('private_key'): if data.get('destinations'):
return service.upload(**args) if data.get('private_key'):
return service.upload(**data)
else: else:
raise Exception("Private key must be provided in order to upload certificate to AWS") 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): class CertificatesStats(AuthenticatedResource):
@ -481,7 +429,7 @@ class CertificatePrivateKey(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"key": "----Begin ...", "key": "-----BEGIN ..."
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
@ -492,17 +440,19 @@ class CertificatePrivateKey(AuthenticatedResource):
if not cert: if not cert:
return dict(message="Cannot find specified certificate"), 404 return dict(message="Cannot find specified certificate"), 404
role = role_service.get_by_name(cert.owner) # allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
permission = ViewKeyPermission(certificate_id, getattr(role, 'name', None)) if not permission.can():
return dict(message='You are not authorized to view this key'), 403
if permission.can(): log_service.create(g.current_user, 'key_view', certificate=cert)
response = make_response(jsonify(key=cert.private_key), 200) response = make_response(jsonify(key=cert.private_key), 200)
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store' response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
response.headers['pragma'] = 'no-cache' response.headers['pragma'] = 'no-cache'
return response return response
return dict(message='You are not authorized to view this key'), 403
class Certificates(AuthenticatedResource): class Certificates(AuthenticatedResource):
@ -510,7 +460,7 @@ class Certificates(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(Certificates, self).__init__() super(Certificates, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, certificate_output_schema)
def get(self, certificate_id): def get(self, certificate_id):
""" """
.. http:get:: /certificates/1 .. http:get:: /certificates/1
@ -534,33 +484,65 @@ class Certificates(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "status": null,
"name": "cert1", "cn": "*.test.example.net",
"description": "this is cert1", "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, "bits": 2048,
"deleted": false, "body": "-----BEGIN CERTIFICATE-----...",
"issuer": "ExampeInc.", "description": null,
"serial": "123450", "deleted": null,
"chain": "-----Begin ...", "notifications": [{
"body": "-----Begin ...", "id": 1
"san": true, }],
"owner": "bob@example.com", "signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true, "active": true,
"notBefore": "2015-06-05T17:09:39", "domains": [{
"notAfter": "2015-06-10T17:09:39", "sensitive": false,
"signingAlgorithm": "sha2", "id": 1090,
"cn": "example.com", "name": "*.test.example.net"
"status": "unknown" }],
"rotation": true,
"rotationPolicy": {"name": "default"},
"replaces": [],
"replaced": [],
"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 :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
return service.get(certificate_id) return service.get(certificate_id)
@marshal_items(FIELDS) @validate_schema(certificate_edit_input_schema, certificate_output_schema)
def put(self, certificate_id): def put(self, certificate_id, data=None):
""" """
.. http:put:: /certificates/1 .. http:put:: /certificates/1
@ -591,53 +573,85 @@ class Certificates(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"id": 1, "status": null,
"name": "cert1", "cn": "*.test.example.net",
"description": "this is cert1", "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, "bits": 2048,
"deleted": false, "body": "-----BEGIN CERTIFICATE-----...",
"issuer": "ExampeInc.", "description": null,
"serial": "123450", "deleted": null,
"chain": "-----Begin ...", "notifications": [{
"body": "-----Begin ...", "id": 1
"san": true, }]
"owner": "jimbob@example.com", "signingAlgorithm": "sha256",
"active": false, "user": {
"notBefore": "2015-06-05T17:09:39", "username": "jane",
"notAfter": "2015-06-10T17:09:39", "active": true,
"cn": "example.com", "email": "jane@example.com",
"status": "unknown", "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"
}],
"rotation": true,
"rotationPolicy": {"name": "default"},
"san": null
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
self.reqparse.add_argument('active', type=bool, location='json')
self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=notification_list, default=[], location='json')
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
args = self.reqparse.parse_args()
cert = service.get(certificate_id) cert = service.get(certificate_id)
role = role_service.get_by_name(cert.owner)
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) if not cert:
return dict(message="Cannot find specified certificate"), 404
if permission.can(): # allow creators
return service.update( if g.current_user != cert.user:
certificate_id, owner_role = role_service.get_by_name(cert.owner)
args['owner'], permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
args['description'],
args['active'],
args['destinations'],
args['notifications'],
args['replacements']
)
return dict(message='You are not authorized to update this certificate'), 403 if not permission.can():
return dict(message='You are not authorized to update this certificate'), 403
for destination in data['destinations']:
if destination.plugin.requires_key:
if not cert.private_key:
return dict(
message='Unable to add destination: {0}. Certificate does not have required private key.'.format(
destination.label
)
), 400
cert = service.update(certificate_id, **data)
log_service.create(g.current_user, 'update_cert', certificate=cert)
return cert
class NotificationCertificatesList(AuthenticatedResource): class NotificationCertificatesList(AuthenticatedResource):
@ -647,7 +661,7 @@ class NotificationCertificatesList(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(NotificationCertificatesList, self).__init__() super(NotificationCertificatesList, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, certificates_output_schema)
def get(self, notification_id): def get(self, notification_id):
""" """
.. http:get:: /notifications/1/certificates .. http:get:: /notifications/1/certificates
@ -671,38 +685,68 @@ class NotificationCertificatesList(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"items": [ "items": [{
{ "status": null,
"id": 1, "cn": "*.test.example.net",
"name": "cert1", "chain": "",
"description": "this is cert1", "authority": {
"bits": 2048, "active": true,
"deleted": false, "owner": "secure@example.com",
"issuer": "ExampeInc.", "id": 1,
"serial": "123450", "description": "verisign test authority",
"chain": "-----Begin ...", "name": "verisign"
"body": "-----Begin ...", },
"san": true, "owner": "joe@example.com",
"owner": 'bob@example.com", "serial": "82311058732025924142789179368889309156",
"active": true, "id": 2288,
"notBefore": "2015-06-05T17:09:39", "issuer": "SymantecCorporation",
"notAfter": "2015-06-10T17:09:39", "notBefore": "2016-06-03T00:00:00+00:00",
"signingAlgorithm": "sha2", "notAfter": "2018-01-12T23:59:59+00:00",
"cn": "example.com", "destinations": [],
"status": "unknown" "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": [],
"replaced": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"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 "total": 1
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair format is k;v
:query limit: limit number. default is 10 :query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
parser = paginated_parser.copy() parser = paginated_parser.copy()
parser.add_argument('timeRange', type=int, dest='time_range', location='args') parser.add_argument('timeRange', type=int, dest='time_range', location='args')
@ -715,6 +759,7 @@ class NotificationCertificatesList(AuthenticatedResource):
args = parser.parse_args() args = parser.parse_args()
args['notification_id'] = notification_id args['notification_id'] = notification_id
args['user'] = g.current_user
return service.render(args) return service.render(args)
@ -723,7 +768,7 @@ class CertificatesReplacementsList(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificatesReplacementsList, self).__init__() super(CertificatesReplacementsList, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, certificates_output_schema)
def get(self, certificate_id): def get(self, certificate_id):
""" """
.. http:get:: /certificates/1/replacements .. http:get:: /certificates/1/replacements
@ -746,29 +791,64 @@ class CertificatesReplacementsList(AuthenticatedResource):
Vary: Accept Vary: Accept
Content-Type: text/javascript Content-Type: text/javascript
[{ {
"id": 1, "items": [{
"name": "cert1", "status": null,
"description": "this is cert1", "cn": "*.test.example.net",
"bits": 2048, "chain": "",
"deleted": false, "authority": {
"issuer": "ExampeInc.", "active": true,
"serial": "123450", "owner": "secure@example.com",
"chain": "-----Begin ...", "id": 1,
"body": "-----Begin ...", "description": "verisign test authority",
"san": true, "name": "verisign"
"owner": "bob@example.com", },
"active": true, "owner": "joe@example.com",
"notBefore": "2015-06-05T17:09:39", "serial": "82311058732025924142789179368889309156",
"notAfter": "2015-06-10T17:09:39", "id": 2288,
"signingAlgorithm": "sha2", "issuer": "SymantecCorporation",
"cn": "example.com", "notBefore": "2016-06-03T00:00:00+00:00",
"status": "unknown" "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": [],
"replaced": [],
"rotation": true,
"rotationPolicy": {"name": "default"},
"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 :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
return service.get(certificate_id).replaces return service.get(certificate_id).replaces
@ -778,7 +858,8 @@ class CertificateExport(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(CertificateExport, self).__init__() super(CertificateExport, self).__init__()
def post(self, certificate_id): @validate_schema(certificate_export_input_schema, None)
def post(self, certificate_id, data=None):
""" """
.. http:post:: /certificates/1/export .. http:post:: /certificates/1/export
@ -842,23 +923,102 @@ class CertificateExport(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
self.reqparse.add_argument('export', type=dict, required=True, location='json')
args = self.reqparse.parse_args()
cert = service.get(certificate_id) cert = service.get(certificate_id)
role = role_service.get_by_name(cert.owner)
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None)) if not cert:
return dict(message="Cannot find specified certificate"), 404
if permission.can(): plugin = data['plugin']['plugin_object']
extension, passphrase, data = service.export(cert, args['export']['plugin'])
# we take a hit in message size when b64 encoding
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data))
return dict(message='You are not authorized to export this certificate'), 403 if plugin.requires_key:
if not cert.private_key:
return dict(
message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(
plugin.slug)), 400
else:
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
return dict(message='You are not authorized to export this certificate.'), 403
options = data['plugin']['plugin_options']
log_service.create(g.current_user, 'key_view', certificate=cert)
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).decode('utf-8'))
class CertificateRevoke(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificateRevoke, self).__init__()
@validate_schema(None, None)
def put(self, certificate_id, data=None):
"""
.. http:put:: /certificates/1/revoke
Revoke a certificate
**Example request**:
.. sourcecode:: http
POST /certificates/1/revoke 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
{
'id': 1
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
cert = service.get(certificate_id)
if not cert:
return dict(message="Cannot find specified certificate"), 404
# allow creators
if g.current_user != cert.user:
owner_role = role_service.get_by_name(cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
if not permission.can():
return dict(message='You are not authorized to revoke this certificate.'), 403
if not cert.external_id:
return dict(message='Cannot revoke certificate. No external id found.'), 400
if cert.endpoints:
return dict(message='Cannot revoke certificate. Endpoints are deployed with the given certificate.'), 403
plugin = plugins.get(cert.authority.plugin_name)
plugin.revoke_certificate(cert, data)
log_service.create(g.current_user, 'revoke_cert', certificate=cert)
return dict(id=cert.id)
api.add_resource(CertificateRevoke, '/certificates/<int:certificate_id>/revoke', endpoint='revokeCertificate')
api.add_resource(CertificatesList, '/certificates', endpoint='certificates') api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate') api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')

267
lemur/common/defaults.py Normal file
View File

@ -0,0 +1,267 @@
import re
import unicodedata
from cryptography import x509
from flask import current_app
from lemur.extensions import sentry
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
def text_to_slug(value):
"""Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters."""
# Strip all character accents: decompose Unicode characters and then drop combining chars.
value = ''.join(c for c in unicodedata.normalize('NFKD', value) if not unicodedata.combining(c))
# Replace all remaining non-alphanumeric characters with '-'. Multiple characters get collapsed into a single dash.
# Except, keep 'xn--' used in IDNA domain names as is.
value = re.sub(r'[^A-Za-z0-9.]+(?<!xn--)', '-', value)
# '-' in the beginning or end of string looks ugly.
return value.strip('-')
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.replace(' ', ''),
not_before=not_before.strftime('%Y%m%d'),
not_after=not_after.strftime('%Y%m%d')
)
temp = temp.replace('*', "WILDCARD")
return text_to_slug(temp)
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
"""
try:
return cert.subject.get_attributes_for_oid(
x509.OID_COMMON_NAME
)[0].value.strip()
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get common name! {0}".format(e))
def organization(cert):
"""
Attempt to get the organization name from a given certificate.
:param cert:
:return:
"""
try:
return cert.subject.get_attributes_for_oid(
x509.OID_ORGANIZATION_NAME
)[0].value.strip()
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get organization! {0}".format(e))
def organizational_unit(cert):
"""
Attempt to get the organization unit from a given certificate.
:param cert:
:return:
"""
try:
return cert.subject.get_attributes_for_oid(
x509.OID_ORGANIZATIONAL_UNIT_NAME
)[0].value.strip()
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get organizational unit! {0}".format(e))
def country(cert):
"""
Attempt to get the country from a given certificate.
:param cert:
:return:
"""
try:
return cert.subject.get_attributes_for_oid(
x509.OID_COUNTRY_NAME
)[0].value.strip()
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get country! {0}".format(e))
def state(cert):
"""
Attempt to get the from a given certificate.
:param cert:
:return:
"""
try:
return cert.subject.get_attributes_for_oid(
x509.OID_STATE_OR_PROVINCE_NAME
)[0].value.strip()
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get state! {0}".format(e))
def location(cert):
"""
Attempt to get the location name from a given certificate.
:param cert:
:return:
"""
try:
return cert.subject.get_attributes_for_oid(
x509.OID_LOCALITY_NAME
)[0].value.strip()
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get location! {0}".format(e))
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 x509.ExtensionNotFound:
if current_app.config.get("LOG_SSL_SUBJ_ALT_NAME_ERRORS", True):
sentry.captureException()
except Exception as e:
sentry.captureException()
return domains
def serial(cert):
"""
Fetch the serial number from the certificate.
:param cert:
:return: serial number
"""
return cert.serial_number
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
"""
try:
return cert.public_key().key_size
except AttributeError:
sentry.captureException()
current_app.logger.debug('Unable to get bitstrength.')
def issuer(cert):
"""
Gets a sane issuer name from a given certificate.
:param cert:
:return: Issuer
"""
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
try:
# Try organization name or fall back to CN
issuer = (cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)
or cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME))
issuer = str(issuer[0].value)
for c in delchars:
issuer = issuer.replace(c, "")
return issuer
except Exception as e:
sentry.captureException()
current_app.logger.error("Unable to get issuer! {0}".format(e))
return "Unknown"
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

414
lemur/common/fields.py Normal file
View File

@ -0,0 +1,414 @@
"""
.. module: lemur.common.fields
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
import warnings
import ipaddress
from flask import current_app
from datetime import datetime as dt
from cryptography import x509
from marshmallow import utils
from marshmallow.fields import Field
from marshmallow.exceptions import ValidationError
from lemur.common import validators
class Hex(Field):
"""
A hex formatted string.
"""
def _serialize(self, value, attr, obj):
if value:
value = hex(int(value))[2:].upper()
return value
class ArrowDateTime(Field):
"""A formatted datetime string in UTC.
Example: ``'2014-12-22T03:12:58.019077+00:00'``
Timezone-naive `datetime` objects are converted to
UTC (+00:00) by :meth:`Schema.dump <marshmallow.Schema.dump>`.
:meth:`Schema.load <marshmallow.Schema.load>` returns `datetime`
objects that are timezone-aware.
:param str format: Either ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601),
or a date format string. If `None`, defaults to "iso".
:param kwargs: The same keyword arguments that :class:`Field` receives.
"""
DATEFORMAT_SERIALIZATION_FUNCS = {
'iso': utils.isoformat,
'iso8601': utils.isoformat,
'rfc': utils.rfcformat,
'rfc822': utils.rfcformat,
}
DATEFORMAT_DESERIALIZATION_FUNCS = {
'iso': utils.from_iso,
'iso8601': utils.from_iso,
'rfc': utils.from_rfc,
'rfc822': utils.from_rfc,
}
DEFAULT_FORMAT = 'iso'
localtime = False
default_error_messages = {
'invalid': 'Not a valid datetime.',
'format': '"{input}" cannot be formatted as a datetime.',
}
def __init__(self, format=None, **kwargs):
super(ArrowDateTime, self).__init__(**kwargs)
# Allow this to be None. It may be set later in the ``_serialize``
# or ``_desrialize`` methods This allows a Schema to dynamically set the
# dateformat, e.g. from a Meta option
self.dateformat = format
def _add_to_schema(self, field_name, schema):
super(ArrowDateTime, self)._add_to_schema(field_name, schema)
self.dateformat = self.dateformat or schema.opts.dateformat
def _serialize(self, value, attr, obj):
if value is None:
return None
self.dateformat = self.dateformat or self.DEFAULT_FORMAT
format_func = self.DATEFORMAT_SERIALIZATION_FUNCS.get(self.dateformat, None)
if format_func:
try:
return format_func(value, localtime=self.localtime)
except (AttributeError, ValueError) as err:
self.fail('format', input=value)
else:
return value.strftime(self.dateformat)
def _deserialize(self, value, attr, data):
if not value: # Falsy values, e.g. '', None, [] are not valid
raise self.fail('invalid')
self.dateformat = self.dateformat or self.DEFAULT_FORMAT
func = self.DATEFORMAT_DESERIALIZATION_FUNCS.get(self.dateformat)
if func:
try:
return arrow.get(func(value))
except (TypeError, AttributeError, ValueError):
raise self.fail('invalid')
elif self.dateformat:
try:
return dt.datetime.strptime(value, self.dateformat)
except (TypeError, AttributeError, ValueError):
raise self.fail('invalid')
elif utils.dateutil_available:
try:
return arrow.get(utils.from_datestring(value))
except TypeError:
raise self.fail('invalid')
else:
warnings.warn('It is recommended that you install python-dateutil '
'for improved datetime deserialization.')
raise self.fail('invalid')
class KeyUsageExtension(Field):
"""An x509.KeyUsage ExtensionType object
Dict of KeyUsage names/values are deserialized into an x509.KeyUsage object
and back.
:param kwargs: The same keyword arguments that :class:`Field` receives.
"""
def _serialize(self, value, attr, obj):
return {
'useDigitalSignature': value.digital_signature,
'useNonRepudiation': value.content_commitment,
'useKeyEncipherment': value.key_encipherment,
'useDataEncipherment': value.data_encipherment,
'useKeyAgreement': value.key_agreement,
'useKeyCertSign': value.key_cert_sign,
'useCRLSign': value.crl_sign,
'useEncipherOnly': value._encipher_only,
'useDecipherOnly': value._decipher_only
}
def _deserialize(self, value, attr, data):
keyusages = {
'digital_signature': False,
'content_commitment': False,
'key_encipherment': False,
'data_encipherment': False,
'key_agreement': False,
'key_cert_sign': False,
'crl_sign': False,
'encipher_only': False,
'decipher_only': False
}
for k, v in value.items():
if k == 'useDigitalSignature':
keyusages['digital_signature'] = v
elif k == 'useNonRepudiation':
keyusages['content_commitment'] = v
elif k == 'useKeyEncipherment':
keyusages['key_encipherment'] = v
elif k == 'useDataEncipherment':
keyusages['data_encipherment'] = v
elif k == 'useKeyCertSign':
keyusages['key_cert_sign'] = v
elif k == 'useCRLSign':
keyusages['crl_sign'] = v
elif k == 'useKeyAgreement':
keyusages['key_agreement'] = v
elif k == 'useEncipherOnly' and v:
keyusages['encipher_only'] = True
keyusages['key_agreement'] = True
elif k == 'useDecipherOnly' and v:
keyusages['decipher_only'] = True
keyusages['key_agreement'] = True
if keyusages['encipher_only'] and keyusages['decipher_only']:
raise ValidationError('A certificate cannot have both Encipher Only and Decipher Only Extended Key Usages.')
return x509.KeyUsage(
digital_signature=keyusages['digital_signature'],
content_commitment=keyusages['content_commitment'],
key_encipherment=keyusages['key_encipherment'],
data_encipherment=keyusages['data_encipherment'],
key_agreement=keyusages['key_agreement'],
key_cert_sign=keyusages['key_cert_sign'],
crl_sign=keyusages['crl_sign'],
encipher_only=keyusages['encipher_only'],
decipher_only=keyusages['decipher_only']
)
class ExtendedKeyUsageExtension(Field):
"""An x509.ExtendedKeyUsage ExtensionType object
Dict of ExtendedKeyUsage names/values are deserialized into an x509.ExtendedKeyUsage object
and back.
:param kwargs: The same keyword arguments that :class:`Field` receives.
"""
def _serialize(self, value, attr, obj):
usages = value._usages
usage_list = {}
for usage in usages:
if usage == x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH:
usage_list['useClientAuthentication'] = True
elif usage == x509.oid.ExtendedKeyUsageOID.SERVER_AUTH:
usage_list['useServerAuthentication'] = True
elif usage == x509.oid.ExtendedKeyUsageOID.CODE_SIGNING:
usage_list['useCodeSigning'] = True
elif usage == x509.oid.ExtendedKeyUsageOID.EMAIL_PROTECTION:
usage_list['useEmailProtection'] = True
elif usage == x509.oid.ExtendedKeyUsageOID.TIME_STAMPING:
usage_list['useTimestamping'] = True
elif usage == x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING:
usage_list['useOCSPSigning'] = True
elif usage.dotted_string == '1.3.6.1.5.5.7.3.14':
usage_list['useEapOverLAN'] = True
elif usage.dotted_string == '1.3.6.1.5.5.7.3.13':
usage_list['useEapOverPPP'] = True
elif usage.dotted_string == '1.3.6.1.4.1.311.20.2.2':
usage_list['useSmartCardLogon'] = True
else:
current_app.logger.warning('Unable to serialize ExtendedKeyUsage with OID: {usage}'.format(usage=usage.dotted_string))
return usage_list
def _deserialize(self, value, attr, data):
usage_oids = []
for k, v in value.items():
if k == 'useClientAuthentication' and v:
usage_oids.append(x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH)
elif k == 'useServerAuthentication' and v:
usage_oids.append(x509.oid.ExtendedKeyUsageOID.SERVER_AUTH)
elif k == 'useCodeSigning' and v:
usage_oids.append(x509.oid.ExtendedKeyUsageOID.CODE_SIGNING)
elif k == 'useEmailProtection' and v:
usage_oids.append(x509.oid.ExtendedKeyUsageOID.EMAIL_PROTECTION)
elif k == 'useTimestamping' and v:
usage_oids.append(x509.oid.ExtendedKeyUsageOID.TIME_STAMPING)
elif k == 'useOCSPSigning' and v:
usage_oids.append(x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING)
elif k == 'useEapOverLAN' and v:
usage_oids.append(x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.3.14"))
elif k == 'useEapOverPPP' and v:
usage_oids.append(x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.3.13"))
elif k == 'useSmartCardLogon' and v:
usage_oids.append(x509.oid.ObjectIdentifier("1.3.6.1.4.1.311.20.2.2"))
else:
current_app.logger.warning('Unable to deserialize ExtendedKeyUsage with name: {key}'.format(key=k))
return x509.ExtendedKeyUsage(usage_oids)
class BasicConstraintsExtension(Field):
"""An x509.BasicConstraints ExtensionType object
Dict of CA boolean and a path_length integer names/values are deserialized into an x509.BasicConstraints object
and back.
:param kwargs: The same keyword arguments that :class:`Field` receives.
"""
def _serialize(self, value, attr, obj):
return {'ca': value.ca, 'path_length': value.path_length}
def _deserialize(self, value, attr, data):
ca = value.get('ca', False)
path_length = value.get('path_length', None)
if ca:
if not isinstance(path_length, (type(None), int)):
raise ValidationError('A CA certificate path_length (for BasicConstraints) must be None or an integer.')
return x509.BasicConstraints(ca=True, path_length=path_length)
else:
return x509.BasicConstraints(ca=False, path_length=None)
class SubjectAlternativeNameExtension(Field):
"""An x509.SubjectAlternativeName ExtensionType object
Dict of CA boolean and a path_length integer names/values are deserialized into an x509.BasicConstraints object
and back.
:param kwargs: The same keyword arguments that :class:`Field` receives.
"""
def _serialize(self, value, attr, obj):
general_names = []
name_type = None
if value:
for name in value._general_names:
value = name.value
if isinstance(name, x509.DNSName):
name_type = 'DNSName'
elif isinstance(name, x509.IPAddress):
if isinstance(value, ipaddress.IPv4Network):
name_type = 'IPNetwork'
else:
name_type = 'IPAddress'
value = str(value)
elif isinstance(name, x509.UniformResourceIdentifier):
name_type = 'uniformResourceIdentifier'
elif isinstance(name, x509.DirectoryName):
name_type = 'directoryName'
elif isinstance(name, x509.RFC822Name):
name_type = 'rfc822Name'
elif isinstance(name, x509.RegisteredID):
name_type = 'registeredID'
value = value.dotted_string
else:
current_app.logger.warning('Unknown SubAltName type: {name}'.format(name=name))
general_names.append({'nameType': name_type, 'value': value})
return general_names
def _deserialize(self, value, attr, data):
general_names = []
for name in value:
if name['nameType'] == 'DNSName':
validators.sensitive_domain(name['value'])
general_names.append(x509.DNSName(name['value']))
elif name['nameType'] == 'IPAddress':
general_names.append(x509.IPAddress(ipaddress.ip_address(name['value'])))
elif name['nameType'] == 'IPNetwork':
general_names.append(x509.IPAddress(ipaddress.ip_network(name['value'])))
elif name['nameType'] == 'uniformResourceIdentifier':
general_names.append(x509.UniformResourceIdentifier(name['value']))
elif name['nameType'] == 'directoryName':
# TODO: Need to parse a string in name['value'] like:
# 'CN=Common Name, O=Org Name, OU=OrgUnit Name, C=US, ST=ST, L=City/emailAddress=person@example.com'
# or
# 'CN=Common Name/O=Org Name/OU=OrgUnit Name/C=US/ST=NH/L=City/emailAddress=person@example.com'
# and turn it into something like:
# x509.Name([
# x509.NameAttribute(x509.OID_COMMON_NAME, "Common Name"),
# x509.NameAttribute(x509.OID_ORGANIZATION_NAME, "Org Name"),
# x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, "OrgUnit Name"),
# x509.NameAttribute(x509.OID_COUNTRY_NAME, "US"),
# x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, "NH"),
# x509.NameAttribute(x509.OID_LOCALITY_NAME, "City"),
# x509.NameAttribute(x509.OID_EMAIL_ADDRESS, "person@example.com")
# ]
# general_names.append(x509.DirectoryName(x509.Name(BLAH))))
pass
elif name['nameType'] == 'rfc822Name':
general_names.append(x509.RFC822Name(name['value']))
elif name['nameType'] == 'registeredID':
general_names.append(x509.RegisteredID(x509.ObjectIdentifier(name['value'])))
elif name['nameType'] == 'otherName':
# This has two inputs (type and value), so it doesn't fit the mold of the rest of these GeneralName entities.
# general_names.append(x509.OtherName(name['type'], bytes(name['value']), 'utf-8'))
pass
elif name['nameType'] == 'x400Address':
# The Python Cryptography library doesn't support x400Address types (yet?)
pass
elif name['nameType'] == 'EDIPartyName':
# The Python Cryptography library doesn't support EDIPartyName types (yet?)
pass
else:
current_app.logger.warning('Unable to deserialize SubAltName with type: {name_type}'.format(name_type=name['nameType']))
return x509.SubjectAlternativeName(general_names)

View File

@ -1,16 +1,29 @@
""" """
.. module: lemur.common.health .. module: lemur.common.health
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import Blueprint from flask import Blueprint
from lemur.database import db
from lemur.extensions import sentry
mod = Blueprint('healthCheck', __name__) mod = Blueprint('healthCheck', __name__)
@mod.route('/healthcheck') @mod.route('/healthcheck')
def health(): def health():
return 'ok' try:
if healthcheck(db):
return 'ok'
except Exception:
sentry.captureException()
return 'db check failed'
def healthcheck(db):
with db.engine.connect() as connection:
connection.execute('SELECT 1;')
return True

View File

@ -1,13 +1,15 @@
""" """
.. module: lemur.common.managers .. module: lemur.common.managers
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import current_app from flask import current_app
from lemur.exceptions import InvalidConfiguration
# inspired by https://github.com/getsentry/sentry # inspired by https://github.com/getsentry/sentry
class InstanceManager(object): class InstanceManager(object):
@ -58,9 +60,14 @@ class InstanceManager(object):
results.append(cls()) results.append(cls())
else: else:
results.append(cls) results.append(cls)
except Exception:
current_app.logger.exception('Unable to import %s', cls_path) except InvalidConfiguration as e:
current_app.logger.warning("Plugin '{0}' may not work correctly. {1}".format(class_name, e))
except Exception as e:
current_app.logger.exception("Unable to import {0}. Reason: {1}".format(cls_path, e))
continue continue
self.cache = results self.cache = results
return results return results

24
lemur/common/missing.py Normal file
View File

@ -0,0 +1,24 @@
import arrow
from flask import current_app
from lemur.common.utils import is_weekend
def convert_validity_years(data):
"""
Convert validity years to validity_start and validity_end
:param data:
:return:
"""
if data.get('validity_years'):
now = arrow.utcnow()
data['validity_start'] = now.isoformat()
end = now.replace(years=+int(data['validity_years']))
if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True):
if is_weekend(end):
end = end.replace(days=-2)
data['validity_end'] = end.isoformat()
return data

174
lemur/common/schema.py Normal file
View File

@ -0,0 +1,174 @@
"""
.. module: lemur.common.schema
:platform: unix
:copyright: (c) 2018 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 inflection import camelize, underscore
from marshmallow import Schema, post_dump, pre_load
from lemur.extensions import sentry
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)
def unwrap_envelope(self, data, many):
if many:
if data['items']:
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 unwrap_pagination(data, output_schema):
if not output_schema:
return data
if isinstance(data, dict):
if 'total' in data.keys():
if data.get('total') == 0:
return data
marshaled_data = {'total': data['total']}
marshaled_data['items'] = output_schema.dump(data['items'], many=True).data
return marshaled_data
return output_schema.dump(data).data
elif isinstance(data, list):
marshaled_data = {'total': len(data)}
marshaled_data['items'] = output_schema.dump(data, many=True).data
return marshaled_data
return output_schema.dump(data).data
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:
sentry.captureException()
current_app.logger.exception(e)
return dict(message=str(e)), 500
if isinstance(resp, tuple):
return resp[0], resp[1]
if not resp:
return dict(message="No data found"), 404
return unwrap_pagination(resp, output_schema), 200
return decorated_function
return decorator

View File

@ -1,20 +1,31 @@
""" """
.. module: lemur.common.utils .. module: lemur.common.utils
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import string
import random import random
from functools import wraps import string
from flask import current_app import sqlalchemy
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from flask_restful.reqparse import RequestParser
from sqlalchemy import and_, func
from flask.ext.restful import marshal from lemur.constants import CERTIFICATE_KEY_TYPES
from flask.ext.restful.reqparse import RequestParser from lemur.exceptions import InvalidConfiguration
from flask.ext.sqlalchemy import Pagination
paginated_parser = RequestParser()
paginated_parser.add_argument('count', type=int, default=10, location='args')
paginated_parser.add_argument('page', type=int, default=1, location='args')
paginated_parser.add_argument('sortDir', type=str, dest='sort_dir', location='args')
paginated_parser.add_argument('sortBy', type=str, dest='sort_by', location='args')
paginated_parser.add_argument('filter', type=str, location='args')
def get_psuedo_random_string(): def get_psuedo_random_string():
@ -28,51 +39,175 @@ def get_psuedo_random_string():
return challenge return challenge
class marshal_items(object): def parse_certificate(body):
def __init__(self, fields, envelope=None): """
self.fields = fields Helper function that parses a PEM certificate.
self.envelop = envelope
def __call__(self, f): :param body:
def _filter_items(items): :return:
filtered_items = [] """
for item in items: if isinstance(body, str):
filtered_items.append(marshal(item, self.fields)) body = body.encode('utf-8')
return filtered_items
@wraps(f) return x509.load_pem_x509_certificate(body, default_backend())
def wrapper(*args, **kwargs):
try:
resp = f(*args, **kwargs)
# this is a bit weird way to handle non standard error codes returned from the marshaled function
if isinstance(resp, tuple):
return resp[0], resp[1]
if isinstance(resp, Pagination):
return {'items': _filter_items(resp.items), 'total': resp.total}
if isinstance(resp, list):
return {'items': _filter_items(resp), 'total': len(resp)}
return marshal(resp, self.fields)
except Exception as e:
current_app.logger.exception(e)
# this is a little weird hack to respect flask restful parsing errors on marshaled functions
if hasattr(e, 'code'):
if hasattr(e, 'data'):
return {'message': e.data['message']}, 400
else:
return {'message': {'exception': 'unknown'}}, 400
else:
return {'message': {'exception': str(e)}}, 400
return wrapper
paginated_parser = RequestParser() def parse_csr(csr):
"""
Helper function that parses a CSR.
paginated_parser.add_argument('count', type=int, default=10, location='args') :param csr:
paginated_parser.add_argument('page', type=int, default=1, location='args') :return:
paginated_parser.add_argument('sortDir', type=str, dest='sort_dir', location='args') """
paginated_parser.add_argument('sortBy', type=str, dest='sort_by', location='args') if isinstance(csr, str):
paginated_parser.add_argument('filter', type=str, location='args') csr = csr.encode('utf-8')
return x509.load_pem_x509_csr(csr, default_backend())
def get_authority_key(body):
"""Returns the authority key for a given certificate in hex format"""
parsed_cert = parse_certificate(body)
authority_key = parsed_cert.extensions.get_extension_for_class(
x509.AuthorityKeyIdentifier).value.key_identifier
return authority_key.hex()
def generate_private_key(key_type):
"""
Generates a new private key based on key_type.
Valid key types: RSA2048, RSA4096', 'ECCPRIME192V1', 'ECCPRIME256V1', 'ECCSECP192R1',
'ECCSECP224R1', 'ECCSECP256R1', 'ECCSECP384R1', 'ECCSECP521R1', 'ECCSECP256K1',
'ECCSECT163K1', 'ECCSECT233K1', 'ECCSECT283K1', 'ECCSECT409K1', 'ECCSECT571K1',
'ECCSECT163R2', 'ECCSECT233R1', 'ECCSECT283R1', 'ECCSECT409R1', 'ECCSECT571R2'
:param key_type:
:return:
"""
_CURVE_TYPES = {
"ECCPRIME192V1": ec.SECP192R1(),
"ECCPRIME256V1": ec.SECP256R1(),
"ECCSECP192R1": ec.SECP192R1(),
"ECCSECP224R1": ec.SECP224R1(),
"ECCSECP256R1": ec.SECP256R1(),
"ECCSECP384R1": ec.SECP384R1(),
"ECCSECP521R1": ec.SECP521R1(),
"ECCSECP256K1": ec.SECP256K1(),
"ECCSECT163K1": ec.SECT163K1(),
"ECCSECT233K1": ec.SECT233K1(),
"ECCSECT283K1": ec.SECT283K1(),
"ECCSECT409K1": ec.SECT409K1(),
"ECCSECT571K1": ec.SECT571K1(),
"ECCSECT163R2": ec.SECT163R2(),
"ECCSECT233R1": ec.SECT233R1(),
"ECCSECT283R1": ec.SECT283R1(),
"ECCSECT409R1": ec.SECT409R1(),
"ECCSECT571R2": ec.SECT571R1(),
}
if key_type not in CERTIFICATE_KEY_TYPES:
raise Exception("Invalid key type: {key_type}. Supported key types: {choices}".format(
key_type=key_type,
choices=",".join(CERTIFICATE_KEY_TYPES)
))
if 'RSA' in key_type:
key_size = int(key_type[3:])
return rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend()
)
elif 'ECC' in key_type:
return ec.generate_private_key(
curve=_CURVE_TYPES[key_type],
backend=default_backend()
)
def is_weekend(date):
"""
Determines if a given date is on a weekend.
:param date:
:return:
"""
if date.weekday() > 5:
return True
def validate_conf(app, required_vars):
"""
Ensures that the given fields are set in the applications conf.
:param app:
:param required_vars: list
"""
for var in required_vars:
if not app.config.get(var):
raise InvalidConfiguration("Required variable '{var}' is not set in Lemur's conf.".format(var=var))
# https://bitbucket.org/zzzeek/sqlalchemy/wiki/UsageRecipes/WindowedRangeQuery
def column_windows(session, column, windowsize):
"""Return a series of WHERE clauses against
a given column that break it into windows.
Result is an iterable of tuples, consisting of
((start, end), whereclause), where (start, end) are the ids.
Requires a database that supports window functions,
i.e. Postgresql, SQL Server, Oracle.
Enhance this yourself ! Add a "where" argument
so that windows of just a subset of rows can
be computed.
"""
def int_for_range(start_id, end_id):
if end_id:
return and_(
column >= start_id,
column < end_id
)
else:
return column >= start_id
q = session.query(
column,
func.row_number().over(order_by=column).label('rownum')
).from_self(column)
if windowsize > 1:
q = q.filter(sqlalchemy.text("rownum %% %d=1" % windowsize))
intervals = [id for id, in q]
while intervals:
start = intervals.pop(0)
if intervals:
end = intervals[0]
else:
end = None
yield int_for_range(start, end)
def windowed_query(q, column, windowsize):
""""Break a Query into windows on a given column."""
for whereclause in column_windows(
q.session,
column, windowsize):
for row in q.filter(whereclause).order_by(column):
yield row
def truthiness(s):
"""If input string resembles something truthy then return True, else False."""
return s.lower() in ('true', 'yes', 'on', 't', '1')

143
lemur/common/validators.py Normal file
View File

@ -0,0 +1,143 @@
import re
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import NameOID
from flask import current_app
from marshmallow.exceptions import ValidationError
from lemur.auth.permissions import SensitiveDomainPermission
from lemur.common.utils import parse_certificate, is_weekend
from lemur.domains import service as domain_service
def public_certificate(body):
"""
Determines if specified string is valid public certificate.
:param body:
:return:
"""
try:
parse_certificate(body)
except Exception as e:
current_app.logger.exception(e)
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:
if isinstance(key, bytes):
serialization.load_pem_private_key(key, None, backend=default_backend())
else:
serialization.load_pem_private_key(key.encode('utf-8'), None, backend=default_backend())
except Exception:
raise ValidationError('Private key presented is not valid.')
def common_name(value):
"""If the common name could be a domain name, apply domain validation rules."""
# Common name could be a domain name, or a human-readable name of the subject (often used in CA names or client
# certificates). As a simple heuristic, we assume that human-readable names always include a space.
# However, to avoid confusion for humans, we also don't count spaces at the beginning or end of the string.
if ' ' not in value.strip():
return sensitive_domain(value)
def sensitive_domain(domain):
"""
Checks if user has the admin role, the domain does not match sensitive domains and whitelisted domain patterns.
:param domain: domain name (str)
:return:
"""
if SensitiveDomainPermission().can():
# User has permission, no need to check anything
return
whitelist = current_app.config.get('LEMUR_WHITELISTED_DOMAINS', [])
if whitelist and not any(re.match(pattern, domain) for pattern in whitelist):
raise ValidationError('Domain {0} does not match whitelisted domain patterns. '
'Contact an administrator to issue the certificate.'.format(domain))
if any(d.sensitive for d in domain_service.get_by_name(domain)):
raise ValidationError('Domain {0} has been marked as sensitive. '
'Contact an administrator to issue the certificate.'.format(domain))
def encoding(oid_encoding):
"""
Determines if the specified oid type is valid.
:param oid_encoding:
:return:
"""
valid_types = ['b64asn1', 'string', 'ia5string']
if oid_encoding.lower() not in [o_type.lower() for o_type in valid_types]:
raise ValidationError('Invalid Oid Encoding: {0} choose from {1}'.format(oid_encoding, ",".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 and allowed.
:param data:
:return:
"""
try:
request = x509.load_pem_x509_csr(data.encode('utf-8'), default_backend())
except Exception:
raise ValidationError('CSR presented is not valid.')
# Validate common name and SubjectAltNames
for name in request.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
common_name(name.value)
try:
alt_names = request.extensions.get_extension_for_class(x509.SubjectAlternativeName)
for name in alt_names.value.get_values_for_type(x509.DNSName):
sensitive_domain(name)
except x509.ExtensionNotFound:
pass
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_start') and data.get('validity_end'):
if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True):
if is_weekend(data.get('validity_end')):
raise ValidationError('Validity end must not land on a weekend.')
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').date() < data['authority'].authority_certificate.not_before.date():
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
if data.get('validity_end').date() > data['authority'].authority_certificate.not_after.date():
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
return data

View File

@ -1,8 +1,34 @@
""" """
.. module: lemur.constants .. module: lemur.constants
:copyright: (c) 2015 by Netflix Inc. :copyright: (c) 2018 by Netflix Inc.
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
""" """
SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}" SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}"
DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}" DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}"
NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}" NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}"
SUCCESS_METRIC_STATUS = 'success'
FAILURE_METRIC_STATUS = 'failure'
CERTIFICATE_KEY_TYPES = [
'RSA2048',
'RSA4096',
'ECCPRIME192V1',
'ECCPRIME256V1',
'ECCSECP192R1',
'ECCSECP224R1',
'ECCSECP256R1',
'ECCSECP384R1',
'ECCSECP521R1',
'ECCSECP256K1',
'ECCSECT163K1',
'ECCSECT233K1',
'ECCSECT283K1',
'ECCSECT409K1',
'ECCSECT571K1',
'ECCSECT163R2',
'ECCSECT233R1',
'ECCSECT283R1',
'ECCSECT409R1',
'ECCSECT571R2'
]

View File

@ -4,14 +4,15 @@
:synopsis: This module contains all of the database related methods :synopsis: This module contains all of the database related methods
needed for lemur to interact with a datastore needed for lemur to interact with a datastore
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from sqlalchemy import exc from inflection import underscore
from sqlalchemy import exc, func
from sqlalchemy.sql import and_, or_ from sqlalchemy.sql import and_, or_
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm import make_transient
from lemur.extensions import db from lemur.extensions import db
from lemur.exceptions import AttrNotFound, DuplicateError from lemur.exceptions import AttrNotFound, DuplicateError
@ -75,6 +76,16 @@ def add(model):
db.session.add(model) db.session.add(model)
def get_model_column(model, field):
if field in getattr(model, 'sensitive_fields', ()):
raise AttrNotFound(field)
column = model.__table__.columns._data.get(field, None)
if column is None:
raise AttrNotFound(field)
return column
def find_all(query, model, kwargs): def find_all(query, model, kwargs):
""" """
Returns a query object that ensures that all kwargs Returns a query object that ensures that all kwargs
@ -91,7 +102,7 @@ def find_all(query, model, kwargs):
if not isinstance(value, list): if not isinstance(value, list):
value = value.split(',') value = value.split(',')
conditions.append(getattr(model, attr).in_(value)) conditions.append(get_model_column(model, attr).in_(value))
return query.filter(and_(*conditions)) return query.filter(and_(*conditions))
@ -108,7 +119,7 @@ def find_any(query, model, kwargs):
""" """
or_args = [] or_args = []
for attr, value in kwargs.items(): for attr, value in kwargs.items():
or_args.append(or_(getattr(model, attr) == value)) or_args.append(or_(get_model_column(model, attr) == value))
exprs = or_(*or_args) exprs = or_(*or_args)
return query.filter(exprs) return query.filter(exprs)
@ -123,10 +134,7 @@ def get(model, value, field="id"):
:return: :return:
""" """
query = session_query(model) query = session_query(model)
try: return query.filter(get_model_column(model, field) == value).scalar()
return query.filter(getattr(model, field) == value).one()
except NoResultFound as e:
return
def get_all(model, value, field="id"): def get_all(model, value, field="id"):
@ -139,7 +147,7 @@ def get_all(model, value, field="id"):
:return: :return:
""" """
query = session_query(model) query = session_query(model)
return query.filter(getattr(model, field) == value) return query.filter(get_model_column(model, field) == value)
def create(model): def create(model):
@ -191,7 +199,8 @@ def filter(query, model, terms):
:param terms: :param terms:
:return: :return:
""" """
return query.filter(getattr(model, terms[0]).ilike('%{}%'.format(terms[1]))) column = get_model_column(model, underscore(terms[0]))
return query.filter(column.ilike('%{}%'.format(terms[1])))
def sort(query, model, field, direction): def sort(query, model, field, direction):
@ -204,13 +213,8 @@ def sort(query, model, field, direction):
:param field: :param field:
:param direction: :param direction:
""" """
try: column = get_model_column(model, underscore(field))
field = getattr(model, field) return query.order_by(column.desc() if direction == 'desc' else column.asc())
direction = getattr(field, direction)
query = query.order_by(direction())
return query
except AttributeError:
raise AttrNotFound(field)
def paginate(query, page, count): def paginate(query, page, count):
@ -237,9 +241,6 @@ def update_list(model, model_attr, item_model, items):
""" """
ids = [] ids = []
for i in items:
ids.append(i['id'])
for i in getattr(model, model_attr): for i in getattr(model, model_attr):
if i.id not in ids: if i.id not in ids:
getattr(model, model_attr).remove(i) getattr(model, model_attr).remove(i)
@ -254,6 +255,29 @@ def update_list(model, model_attr, item_model, items):
return model 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 get_count(q):
"""
Count the number of rows in a table. More efficient than count(*)
:param q:
:return:
"""
count_q = q.statement.with_only_columns([func.count()]).order_by(None)
count = q.session.execute(count_q).scalar()
return count
def sort_and_page(query, model, args): def sort_and_page(query, model, args):
""" """
Helper that allows us to combine sorting and paging Helper that allows us to combine sorting and paging
@ -268,9 +292,17 @@ def sort_and_page(query, model, args):
page = args.pop('page') page = args.pop('page')
count = args.pop('count') count = args.pop('count')
if args.get('user'):
user = args.pop('user')
query = find_all(query, model, args) query = find_all(query, model, args)
if sort_by and sort_dir: if sort_by and sort_dir:
query = sort(query, model, sort_by, sort_dir) query = sort(query, model, sort_by, sort_dir)
return paginate(query, page, count) total = get_count(query)
# offset calculated at zero
page -= 1
items = query.offset(count * page).limit(count).all()
return dict(items=items, total=total)

View File

@ -1,56 +0,0 @@
"""
.. module: lemur.decorators
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from builtins import str
from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper
# this is only used for dev
def crossdomain(origin=None, methods=None, headers=None,
max_age=21600, attach_to_all=True,
automatic_options=True): # pragma: no cover
if methods is not None:
methods = ', '.join(sorted(x.upper() for x in methods))
if headers is not None and not isinstance(headers, str):
headers = ', '.join(x.upper() for x in headers)
if not isinstance(origin, str):
origin = ', '.join(origin)
if isinstance(max_age, timedelta):
max_age = max_age.total_seconds()
def get_methods():
if methods is not None:
return methods
options_resp = current_app.make_default_options_response()
return options_resp.headers['allow']
def decorator(f):
def wrapped_function(*args, **kwargs):
if automatic_options and request.method == 'OPTIONS':
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
if not attach_to_all and request.method != 'OPTIONS':
return resp
h = resp.headers
h['Access-Control-Allow-Origin'] = origin
h['Access-Control-Allow-Methods'] = get_methods()
h['Access-Control-Max-Age'] = str(max_age)
h['Access-Control-Allow-Headers'] = "Origin, X-Requested-With, Content-Type, Accept, Authorization "
h['Access-Control-Allow-Credentials'] = 'true'
return resp
f.provide_automatic_options = False
return update_wrapper(wrapped_function, f)
return decorator

View File

@ -3,15 +3,13 @@
import os import os
_basedir = os.path.abspath(os.path.dirname(__file__)) _basedir = os.path.abspath(os.path.dirname(__file__))
ADMINS = frozenset([''])
THREADS_PER_PAGE = 8 THREADS_PER_PAGE = 8
# General # General
# These will need to be set to `True` if you are developing locally # These will need to be set to `True` if you are developing locally
CORS = False CORS = False
debug = False DEBUG = False
# Logging # Logging

23
lemur/defaults/schemas.py Normal file
View File

@ -0,0 +1,23 @@
"""
.. module: lemur.defaults.schemas
:platform: unix
:copyright: (c) 2018 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 LemurOutputSchema
from lemur.authorities.schemas import AuthorityNestedOutputSchema
class DefaultOutputSchema(LemurOutputSchema):
authority = fields.Nested(AuthorityNestedOutputSchema)
country = fields.String()
state = fields.String()
location = fields.String()
organization = fields.String()
organizational_unit = fields.String()
issuer_plugin = fields.String()
default_output_schema = DefaultOutputSchema()

View File

@ -1,13 +1,17 @@
""" """
.. module: lemur.status.views .. module: lemur.defaults.views
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
""" """
from flask import current_app, Blueprint from flask import current_app, Blueprint
from flask.ext.restful import Api from flask_restful import Api
from lemur.common.schema import validate_schema
from lemur.authorities.service import get_by_name
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.defaults.schemas import default_output_schema
mod = Blueprint('default', __name__) mod = Blueprint('default', __name__)
api = Api(mod) api = Api(mod)
@ -18,6 +22,7 @@ class LemurDefaults(AuthenticatedResource):
def __init__(self): def __init__(self):
super(LemurDefaults) super(LemurDefaults)
@validate_schema(None, default_output_schema)
def get(self): def get(self):
""" """
.. http:get:: /defaults .. http:get:: /defaults
@ -45,19 +50,26 @@ class LemurDefaults(AuthenticatedResource):
"state": "CA", "state": "CA",
"location": "Los Gatos", "location": "Los Gatos",
"organization": "Netflix", "organization": "Netflix",
"organizationalUnit": "Operations" "organizationalUnit": "Operations",
"dnsProviders": [{"name": "test", ...}, {...}],
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
""" """
default_authority = get_by_name(current_app.config.get('LEMUR_DEFAULT_AUTHORITY'))
return dict( return dict(
country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'), country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
state=current_app.config.get('LEMUR_DEFAULT_STATE'), state=current_app.config.get('LEMUR_DEFAULT_STATE'),
location=current_app.config.get('LEMUR_DEFAULT_LOCATION'), location=current_app.config.get('LEMUR_DEFAULT_LOCATION'),
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'), organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT') organizational_unit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
issuer_plugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN'),
authority=default_authority,
) )
api.add_resource(LemurDefaults, '/defaults', endpoint='default') api.add_resource(LemurDefaults, '/defaults', endpoint='default')

View File

View File

@ -0,0 +1,15 @@
from lemur import database
def rotate_certificate(endpoint, new_cert):
"""
Rotates a certificate on a given endpoint.
:param endpoint:
:param new_cert:
:return:
"""
# ensure that certificate is available for rotation
endpoint.source.plugin.update_endpoint(endpoint, new_cert)
endpoint.certificate = new_cert
database.update(endpoint)

View File

@ -1,11 +1,10 @@
""" """
.. module: lemur.destinations.models .. module: lemur.destinations.models
:platform: unix :platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
import copy
from sqlalchemy import Column, Integer, String, Text from sqlalchemy import Column, Integer, String, Text
from sqlalchemy_utils import JSONType from sqlalchemy_utils import JSONType
from lemur.database import db from lemur.database import db
@ -23,7 +22,7 @@ class Destination(db.Model):
@property @property
def plugin(self): def plugin(self):
p = plugins.get(self.plugin_name) return plugins.get(self.plugin_name)
c = copy.deepcopy(p)
c.options = self.options def __repr__(self):
return c return "Destination(label={label})".format(label=self.label)

View File

@ -0,0 +1,43 @@
"""
.. module: lemur.destinations.schemas
:platform: unix
:copyright: (c) 2018 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):
if data:
data['plugin']['pluginOptions'] = data['options']
return data
class DestinationNestedOutputSchema(DestinationOutputSchema):
__envelope__ = False
destination_input_schema = DestinationInputSchema()
destinations_output_schema = DestinationOutputSchema(many=True)
destination_output_schema = DestinationOutputSchema()

View File

@ -1,7 +1,7 @@
""" """
.. module: lemur.destinations.service .. module: lemur.destinations.service
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
@ -22,6 +22,11 @@ def create(label, plugin_name, options, description=None):
:rtype : Destination :rtype : Destination
:return: New destination :return: New destination
""" """
# remove any sub-plugin objects before try to save the json options
for option in options:
if 'plugin' in option['type']:
del option['value']['plugin_object']
destination = Destination(label=label, options=options, plugin_name=plugin_name, description=description) destination = Destination(label=label, options=options, plugin_name=plugin_name, description=description)
return database.create(destination) return database.create(destination)
@ -56,7 +61,7 @@ def delete(destination_id):
def get(destination_id): def get(destination_id):
""" """
Retrieves an destination by it's lemur assigned ID. Retrieves an destination by its lemur assigned ID.
:param destination_id: Lemur assigned ID :param destination_id: Lemur assigned ID
:rtype : Destination :rtype : Destination
@ -67,7 +72,7 @@ def get(destination_id):
def get_by_label(label): def get_by_label(label):
""" """
Retrieves a destination by it's label Retrieves a destination by its label
:param label: :param label:
:return: :return:
@ -86,10 +91,6 @@ def get_all():
def render(args): 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') filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None) certificate_id = args.pop('certificate_id', None)
@ -103,12 +104,7 @@ def render(args):
terms = filt.split(';') terms = filt.split(';')
query = database.filter(query, Destination, terms) query = database.filter(query, Destination, terms)
query = database.find_all(query, Destination, args) return database.sort_and_page(query, Destination, args)
if sort_by and sort_dir:
query = database.sort(query, Destination, sort_by, sort_dir)
return database.paginate(query, page, count)
def stats(**kwargs): def stats(**kwargs):

View File

@ -2,39 +2,33 @@
.. module: lemur.destinations.views .. module: lemur.destinations.views
:platform: Unix :platform: Unix
:synopsis: This module contains all of the accounts view code. :synopsis: This module contains all of the accounts view code.
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import Blueprint from flask import Blueprint
from flask.ext.restful import Api, reqparse, fields from flask_restful import Api, reqparse
from lemur.destinations import service from lemur.destinations import service
from lemur.auth.service import AuthenticatedResource from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import admin_permission 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__) mod = Blueprint('destinations', __name__)
api = Api(mod) 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): class DestinationsList(AuthenticatedResource):
""" Defines the 'destinations' endpoint """ """ Defines the 'destinations' endpoint """
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(DestinationsList, self).__init__() super(DestinationsList, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, destinations_output_schema)
def get(self): def get(self):
""" """
.. http:get:: /destinations .. http:get:: /destinations
@ -58,32 +52,40 @@ class DestinationsList(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"items": [ "items": [{
{ "description": "test",
"destinationOptions": [ "options": [{
{ "name": "accountNumber",
"name": "accountNumber", "required": true,
"required": true, "value": "111111111111111",
"value": 111111111112, "helpMessage": "Must be a valid AWS account number!",
"helpMessage": "Must be a valid AWS account number!", "validation": "/^[0-9]{12,12}$/",
"validation": "/^[0-9]{12,12}$/", "type": "str"
"type": "int" }],
} "id": 4,
], "plugin": {
"pluginName": "aws-destination", "pluginOptions": [{
"id": 3, "name": "accountNumber",
"description": "test", "required": true,
"label": "test" "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 "total": 1
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int. default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair format is k;v
:query limit: limit number. default is 10 :query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """
@ -92,8 +94,8 @@ class DestinationsList(AuthenticatedResource):
return service.render(args) return service.render(args)
@admin_permission.require(http_exception=403) @admin_permission.require(http_exception=403)
@marshal_items(FIELDS) @validate_schema(destination_input_schema, destination_output_schema)
def post(self): def post(self, data=None):
""" """
.. http:post:: /destinations .. http:post:: /destinations
@ -108,20 +110,30 @@ class DestinationsList(AuthenticatedResource):
Accept: application/json, text/javascript 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", "name": "accountNumber",
"required": true, "required": true,
"value": 111111111112, "value": "34324324",
"helpMessage": "Must be a valid AWS account number!", "helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/", "validation": "/^[0-9]{12,12}$/",
"type": "int" "type": "str"
} }],
], "description": "Allow the uploading of certificates to AWS IAM",
"pluginName": "aws-destination", "slug": "aws-destination",
"id": 3, "title": "AWS"
"description": "test", },
"label": "test" "label": "test546"
} }
**Example response**: **Example response**:
@ -133,20 +145,30 @@ class DestinationsList(AuthenticatedResource):
Content-Type: text/javascript 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", "name": "accountNumber",
"required": true, "required": true,
"value": 111111111112, "value": "111111111111111",
"helpMessage": "Must be a valid AWS account number!", "helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/", "validation": "/^[0-9]{12,12}$/",
"type": "int" "type": "str"
} }],
], "description": "Allow the uploading of certificates to AWS IAM",
"pluginName": "aws-destination", "slug": "aws-destination",
"id": 3, "title": "AWS"
"description": "test", },
"label": "test" "label": "test546"
} }
:arg label: human readable account label :arg label: human readable account label
@ -154,12 +176,7 @@ class DestinationsList(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """
self.reqparse.add_argument('label', type=str, location='json', required=True) return service.create(data['label'], data['plugin']['slug'], data['plugin']['plugin_options'], data['description'])
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'])
class Destinations(AuthenticatedResource): class Destinations(AuthenticatedResource):
@ -167,7 +184,7 @@ class Destinations(AuthenticatedResource):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(Destinations, self).__init__() super(Destinations, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, destination_output_schema)
def get(self, destination_id): def get(self, destination_id):
""" """
.. http:get:: /destinations/1 .. http:get:: /destinations/1
@ -191,20 +208,30 @@ class Destinations(AuthenticatedResource):
Content-Type: text/javascript 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", "name": "accountNumber",
"required": true, "required": true,
"value": 111111111112, "value": "111111111111111",
"helpMessage": "Must be a valid AWS account number!", "helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/", "validation": "/^[0-9]{12,12}$/",
"type": "int" "type": "str"
} }],
], "description": "Allow the uploading of certificates to AWS IAM",
"pluginName": "aws-destination", "slug": "aws-destination",
"id": 3, "title": "AWS"
"description": "test", },
"label": "test" "label": "test546"
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
@ -213,8 +240,8 @@ class Destinations(AuthenticatedResource):
return service.get(destination_id) return service.get(destination_id)
@admin_permission.require(http_exception=403) @admin_permission.require(http_exception=403)
@marshal_items(FIELDS) @validate_schema(destination_input_schema, destination_output_schema)
def put(self, destination_id): def put(self, destination_id, data=None):
""" """
.. http:put:: /destinations/1 .. http:put:: /destinations/1
@ -228,23 +255,35 @@ class Destinations(AuthenticatedResource):
Host: example.com Host: example.com
Accept: application/json, text/javascript 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", "name": "accountNumber",
"required": true, "required": true,
"value": 111111111112, "value": "34324324",
"helpMessage": "Must be a valid AWS account number!", "helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/", "validation": "/^[0-9]{12,12}$/",
"type": "int" "type": "str"
} }],
], "description": "Allow the uploading of certificates to AWS IAM",
"pluginName": "aws-destination", "slug": "aws-destination",
"id": 3, "title": "AWS"
"description": "test", },
"label": "test" "label": "test546"
} }
**Example response**: **Example response**:
.. sourcecode:: http .. sourcecode:: http
@ -254,20 +293,30 @@ class Destinations(AuthenticatedResource):
Content-Type: text/javascript 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", "name": "accountNumber",
"required": true, "required": true,
"value": 111111111112, "value": "111111111111111",
"helpMessage": "Must be a valid AWS account number!", "helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/", "validation": "/^[0-9]{12,12}$/",
"type": "int" "type": "str"
} }],
], "description": "Allow the uploading of certificates to AWS IAM",
"pluginName": "aws-destination", "slug": "aws-destination",
"id": 3, "title": "AWS"
"description": "test", },
"label": "test" "label": "test546"
} }
:arg accountNumber: aws account number :arg accountNumber: aws account number
@ -276,12 +325,7 @@ class Destinations(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """
self.reqparse.add_argument('label', type=str, location='json', required=True) return service.update(destination_id, data['label'], data['plugin']['plugin_options'], data['description'])
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'])
@admin_permission.require(http_exception=403) @admin_permission.require(http_exception=403)
def delete(self, destination_id): def delete(self, destination_id):
@ -294,7 +338,7 @@ class CertificateDestinations(AuthenticatedResource):
def __init__(self): def __init__(self):
super(CertificateDestinations, self).__init__() super(CertificateDestinations, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, destination_output_schema)
def get(self, certificate_id): def get(self, certificate_id):
""" """
.. http:get:: /certificates/1/destinations .. http:get:: /certificates/1/destinations
@ -318,32 +362,40 @@ class CertificateDestinations(AuthenticatedResource):
Content-Type: text/javascript Content-Type: text/javascript
{ {
"items": [ "items": [{
{ "description": "test",
"destinationOptions": [ "options": [{
{ "name": "accountNumber",
"name": "accountNumber", "required": true,
"required": true, "value": "111111111111111",
"value": 111111111112, "helpMessage": "Must be a valid AWS account number!",
"helpMessage": "Must be a valid AWS account number!", "validation": "/^[0-9]{12,12}$/",
"validation": "/^[0-9]{12,12}$/", "type": "str"
"type": "int" }],
} "id": 4,
], "plugin": {
"pluginName": "aws-destination", "pluginOptions": [{
"id": 3, "name": "accountNumber",
"description": "test", "required": true,
"label": "test" "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 "total": 1
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair format is k;v
:query limit: limit number. default is 10 :query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
""" """

View File

@ -0,0 +1,37 @@
from sqlalchemy import Column, Integer, String, text, Text
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy_utils import ArrowType
from lemur.database import db
from lemur.plugins.base import plugins
from lemur.utils import Vault
class DnsProvider(db.Model):
__tablename__ = 'dns_providers'
id = Column(
Integer(),
primary_key=True,
)
name = Column(String(length=256), unique=True, nullable=True)
description = Column(Text(), nullable=True)
provider_type = Column(String(length=256), nullable=True)
credentials = Column(Vault, nullable=True)
api_endpoint = Column(String(length=256), nullable=True)
date_created = Column(ArrowType(), server_default=text('now()'), nullable=False)
status = Column(String(length=128), nullable=True)
options = Column(JSON, nullable=True)
domains = Column(JSON, nullable=True)
def __init__(self, name, description, provider_type, credentials):
self.name = name
self.description = description
self.provider_type = provider_type
self.credentials = credentials
@property
def plugin(self):
return plugins.get(self.plugin_name)
def __repr__(self):
return "DnsProvider(name={name})".format(name=self.name)

View File

@ -0,0 +1,27 @@
from lemur.common.fields import ArrowDateTime
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from marshmallow import fields
class DnsProvidersNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
name = fields.String()
providerType = fields.String()
description = fields.String()
credentials = fields.String()
api_endpoint = fields.String()
date_created = ArrowDateTime()
class DnsProvidersNestedInputSchema(LemurInputSchema):
__envelope__ = False
name = fields.String()
description = fields.String()
provider_type = fields.Dict()
dns_provider_output_schema = DnsProvidersNestedOutputSchema()
dns_provider_input_schema = DnsProvidersNestedInputSchema()

View File

@ -0,0 +1,111 @@
import json
from flask import current_app
from lemur import database
from lemur.dns_providers.models import DnsProvider
def render(args):
"""
Helper that helps us render the REST Api responses.
:param args:
:return:
"""
query = database.session_query(DnsProvider)
return database.sort_and_page(query, DnsProvider, args)
def get(dns_provider_id):
provider = database.get(DnsProvider, dns_provider_id)
return provider
def get_friendly(dns_provider_id):
"""
Retrieves a dns provider by its lemur assigned ID.
:param dns_provider_id: Lemur assigned ID
:rtype : DnsProvider
:return:
"""
dns_provider = get(dns_provider_id)
dns_provider_friendly = {
"name": dns_provider.name,
"description": dns_provider.description,
"providerType": dns_provider.provider_type,
"options": dns_provider.options,
"credentials": dns_provider.credentials,
}
if dns_provider.provider_type == "route53":
dns_provider_friendly["account_id"] = json.loads(dns_provider.credentials).get("account_id")
return dns_provider_friendly
def delete(dns_provider_id):
"""
Deletes a DNS provider.
:param dns_provider_id: Lemur assigned ID
"""
database.delete(get(dns_provider_id))
def get_types():
provider_config = current_app.config.get(
'ACME_DNS_PROVIDER_TYPES',
{"items": [
{
'name': 'route53',
'requirements': [
{
'name': 'account_id',
'type': 'int',
'required': True,
'helpMessage': 'AWS Account number'
},
]
},
{
'name': 'cloudflare',
'requirements': [
{
'name': 'email',
'type': 'str',
'required': True,
'helpMessage': 'Cloudflare Email'
},
{
'name': 'key',
'type': 'str',
'required': True,
'helpMessage': 'Cloudflare Key'
},
]
},
{
'name': 'dyn',
},
]}
)
if not provider_config:
raise Exception("No DNS Provider configuration specified.")
provider_config["total"] = len(provider_config.get("items"))
return provider_config
def create(data):
provider_name = data.get("name")
credentials = {}
for item in data.get("provider_type", {}).get("requirements", []):
credentials[item["name"]] = item["value"]
dns_provider = DnsProvider(
name=provider_name,
description=data.get("description"),
provider_type=data.get("provider_type").get("name"),
credentials=json.dumps(credentials),
)
created = database.create(dns_provider)
return created.id

View File

@ -0,0 +1,170 @@
"""
.. module: lemur.dns)providers.views
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
"""
from flask import Blueprint, g
from flask_restful import reqparse, Api
from lemur.auth.permissions import admin_permission
from lemur.auth.service import AuthenticatedResource
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
from lemur.dns_providers import service
from lemur.dns_providers.schemas import dns_provider_output_schema, dns_provider_input_schema
mod = Blueprint('dns_providers', __name__)
api = Api(mod)
class DnsProvidersList(AuthenticatedResource):
""" Defines the 'dns_providers' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(DnsProvidersList, self).__init__()
@validate_schema(None, dns_provider_output_schema)
def get(self):
"""
.. http:get:: /dns_providers
The current list of DNS Providers
**Example request**:
.. sourcecode:: http
GET /dns_providers 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": [{
"id": 1,
"name": "test",
"description": "test",
"provider_type": "dyn",
"status": "active",
}],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc 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
"""
parser = paginated_parser.copy()
parser.add_argument('dns_provider_id', type=int, location='args')
parser.add_argument('name', type=str, location='args')
parser.add_argument('type', type=str, location='args')
args = parser.parse_args()
args['user'] = g.user
return service.render(args)
@validate_schema(dns_provider_input_schema, None)
@admin_permission.require(http_exception=403)
def post(self, data=None):
"""
Creates a DNS Provider
**Example request**:
{
"providerType": {
"name": "route53",
"requirements": [
{
"name": "account_id",
"type": "int",
"required": true,
"helpMessage": "AWS Account number",
"value": 12345
}
],
"route": "dns_provider_options",
"reqParams": null,
"restangularized": true,
"fromServer": true,
"parentResource": null,
"restangularCollection": false
},
"name": "provider_name",
"description": "provider_description"
}
**Example request 2**
{
"providerType": {
"name": "cloudflare",
"requirements": [
{
"name": "email",
"type": "str",
"required": true,
"helpMessage": "Cloudflare Email",
"value": "test@example.com"
},
{
"name": "key",
"type": "str",
"required": true,
"helpMessage": "Cloudflare Key",
"value": "secretkey"
}
],
"route": "dns_provider_options",
"reqParams": null,
"restangularized": true,
"fromServer": true,
"parentResource": null,
"restangularCollection": false
},
"name": "provider_name",
"description": "provider_description"
}
:return:
"""
return service.create(data)
class DnsProviders(AuthenticatedResource):
@validate_schema(None, dns_provider_output_schema)
def get(self, dns_provider_id):
return service.get_friendly(dns_provider_id)
@admin_permission.require(http_exception=403)
def delete(self, dns_provider_id):
service.delete(dns_provider_id)
return {'result': True}
class DnsProviderOptions(AuthenticatedResource):
""" Defines the 'dns_provider_types' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(DnsProviderOptions, self).__init__()
def get(self):
return service.get_types()
api.add_resource(DnsProvidersList, '/dns_providers', endpoint='dns_providers')
api.add_resource(DnsProviders, '/dns_providers/<int:dns_provider_id>', endpoint='dns_provider')
api.add_resource(DnsProviderOptions, '/dns_provider_options', endpoint='dns_provider_options')

View File

@ -1,13 +1,13 @@
""" """
.. module: lemur.domains.models .. module: lemur.domains.models
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from sqlalchemy import Column, Integer, String from sqlalchemy import Column, Integer, String, Boolean
from lemur.database import db from lemur.database import db
@ -16,11 +16,7 @@ class Domain(db.Model):
__tablename__ = 'domains' __tablename__ = 'domains'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String(256)) name = Column(String(256))
sensitive = Column(Boolean, default=False)
def as_dict(self): def __repr__(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns} return "Domain(name={name})".format(name=self.name)
def serialize(self):
blob = self.as_dict()
blob['certificates'] = [x.id for x in self.certificate]
return blob

35
lemur/domains/schemas.py Normal file
View File

@ -0,0 +1,35 @@
"""
.. module: lemur.domains.schemas
:platform: unix
:copyright: (c) 2018 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(missing=False)
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
class DomainOutputSchema(LemurOutputSchema):
id = fields.Integer()
name = fields.String()
sensitive = fields.Boolean()
# certificates = fields.Nested(CertificateNestedOutputSchema, many=True, missing=[])
class DomainNestedOutputSchema(DomainOutputSchema):
__envelope__ = False
domain_input_schema = DomainInputSchema()
domain_output_schema = DomainOutputSchema()
domains_output_schema = DomainOutputSchema(many=True)

View File

@ -1,7 +1,7 @@
""" """
.. module: lemur.domains.service .. module: lemur.domains.service
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -32,6 +32,43 @@ def get_all():
return database.find_all(query, Domain, {}).all() return database.find_all(query, Domain, {}).all()
def get_by_name(name):
"""
Fetches domain by its 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): def render(args):
""" """
Helper to parse REST Api requests Helper to parse REST Api requests
@ -39,12 +76,7 @@ def render(args):
:param args: :param args:
:return: :return:
""" """
query = database.session_query(Domain).join(Certificate, Domain.certificate) query = database.session_query(Domain)
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter') filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None) certificate_id = args.pop('certificate_id', None)
@ -53,11 +85,7 @@ def render(args):
query = database.filter(query, Domain, terms) query = database.filter(query, Domain, terms)
if certificate_id: if certificate_id:
query = query.join(Certificate, Domain.certificates)
query = query.filter(Certificate.id == certificate_id) query = query.filter(Certificate.id == certificate_id)
query = database.find_all(query, Domain, args) return database.sort_and_page(query, Domain, args)
if sort_by and sort_dir:
query = database.sort(query, Domain, sort_by, sort_dir)
return database.paginate(query, page, count)

View File

@ -1,24 +1,23 @@
""" """
.. module: lemur.domains.views .. module: lemur.domains.views
:platform: Unix :platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from flask import Blueprint from flask import Blueprint
from flask.ext.restful import reqparse, Api, fields from flask_restful import reqparse, Api
from lemur.domains import service from lemur.domains import service
from lemur.auth.service import AuthenticatedResource 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 = { from lemur.domains.schemas import domain_input_schema, domain_output_schema, domains_output_schema
'id': fields.Integer,
'name': fields.String
}
mod = Blueprint('domains', __name__) mod = Blueprint('domains', __name__)
api = Api(mod) api = Api(mod)
@ -29,7 +28,7 @@ class DomainsList(AuthenticatedResource):
def __init__(self): def __init__(self):
super(DomainsList, self).__init__() super(DomainsList, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, domains_output_schema)
def get(self): def get(self):
""" """
.. http:get:: /domains .. http:get:: /domains
@ -57,20 +56,22 @@ class DomainsList(AuthenticatedResource):
{ {
"id": 1, "id": 1,
"name": "www.example.com", "name": "www.example.com",
"sensitive": false
}, },
{ {
"id": 2, "id": 2,
"name": "www.example2.com", "name": "www.example2.com",
"sensitive": false
} }
] ]
"total": 2 "total": 2
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair format is k;v
:query limit: limit number. default is 10 :query count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated
@ -79,13 +80,58 @@ class DomainsList(AuthenticatedResource):
args = parser.parse_args() args = parser.parse_args()
return service.render(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: asc 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): class Domains(AuthenticatedResource):
def __init__(self): def __init__(self):
self.reqparse = reqparse.RequestParser() self.reqparse = reqparse.RequestParser()
super(Domains, self).__init__() super(Domains, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, domain_output_schema)
def get(self, domain_id): def get(self, domain_id):
""" """
.. http:get:: /domains/1 .. http:get:: /domains/1
@ -111,6 +157,7 @@ class Domains(AuthenticatedResource):
{ {
"id": 1, "id": 1,
"name": "www.example.com", "name": "www.example.com",
"sensitive": false
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
@ -119,13 +166,56 @@ class Domains(AuthenticatedResource):
""" """
return service.get(domain_id) 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): class CertificateDomains(AuthenticatedResource):
""" Defines the 'domains' endpoint """ """ Defines the 'domains' endpoint """
def __init__(self): def __init__(self):
super(CertificateDomains, self).__init__() super(CertificateDomains, self).__init__()
@marshal_items(FIELDS) @validate_schema(None, domains_output_schema)
def get(self, certificate_id): def get(self, certificate_id):
""" """
.. http:get:: /certificates/1/domains .. http:get:: /certificates/1/domains
@ -153,20 +243,22 @@ class CertificateDomains(AuthenticatedResource):
{ {
"id": 1, "id": 1,
"name": "www.example.com", "name": "www.example.com",
"sensitive": false
}, },
{ {
"id": 2, "id": 2,
"name": "www.example2.com", "name": "www.example2.com",
"sensitive": false
} }
] ]
"total": 2 "total": 2
} }
:query sortBy: field to sort on :query sortBy: field to sort on
:query sortDir: acs or desc :query sortDir: asc or desc
:query page: int. default is 1 :query page: int default is 1
:query filter: key value pair. format is k=v; :query filter: key value pair format is k;v
:query limit: limit number. default is 10 :query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error :statuscode 200: no error
:statuscode 403: unauthenticated :statuscode 403: unauthenticated

View File

43
lemur/endpoints/cli.py Normal file
View File

@ -0,0 +1,43 @@
"""
.. module: lemur.endpoints.cli
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask_script import Manager
import arrow
from datetime import timedelta
from sqlalchemy import cast
from sqlalchemy_utils import ArrowType
from lemur import database
from lemur.extensions import metrics, sentry
from lemur.endpoints.models import Endpoint
manager = Manager(usage="Handles all endpoint related tasks.")
@manager.option('-ttl', '--time-to-live', type=int, dest='ttl', default=2, help='Time in hours, which endpoint has not been refreshed to remove the endpoint.')
def expire(ttl):
"""
Removed all endpoints that have not been recently updated.
"""
print("[+] Staring expiration of old endpoints.")
try:
now = arrow.utcnow()
expiration = now - timedelta(hours=ttl)
endpoints = database.session_query(Endpoint).filter(cast(Endpoint.last_updated, ArrowType) <= expiration)
for endpoint in endpoints:
print("[!] Expiring endpoint: {name} Last Updated: {last_updated}".format(name=endpoint.name, last_updated=endpoint.last_updated))
database.delete(endpoint)
metrics.send('endpoint_expired', 'counter', 1)
print("[+] Finished expiration.")
except Exception as e:
sentry.captureException()

93
lemur/endpoints/models.py Normal file
View File

@ -0,0 +1,93 @@
"""
.. module: lemur.endpoints.models
:platform: unix
:synopsis: This module contains all of the models need to create an authority within Lemur.
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from sqlalchemy.orm import relationship
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import case
from sqlalchemy_utils import ArrowType
from lemur.database import db
from lemur.models import policies_ciphers
BAD_CIPHERS = [
'Protocol-SSLv3',
'Protocol-SSLv2',
'Protocol-TLSv1'
]
class Cipher(db.Model):
__tablename__ = 'ciphers'
id = Column(Integer, primary_key=True)
name = Column(String(128), nullable=False)
@hybrid_property
def deprecated(self):
return self.name in BAD_CIPHERS
@deprecated.expression
def deprecated(cls):
return case(
[
(cls.name in BAD_CIPHERS, True)
],
else_=False
)
class Policy(db.Model):
___tablename__ = 'policies'
id = Column(Integer, primary_key=True)
name = Column(String(128), nullable=True)
ciphers = relationship('Cipher', secondary=policies_ciphers, backref='policy')
class Endpoint(db.Model):
__tablename__ = 'endpoints'
id = Column(Integer, primary_key=True)
owner = Column(String(128))
name = Column(String(128))
dnsname = Column(String(256))
type = Column(String(128))
active = Column(Boolean, default=True)
port = Column(Integer)
policy_id = Column(Integer, ForeignKey('policy.id'))
policy = relationship('Policy', backref='endpoint')
certificate_id = Column(Integer, ForeignKey('certificates.id'))
source_id = Column(Integer, ForeignKey('sources.id'))
sensitive = Column(Boolean, default=False)
source = relationship('Source', back_populates='endpoints')
last_updated = Column(ArrowType, default=arrow.utcnow, nullable=False)
date_created = Column(ArrowType, default=arrow.utcnow, onupdate=arrow.utcnow, nullable=False)
replaced = association_proxy('certificate', 'replaced')
@property
def issues(self):
issues = []
for cipher in self.policy.ciphers:
if cipher.deprecated:
issues.append({'name': 'deprecated cipher', 'value': '{0} has been deprecated consider removing it.'.format(cipher.name)})
if self.certificate.expired:
issues.append({'name': 'expired certificate', 'value': 'There is an expired certificate attached to this endpoint consider replacing it.'})
if self.certificate.revoked:
issues.append({'name': 'revoked', 'value': 'There is a revoked certificate attached to this endpoint consider replacing it.'})
return issues
def __repr__(self):
return "Endpoint(name={name})".format(name=self.name)

View File

@ -0,0 +1,44 @@
"""
.. module: lemur.endpoints.schemas
:platform: unix
:copyright: (c) 2018 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 LemurOutputSchema
from lemur.certificates.schemas import CertificateNestedOutputSchema
class CipherNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
deprecated = fields.Boolean()
name = fields.String()
class PolicyNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
name = fields.String()
ciphers = fields.Nested(CipherNestedOutputSchema, many=True)
class EndpointOutputSchema(LemurOutputSchema):
id = fields.Integer()
description = fields.String()
name = fields.String()
dnsname = fields.String()
owner = fields.Email()
type = fields.String()
port = fields.Integer()
active = fields.Boolean()
certificate = fields.Nested(CertificateNestedOutputSchema)
policy = fields.Nested(PolicyNestedOutputSchema)
issues = fields.List(fields.Dict())
endpoint_output_schema = EndpointOutputSchema()
endpoints_output_schema = EndpointOutputSchema(many=True)

178
lemur/endpoints/service.py Normal file
View File

@ -0,0 +1,178 @@
"""
.. module: lemur.endpoints.service
:platform: Unix
:synopsis: This module contains all of the services level functions used to
administer endpoints in Lemur
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from sqlalchemy import func
from lemur import database
from lemur.common.utils import truthiness
from lemur.endpoints.models import Endpoint, Policy, Cipher
from lemur.extensions import metrics
def get_all():
"""
Get all endpoints that are currently in Lemur.
:rtype : List
:return:
"""
query = database.session_query(Endpoint)
return database.find_all(query, Endpoint, {}).all()
def get(endpoint_id):
"""
Retrieves an endpoint given it's ID
:param endpoint_id:
:return:
"""
return database.get(Endpoint, endpoint_id)
def get_by_name(name):
"""
Retrieves an endpoint given it's name.
:param name:
:return:
"""
return database.get(Endpoint, name, field='name')
def get_by_dnsname(dnsname):
"""
Retrieves an endpoint given it's name.
:param dnsname:
:return:
"""
return database.get(Endpoint, dnsname, field='dnsname')
def get_by_dnsname_and_port(dnsname, port):
"""
Retrieves and endpoint by it's dnsname and port.
:param dnsname:
:param port:
:return:
"""
return Endpoint.query.filter(Endpoint.dnsname == dnsname).filter(Endpoint.port == port).scalar()
def get_by_source(source_label):
"""
Retrieves all endpoints for a given source.
:param source_label:
:return:
"""
return Endpoint.query.filter(Endpoint.source.label == source_label).all() # noqa
def get_all_pending_rotation():
"""
Retrieves all endpoints which have certificates deployed
that have been replaced.
:return:
"""
return Endpoint.query.filter(Endpoint.replaced.any()).all()
def create(**kwargs):
"""
Creates a new endpoint.
:param kwargs:
:return:
"""
endpoint = Endpoint(**kwargs)
database.create(endpoint)
metrics.send('endpoint_added', 'counter', 1, metric_tags={'source': endpoint.source.label})
return endpoint
def get_or_create_policy(**kwargs):
policy = database.get(Policy, kwargs['name'], field='name')
if not policy:
policy = Policy(**kwargs)
database.create(policy)
return policy
def get_or_create_cipher(**kwargs):
cipher = database.get(Cipher, kwargs['name'], field='name')
if not cipher:
cipher = Cipher(**kwargs)
database.create(cipher)
return cipher
def update(endpoint_id, **kwargs):
endpoint = database.get(Endpoint, endpoint_id)
endpoint.policy = kwargs['policy']
endpoint.certificate = kwargs['certificate']
endpoint.source = kwargs['source']
endpoint.last_updated = arrow.utcnow()
metrics.send('endpoint_updated', 'counter', 1, metric_tags={'source': endpoint.source.label})
database.update(endpoint)
return endpoint
def render(args):
"""
Helper that helps us render the REST Api responses.
:param args:
:return:
"""
query = database.session_query(Endpoint)
filt = args.pop('filter')
if filt:
terms = filt.split(';')
if 'active' in filt: # this is really weird but strcmp seems to not work here??
query = query.filter(Endpoint.active == truthiness(terms[1]))
elif 'port' in filt:
if terms[1] != 'null': # ng-table adds 'null' if a number is removed
query = query.filter(Endpoint.port == terms[1])
elif 'ciphers' in filt:
query = query.filter(
Cipher.name == terms[1]
)
else:
query = database.filter(query, Endpoint, terms)
return database.sort_and_page(query, Endpoint, args)
def stats(**kwargs):
"""
Helper that defines some useful statistics about endpoints.
:param kwargs:
:return:
"""
attr = getattr(Endpoint, kwargs.get('metric'))
query = database.db.session.query(attr, func.count(attr))
items = query.group_by(attr).all()
keys = []
values = []
for key, count in items:
keys.append(key)
values.append(count)
return {'labels': keys, 'values': values}

107
lemur/endpoints/views.py Normal file
View File

@ -0,0 +1,107 @@
"""
.. module: lemur.endpoints.views
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint, g
from flask_restful import reqparse, Api
from lemur.common.utils import paginated_parser
from lemur.common.schema import validate_schema
from lemur.auth.service import AuthenticatedResource
from lemur.endpoints import service
from lemur.endpoints.schemas import endpoint_output_schema, endpoints_output_schema
mod = Blueprint('endpoints', __name__)
api = Api(mod)
class EndpointsList(AuthenticatedResource):
""" Defines the 'endpoints' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(EndpointsList, self).__init__()
@validate_schema(None, endpoints_output_schema)
def get(self):
"""
.. http:get:: /endpoints
The current list of endpoints
**Example request**:
.. sourcecode:: http
GET /endpoints 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
:query sortBy: field to sort on
:query sortDir: asc or desc
:query page: int default is 1
:query filter: key value pair. format is k;v
:query limit: limit number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
:note: this will only show certificates that the current user is authorized to use
"""
parser = paginated_parser.copy()
args = parser.parse_args()
args['user'] = g.current_user
return service.render(args)
class Endpoints(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Endpoints, self).__init__()
@validate_schema(None, endpoint_output_schema)
def get(self, endpoint_id):
"""
.. http:get:: /endpoints/1
One endpoint
**Example request**:
.. sourcecode:: http
GET /endpoints/1 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
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.get(endpoint_id)
api.add_resource(EndpointsList, '/endpoints', endpoint='endpoints')
api.add_resource(Endpoints, '/endpoints/<int:endpoint_id>', endpoint='endpoint')

View File

@ -1,14 +1,14 @@
""" """
.. module: lemur.exceptions .. module: lemur.exceptions
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
""" """
from flask import current_app from flask import current_app
class LemurException(Exception): class LemurException(Exception):
def __init__(self): def __init__(self, *args, **kwargs):
current_app.logger.error(self) current_app.logger.exception(self)
class DuplicateError(LemurException): class DuplicateError(LemurException):
@ -19,51 +19,26 @@ class DuplicateError(LemurException):
return repr("Duplicate found! Could not create: {0}".format(self.key)) return repr("Duplicate found! Could not create: {0}".format(self.key))
class AuthenticationFailedException(LemurException):
def __init__(self, remote_ip, user_agent):
self.remote_ip = remote_ip
self.user_agent = user_agent
def __str__(self):
return repr("Failed login from: {} {}".format(self.remote_ip, self.user_agent))
class IntegrityError(LemurException):
def __init__(self, message):
self.message = message
def __str__(self):
return repr(self.message)
class InvalidListener(LemurException): class InvalidListener(LemurException):
def __str__(self): def __str__(self):
return repr("Invalid listener, ensure you select a certificate if you are using a secure protocol") return repr("Invalid listener, ensure you select a certificate if you are using a secure protocol")
class CertificateUnavailable(LemurException):
def __str__(self):
return repr("The certificate requested is not available")
class AttrNotFound(LemurException): class AttrNotFound(LemurException):
def __init__(self, field): def __init__(self, field):
self.field = field self.field = field
def __str__(self): def __str__(self):
return repr("The field '{0}' is not sortable".format(self.field)) return repr("The field '{0}' is not sortable or filterable".format(self.field))
class NoPersistanceFound(Exception): class InvalidConfiguration(Exception):
def __str__(self): pass
return repr("No peristence method found, Lemur cannot persist sensitive information")
class NoEncryptionKeyFound(Exception): class InvalidAuthority(Exception):
def __str__(self): pass
return repr("Aborting... Lemur cannot locate db encryption key, is ENCRYPTION_KEY set?")
class InvalidToken(Exception): class UnknownProvider(Exception):
def __str__(self): pass
return repr("Invalid token")

View File

@ -1,19 +1,31 @@
""" """
.. module: lemur.extensions .. module: lemur.extensions
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
""" """
from flask.ext.sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() db = SQLAlchemy()
from flask.ext.migrate import Migrate from flask_migrate import Migrate
migrate = Migrate() migrate = Migrate()
from flask.ext.bcrypt import Bcrypt from flask_bcrypt import Bcrypt
bcrypt = Bcrypt() bcrypt = Bcrypt()
from flask.ext.principal import Principal from flask_principal import Principal
principal = Principal() principal = Principal(use_sessions=False)
from flask_mail import Mail from flask_mail import Mail
smtp_mail = Mail() smtp_mail = Mail()
from lemur.metrics import Metrics
metrics = Metrics()
from raven.contrib.flask import Sentry
sentry = Sentry()
from blinker import Namespace
signals = Namespace()
from flask_cors import CORS
cors = CORS()

View File

@ -4,7 +4,7 @@
:synopsis: This module contains all the needed functions to allow :synopsis: This module contains all the needed functions to allow
the factory app creation. the factory app creation.
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -14,12 +14,14 @@ import imp
import errno import errno
import pkg_resources import pkg_resources
from logging import Formatter from logging import Formatter, StreamHandler
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from flask import Flask from flask import Flask
from lemur.certificates.hooks import activate_debug_dump
from lemur.common.health import mod as health 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, sentry, cors
DEFAULT_BLUEPRINTS = ( DEFAULT_BLUEPRINTS = (
@ -73,7 +75,8 @@ def from_file(file_path, silent=False):
d.__file__ = file_path d.__file__ = file_path
try: try:
with open(file_path) as config_file: with open(file_path) as config_file:
exec(compile(config_file.read(), file_path, 'exec'), d.__dict__) exec(compile(config_file.read(), # nosec: config file safe
file_path, 'exec'), d.__dict__)
except IOError as e: except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR): if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False return False
@ -90,15 +93,22 @@ def configure_app(app, config=None):
:param config: :param config:
:return: :return:
""" """
try: # respect the config first
app.config.from_envvar("LEMUR_CONF") if config and config != 'None':
except RuntimeError: app.config['CONFIG_PATH'] = config
if config and config != 'None': app.config.from_object(from_file(config))
app.config.from_object(from_file(config)) else:
elif os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")): try:
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py"))) app.config.from_envvar("LEMUR_CONF")
else: except RuntimeError:
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py'))) # look in default paths
if os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")):
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py")))
else:
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
# we don't use this
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
def configure_extensions(app): def configure_extensions(app):
@ -112,6 +122,12 @@ def configure_extensions(app):
migrate.init_app(app, db) migrate.init_app(app, db)
principal.init_app(app) principal.init_app(app)
smtp_mail.init_app(app) smtp_mail.init_app(app)
metrics.init_app(app)
sentry.init_app(app)
if app.config['CORS']:
app.config['CORS_HEADERS'] = 'Content-Type'
cors.init_app(app, resources=r'/api/*', headers='Content-Type', origin='*', supports_credentials=True)
def configure_blueprints(app, blueprints): def configure_blueprints(app, blueprints):
@ -143,14 +159,22 @@ def configure_logging(app):
app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG')) app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
app.logger.addHandler(handler) app.logger.addHandler(handler)
stream_handler = StreamHandler()
stream_handler.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
app.logger.addHandler(stream_handler)
if app.config.get('DEBUG_DUMP', False):
activate_debug_dump()
def install_plugins(app): def install_plugins(app):
""" """
Installs new issuers that are not currently bundled with Lemur. Installs new issuers that are not currently bundled with Lemur.
:param settings: :param app:
:return: :return:
""" """
from lemur.plugins import plugins
from lemur.plugins.base import register from lemur.plugins.base import register
# entry_points={ # entry_points={
# 'lemur.plugins': [ # 'lemur.plugins': [
@ -165,3 +189,13 @@ def install_plugins(app):
app.logger.error("Failed to load plugin %r:\n%s\n" % (ep.name, traceback.format_exc())) app.logger.error("Failed to load plugin %r:\n%s\n" % (ep.name, traceback.format_exc()))
else: else:
register(plugin) register(plugin)
# ensure that we have some way to notify
with app.app_context():
slug = app.config.get("LEMUR_DEFAULT_NOTIFICATION_PLUGIN", "email-notification")
try:
plugins.get(slug)
except KeyError:
raise Exception("Unable to location notification plugin: {slug}. Ensure that "
"LEMUR_DEFAULT_NOTIFICATION_PLUGIN is set to a valid and installed notification plugin."
.format(slug=slug))

0
lemur/logs/__init__.py Normal file
View File

23
lemur/logs/models.py Normal file
View File

@ -0,0 +1,23 @@
"""
.. module: lemur.logs.models
:platform: unix
:synopsis: This module contains all of the models related private key audit log.
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import Column, Integer, ForeignKey, PassiveDefault, func, Enum
from sqlalchemy_utils.types.arrow import ArrowType
from lemur.database import db
class Log(db.Model):
__tablename__ = 'logs'
id = Column(Integer, primary_key=True)
certificate_id = Column(Integer, ForeignKey('certificates.id'))
log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', name='log_type'), nullable=False)
logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)

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