Compare commits

..

249 Commits
0.6.0 ... 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
193 changed files with 5492 additions and 796 deletions

View File

@ -1,6 +1,37 @@
Changelog
=========
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`
~~~~~~~~~~~~~~~~~~

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 Netflix, Inc.
Copyright 2018 Netflix, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,6 +1,6 @@
NPM_ROOT = ./node_modules
STATIC_DIR = src/lemur/static/app
SHELL=/bin/bash
USER := $(shell whoami)
develop: update-submodules setup-git
@ -104,4 +104,24 @@ coverage: develop
publish:
python setup.py sdist bdist_wheel upload
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,7 +5,7 @@ Lemur
:alt: Join the chat at https://gitter.im/Netflix/lemur
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. image:: https://readthedocs.io/projects/lemur/badge/?version=latest
.. image:: https://readthedocs.org/projects/lemur/badge/?version=latest
:target: https://lemur.readthedocs.io
:alt: Latest Docs
@ -14,6 +14,10 @@ Lemur
.. image:: https://travis-ci.org/Netflix/lemur.svg
:target: https://travis-ci.org/Netflix/lemur
.. image:: https://coveralls.io/repos/github/Netflix/lemur/badge.svg?branch=master
:target: https://coveralls.io/github/Netflix/lemur?branch=master
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.

View File

@ -10,6 +10,7 @@ services:
command: make test
environment:
SQLALCHEMY_DATABASE_URI: postgresql://lemur:lemur@postgres:5432/lemur
VIRTUAL_ENV: 'true'
postgres:
image: postgres:9.4

View File

@ -65,6 +65,36 @@ Basic Configuration
SQLALCHEMY_DATABASE_URI = 'postgresql://<user>:<password>@<hostname>:5432/lemur'
.. 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:
@ -518,6 +548,21 @@ For more information about how to use social logins, see: `Satellizer <https://g
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
-----------------------
@ -581,6 +626,12 @@ The following configuration properties are required to use the Digicert issuer p
This is the url for the Digicert API (e.g. https://www.digicert.com)
.. data:: DIGICERT_ORDER_TYPE
: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)
.. data:: DIGICERT_API_KEY
:noindex:
@ -1135,6 +1186,31 @@ Digicert
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.

View File

@ -59,7 +59,7 @@ master_doc = 'index'
# General information about the project.
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
# |version| and |release|, also used in various other places throughout 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
* virtualenv (ideally virtualenvwrapper)
* node.js (for npm and building css/javascript)
* (Optional) PostgreSQL
+* `PostgreSQL <https://lemur.readthedocs.io/en/latest/quickstart/index.html#setup-postgres>`_
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
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:

View File

@ -110,6 +110,5 @@ Subpackages
lemur.plugins.lemur_digicert
lemur.plugins.lemur_java
lemur.plugins.lemur_kubernetes
lemur.plugins.lemur_linuxdst
lemur.plugins.lemur_openssl
lemur.plugins.lemur_slack

View File

@ -100,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.
The `IssuerPlugin` exposes two functions::
The `IssuerPlugin` exposes four functions functions::
def create_certificate(self, csr, issuer_options):
# 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.
@ -139,6 +145,19 @@ 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
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
-----------

View File

@ -1,35 +0,0 @@
Flask==0.12
Flask-RESTful==0.3.6
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
Flask-Migrate==2.1.1
Flask-Bcrypt==0.7.1
Flask-Principal==0.4.0
Flask-Mail==0.9.1
SQLAlchemy-Utils==0.32.14
requests==2.11.1
ndg-httpsclient==0.4.2
psycopg2==2.7.3
arrow==0.10.0
six==1.10.0
marshmallow-sqlalchemy==0.13.1
gunicorn==19.7.1
marshmallow==2.13.6
cryptography==1.9
xmltodict==0.11.0
pyjwt==1.5.2
lockfile==0.12.2
inflection==0.3.1
future==0.16.0
boto3==1.4.6
acme==0.18.1
retrying==1.3.3
tabulate==0.7.7
pem==17.1.0
raven[flask]==6.1.0
jinja2==2.9.6
# pyldap==2.4.37 # cannot be installed on rtd - required by ldap auth provider
paramiko==2.2.1 # required for lemur_linuxdst plugin
sphinx
sphinxcontrib-httpdomain
sphinx-rtd-theme

View File

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

View File

@ -1,14 +1,15 @@
"""
.. module: lemur
: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.
.. 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.extensions import metrics
@ -27,6 +28,8 @@ 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 (
__author__, __copyright__, __email__, __license__, __summary__, __title__,
@ -53,7 +56,9 @@ LEMUR_BLUEPRINTS = (
sources_bp,
endpoints_bp,
logs_bp,
api_key_bp
api_key_bp,
pending_certificates_bp,
dns_providers_bp,
)
@ -71,17 +76,6 @@ def configure_hook(app):
"""
from flask import jsonify
from werkzeug.exceptions import HTTPException
from lemur.decorators import crossdomain
if app.config.get('CORS'):
@app.after_request
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
def after(response):
return response
@app.after_request
def log_status(response):
metrics.send('status_code_{}'.format(response.status_code), 'counter', 1)
return response
@app.errorhandler(Exception)
def handle_error(e):
@ -91,3 +85,29 @@ def configure_hook(app):
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
# Get elapsed time in milliseconds
elapsed = time.time() - g.request_start_time
elapsed = int(round(1000 * elapsed))
# 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

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.api_keys.cli
:platform: Unix
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""

View File

@ -2,7 +2,7 @@
.. 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) 2017 by Netflix Inc., see AUTHORS for more
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.api_keys.schemas
:platform: Unix
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.api_keys.service
:platform: Unix
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.api_keys.views
:platform: Unix
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Eric Coan <kungfury@instructure.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.auth.ldap
: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.
.. moduleauthor:: Ian Stahnke <ian.stahnke@myob.com>
"""

View File

@ -2,7 +2,7 @@
.. module: lemur.auth.permissions
:platform: Unix
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -3,7 +3,7 @@
:platform: Unix
:synopsis: This module contains all of the authentication duties for
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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.auth.views
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -14,6 +14,7 @@ from flask import Blueprint, current_app
from flask_restful import reqparse, Resource, Api
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
@ -116,7 +117,6 @@ def retrieve_user(user_api_url, access_token):
profile = r.json()
user = user_service.get_by_email(profile['email'])
metrics.send('successful_login', 'counter', 1)
return user, profile
@ -267,7 +267,7 @@ class Login(Resource):
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
metrics.send('successful_login', 'counter', 1)
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user))
# try ldap login
@ -279,16 +279,16 @@ class Login(Resource):
# Tell Flask-Principal the identity changed
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
metrics.send('successful_login', 'counter', 1)
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('invalid_login', 'counter', 1)
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
return dict(message=ldap_message), 403
# if not valid user - no certificates for you
metrics.send('invalid_login', 'counter', 1)
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
return dict(message='The supplied credentials are invalid'), 403
@ -337,14 +337,14 @@ class Ping(Resource):
roles = create_user_roles(profile)
update_user(user, profile, roles)
if not user.active:
metrics.send('invalid_login', 'counter', 1)
if not user or 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('successful_login', 'counter', 1)
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user))
@ -387,12 +387,14 @@ class OAuth2(Resource):
update_user(user, profile, roles)
if not user.active:
metrics.send('invalid_login', 'counter', 1)
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))
@ -431,15 +433,15 @@ class Google(Resource):
user = user_service.get_by_email(profile['email'])
if not user.active:
metrics.send('invalid_login', 'counter', 1)
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:
metrics.send('successful_login', 'counter', 1)
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
return dict(token=create_token(user))
metrics.send('invalid_login', 'counter', 1)
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
class Providers(Resource):

View File

@ -2,7 +2,7 @@
.. module: lemur.authorities.models
:platform: unix
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -32,6 +32,9 @@ class Authority(db.Model):
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')
authority_pending_certificate = relationship("PendingCertificate", backref='root_authority', uselist=False, foreign_keys='PendingCertificate.root_authority_id')
pending_certificates = relationship('PendingCertificate', backref='authority', foreign_keys='PendingCertificate.authority_id')
def __init__(self, **kwargs):
self.owner = kwargs['owner']
self.roles = kwargs.get('roles', [])
@ -39,6 +42,7 @@ class Authority(db.Model):
self.description = kwargs.get('description')
self.authority_certificate = kwargs['authority_certificate']
self.plugin_name = kwargs['plugin']['slug']
self.options = kwargs.get('options')
@property
def plugin(self):

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.authorities.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -3,12 +3,16 @@
:platform: Unix
:synopsis: This module contains all of the services level functions used to
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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import json
from lemur import database
from lemur.common.utils import truthiness
from lemur.extensions import metrics
from lemur.authorities.models import Authority
from lemur.roles import service as role_service
@ -106,6 +110,8 @@ def create(**kwargs):
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)
@ -170,8 +176,8 @@ def render(args):
if filt:
terms = filt.split(';')
if 'active' in filt: # this is really weird but strcmp seems to not work here??
query = query.filter(Authority.active == terms[1])
if 'active' in filt:
query = query.filter(Authority.active == truthiness(terms[1]))
else:
query = database.filter(query, Authority, terms)

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.authorities.views
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

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)

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.certificate.cli
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -20,6 +20,7 @@ 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
@ -106,16 +107,17 @@ def request_rotation(endpoint, certificate, message, commit):
:param commit:
:return:
"""
status = FAILURE_METRIC_STATUS
if commit:
try:
deployment_service.rotate_certificate(endpoint, certificate)
metrics.send('endpoint_rotation_success', 'counter', 1)
if message:
send_rotation_notification(certificate)
status = SUCCESS_METRIC_STATUS
except Exception as e:
metrics.send('endpoint_rotation_failure', 'counter', 1)
print(
"[!] Failed to rotate endpoint {0} to certificate {1} reason: {2}".format(
endpoint.name,
@ -124,6 +126,8 @@ def request_rotation(endpoint, certificate, message, commit):
)
)
metrics.send('endpoint_rotation', 'counter', 1, metric_tags={'status': status})
def request_reissue(certificate, commit):
"""
@ -132,16 +136,32 @@ def request_reissue(certificate, commit):
:param commit:
:return:
"""
# set the lemur identity for all cli commands
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
status = FAILURE_METRIC_STATUS
try:
print("[+] {0} is eligible for re-issuance".format(certificate.name))
details = get_certificate_primitives(certificate)
print_certificate_details(details)
# set the lemur identity for all cli commands
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
if commit:
new_cert = reissue_certificate(certificate, replace=True)
metrics.send('certificate_reissue_success', 'counter', 1)
print("[+] New certificate named: {0}".format(new_cert.name))
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.')
@ -159,6 +179,8 @@ def rotate(endpoint_name, new_certificate_name, old_certificate_name, message, c
print("[+] Starting endpoint rotation.")
status = FAILURE_METRIC_STATUS
try:
old_cert = validate_certificate(old_certificate_name)
new_cert = validate_certificate(new_certificate_name)
@ -182,14 +204,19 @@ def rotate(endpoint_name, new_certificate_name, old_certificate_name, message, c
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_failure', 'counter', 1)
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.')
@ -204,26 +231,30 @@ def reissue(old_certificate_name, commit):
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():
print("[+] {0} is eligible for re-issuance".format(certificate.name))
request_reissue(certificate, commit)
else:
request_reissue(old_cert, commit)
status = SUCCESS_METRIC_STATUS
print("[+] Done!")
except Exception as e:
sentry.captureException()
metrics.send('certificate_reissue_failure', 'counter', 1)
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.')
@ -275,9 +306,11 @@ def worker(data, commit, reason):
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_failure', 'counter', 1)
metrics.send('certificate_revoke', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
print(
"[!] Failed to revoke certificates. Reason: {}".format(
e

View File

@ -3,7 +3,7 @@ Debugging hooks for dumping imported or generated CSR and certificate details to
.. module: lemur.certificates.hooks
:platform: Unix
:copyright: (c) 2016-2017 by Marti Raudsepp, see AUTHORS for more
:copyright: (c) 2018 by Marti Raudsepp, see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Marti Raudsepp <marti@juffo.org>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.certificates.models
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -33,10 +33,11 @@ from lemur.common import defaults
from lemur.plugins.base import plugins
from lemur.extensions import metrics
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
from lemur.models import certificate_associations, certificate_source_associations, \
certificate_destination_associations, certificate_notification_associations, \
certificate_replacement_associations, roles_certificates
certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations
from lemur.domains.models import Domain
from lemur.policies.models import RotationPolicy
@ -101,6 +102,7 @@ class Certificate(db.Model):
serial = Column(String(128))
cn = Column(String(128))
deleted = Column(Boolean, index=True)
dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True)
not_before = Column(ArrowType)
not_after = Column(ArrowType)
@ -128,6 +130,11 @@ class Certificate(db.Model):
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
backref='replaced')
replaced_by_pending = relationship('PendingCertificate',
secondary=pending_cert_replacement_associations,
backref='pending_replace',
viewonly=True)
logs = relationship('Log', backref='certificate')
endpoints = relationship('Endpoint', backref='certificate')
rotation_policy = relationship("RotationPolicy")
@ -171,6 +178,8 @@ class Certificate(db.Model):
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))
@ -326,9 +335,8 @@ class Certificate(db.Model):
return_extensions['authority_key_identifier'] = aki
# TODO: Don't support CRLDistributionPoints yet https://github.com/Netflix/lemur/issues/662
elif isinstance(value, x509.CRLDistributionPoints):
current_app.logger.warning('CRLDistributionPoints not yet supported for clone operation.')
return_extensions['crl_distribution_points'] = {'include_crl_dp': value}
# TODO: Not supporting custom OIDs yet. https://github.com/Netflix/lemur/issues/665
else:
@ -358,15 +366,16 @@ def update_destinations(target, value, initiator):
:return:
"""
destination_plugin = plugins.get(value.plugin_name)
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()
current_app.logger.exception(e)
metrics.send('destination_upload_failure', 'counter', 1,
metric_tags={'certificate': target.name, 'destination': value.label})
metrics.send('destination_upload', 'counter', 1,
metric_tags={'status': status, 'certificate': target.name, 'destination': value.label})
@event.listens_for(Certificate.replaces, 'append')

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.certificates.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -9,31 +9,30 @@ 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
AssociatedRotationPolicySchema,
)
from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.destinations.schemas import DestinationNestedOutputSchema
from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.roles.schemas import RoleNestedOutputSchema
from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.users.schemas import UserNestedOutputSchema
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.common import validators, missing
from lemur.notifications import service as notification_service
from lemur.common.fields import ArrowDateTime, Hex
class CertificateSchema(LemurInputSchema):
@ -45,11 +44,14 @@ class CertificateCreationSchema(CertificateSchema):
@post_load
def default_notification(self, data):
if not data['notifications']:
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
notification_name = 'DEFAULT_SECURITY'
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
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
@ -67,9 +69,13 @@ class CertificateInputSchema(CertificateCreationSchema):
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(['RSA2048', 'RSA4096']), missing='RSA2048')
key_type = fields.String(
validate=validate.OneOf(CERTIFICATE_KEY_TYPES),
missing='RSA2048')
notify = fields.Boolean(default=True)
rotation = fields.Boolean()
@ -180,6 +186,7 @@ class CertificateOutputSchema(LemurOutputSchema):
description = fields.String()
issuer = fields.String()
name = fields.String()
dns_provider_id = fields.Integer(required=False, allow_none=True)
rotation = fields.Boolean()

View File

@ -1,14 +1,14 @@
"""
.. module: lemur.certificate.service
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from flask import current_app
from sqlalchemy import func, or_, not_, cast, Boolean, Integer
from sqlalchemy import func, or_, not_, cast, Integer
from cryptography import x509
from cryptography.hazmat.backends import default_backend
@ -17,7 +17,7 @@ from cryptography.hazmat.primitives import hashes, serialization
from lemur import database
from lemur.extensions import metrics, signals
from lemur.plugins.base import plugins
from lemur.common.utils import generate_private_key
from lemur.common.utils import generate_private_key, truthiness
from lemur.roles.models import Role
from lemur.domains.models import Domain
@ -25,6 +25,7 @@ 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
@ -63,6 +64,9 @@ def get_by_serial(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()
@ -190,7 +194,7 @@ def mint(**kwargs):
csr_imported.send(authority=authority, csr=csr)
cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
return cert_body, private_key, cert_chain, external_id
return cert_body, private_key, cert_chain, external_id, csr
def import_certificate(**kwargs):
@ -243,11 +247,12 @@ def create(**kwargs):
"""
Creates a new certificate.
"""
cert_body, private_key, cert_chain, external_id = mint(**kwargs)
cert_body, private_key, cert_chain, external_id, csr = mint(**kwargs)
kwargs['body'] = cert_body
kwargs['private_key'] = private_key
kwargs['chain'] = cert_chain
kwargs['external_id'] = external_id
kwargs['csr'] = csr
roles = create_certificate_roles(**kwargs)
@ -256,15 +261,20 @@ def create(**kwargs):
else:
kwargs['roles'] = roles
cert = Certificate(**kwargs)
if cert_body:
cert = Certificate(**kwargs)
kwargs['creator'].certificates.append(cert)
else:
cert = PendingCertificate(**kwargs)
kwargs['creator'].pending_certificates.append(cert)
kwargs['creator'].certificates.append(cert)
cert.authority = kwargs['authority']
certificate_issued.send(certificate=cert, authority=cert.authority)
database.commit()
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
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
@ -288,16 +298,20 @@ def render(args):
if filt:
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:
# we can't rely on issuer being correct in the cert directly so we combine queries
sub_query = database.session_query(Authority.id)\
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
.filter(Authority.name.ilike(term))\
.subquery()
query = query.filter(
or_(
Certificate.issuer.ilike('%{0}%'.format(terms[1])),
Certificate.issuer.ilike(term),
Certificate.authority_id.in_(sub_query)
)
)
@ -305,18 +319,26 @@ def render(args):
elif 'destination' in terms:
query = query.filter(Certificate.destinations.any(Destination.id == terms[1]))
elif 'notify' in filt:
query = query.filter(Certificate.notify == cast(terms[1], Boolean))
query = query.filter(Certificate.notify == truthiness(terms[1]))
elif 'active' in filt:
query = query.filter(Certificate.active == terms[1])
query = query.filter(Certificate.active == truthiness(terms[1]))
elif 'cn' in terms:
query = query.filter(
or_(
Certificate.cn.ilike('%{0}%'.format(terms[1])),
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
Certificate.cn.ilike(term),
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:
query = database.filter(query, Certificate, terms)

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.certificates.verify
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.certificates.views
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -18,6 +18,7 @@ from lemur.auth.service import AuthenticatedResource
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,
@ -267,7 +268,9 @@ class CertificatesList(AuthenticatedResource):
if authority_permission.can():
data['creator'] = g.user
cert = service.create(**data)
log_service.create(g.user, 'create_cert', certificate=cert)
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 the authority: {0}".format(data['authority'].name)), 403
@ -297,12 +300,14 @@ class CertificatesUpload(AuthenticatedResource):
{
"owner": "joe@example.com",
"publicCert": "-----BEGIN CERTIFICATE-----...",
"intermediateCert": "-----BEGIN CERTIFICATE-----...",
"body": "-----BEGIN CERTIFICATE-----...",
"chain": "-----BEGIN CERTIFICATE-----...",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----..."
"destinations": [],
"notifications": [],
"replacements": [],
"roles": [],
"notify": true,
"name": "cert1"
}

View File

@ -10,7 +10,7 @@ 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 (ä => a): decompose Unicode characters and then drop combining chars.
# 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.
@ -162,6 +162,9 @@ def domains(cert):
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()
@ -175,7 +178,7 @@ def serial(cert):
:param cert:
:return: serial number
"""
return cert.serial
return cert.serial_number
def san(cert):

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.common.fields
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.common.health
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.common.managers
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.common.schema
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -135,7 +135,6 @@ def unwrap_pagination(data, output_schema):
marshaled_data = {'total': len(data)}
marshaled_data['items'] = output_schema.dump(data, many=True).data
return marshaled_data
return output_schema.dump(data).data

View File

@ -1,23 +1,22 @@
"""
.. module: lemur.common.utils
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import string
import random
import string
import sqlalchemy
from sqlalchemy import and_, func
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from flask_restful.reqparse import RequestParser
from sqlalchemy import and_, func
from lemur.constants import CERTIFICATE_KEY_TYPES
from lemur.exceptions import InvalidConfiguration
paginated_parser = RequestParser()
@ -53,6 +52,19 @@ def parse_certificate(body):
return x509.load_pem_x509_certificate(body, default_backend())
def parse_csr(csr):
"""
Helper function that parses a CSR.
:param csr:
:return:
"""
if isinstance(csr, str):
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)
@ -65,17 +77,43 @@ def generate_private_key(key_type):
"""
Generates a new private key based on key_type.
Valid key types: RSA2048, RSA4096
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:
"""
valid_key_types = ['RSA2048', 'RSA4096']
if key_type not in valid_key_types:
_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(valid_key_types)
choices=",".join(CERTIFICATE_KEY_TYPES)
))
if 'RSA' in key_type:
@ -85,6 +123,11 @@ def generate_private_key(key_type):
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):
@ -162,3 +205,9 @@ def windowed_query(q, column, windowsize):
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')

View File

@ -1,8 +1,34 @@
"""
.. module: lemur.constants
:copyright: (c) 2015 by Netflix Inc.
:copyright: (c) 2018 by Netflix Inc.
:license: Apache, see LICENSE for more details.
"""
SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}"
DEFAULT_NAMING_TEMPLATE = "{subject}-{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,13 +4,13 @@
:synopsis: This module contains all of the database related methods
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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from inflection import underscore
from sqlalchemy import exc
from sqlalchemy import exc, func
from sqlalchemy.sql import and_, or_
from sqlalchemy.orm import make_transient
@ -267,6 +267,17 @@ def clone(model):
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):
"""
Helper that allows us to combine sorting and paging
@ -289,7 +300,7 @@ def sort_and_page(query, model, args):
if sort_by and sort_dir:
query = sort(query, model, sort_by, sort_dir)
total = query.count()
total = get_count(query)
# offset calculated at zero
page -= 1

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

@ -1,7 +1,7 @@
"""
.. module: lemur.defaults.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,6 +1,6 @@
"""
.. 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.
"""
from flask import current_app, Blueprint
@ -50,7 +50,8 @@ class LemurDefaults(AuthenticatedResource):
"state": "CA",
"location": "Los Gatos",
"organization": "Netflix",
"organizationalUnit": "Operations"
"organizationalUnit": "Operations",
"dnsProviders": [{"name": "test", ...}, {...}],
}
:reqheader Authorization: OAuth token to authenticate
@ -67,7 +68,7 @@ class LemurDefaults(AuthenticatedResource):
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
organizational_unit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
issuer_plugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN'),
authority=default_authority
authority=default_authority,
)

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.destinations.models
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.destinations.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.destinations.service
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -2,7 +2,7 @@
.. module: lemur.destinations.views
:platform: Unix
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

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,7 +1,7 @@
"""
.. module: lemur.domains.models
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.domains.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.domains.service
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.domains.views
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.endpoints.cli
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -2,7 +2,7 @@
.. module: lemur.endpoints.models
:platform: unix
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.endpoints.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -3,18 +3,17 @@
:platform: Unix
:synopsis: This module contains all of the services level functions used to
administer endpoints 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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import arrow
from flask import current_app
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
@ -132,19 +131,6 @@ def update(endpoint_id, **kwargs):
return endpoint
def rotate_certificate(endpoint, new_cert):
"""Rotates a certificate on a given endpoint."""
try:
endpoint.source.plugin.update_endpoint(endpoint, new_cert)
endpoint.certificate = new_cert
database.update(endpoint)
metrics.send('certificate_rotate_success', 'counter', 1, metric_tags={'endpoint': endpoint.name, 'source': endpoint.source.label})
except Exception as e:
metrics.send('certificate_rotate_failure', 'counter', 1, metric_tags={'endpoint': endpoint.name})
current_app.logger.exception(e)
raise e
def render(args):
"""
Helper that helps us render the REST Api responses.
@ -157,7 +143,7 @@ def render(args):
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 == terms[1])
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])

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.endpoints.views
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,6 +1,6 @@
"""
.. 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.
"""
from flask import current_app
@ -34,3 +34,11 @@ class AttrNotFound(LemurException):
class InvalidConfiguration(Exception):
pass
class InvalidAuthority(Exception):
pass
class UnknownProvider(Exception):
pass

View File

@ -1,6 +1,6 @@
"""
.. 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.
"""
from flask_sqlalchemy import SQLAlchemy
@ -26,3 +26,6 @@ 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
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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -21,7 +21,7 @@ from flask import Flask
from lemur.certificates.hooks import activate_debug_dump
from lemur.common.health import mod as health
from lemur.extensions import db, migrate, principal, smtp_mail, metrics, sentry
from lemur.extensions import db, migrate, principal, smtp_mail, metrics, sentry, cors
DEFAULT_BLUEPRINTS = (
@ -125,6 +125,10 @@ def configure_extensions(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):
"""

View File

@ -2,7 +2,7 @@
.. module: lemur.logs.models
:platform: unix
:synopsis: This module contains all of the models related private key audit log.
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.logs.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -3,7 +3,7 @@
:platform: Unix
:synopsis: This module contains all of the services level functions used to
administer logs 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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.logs.views
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -21,6 +21,7 @@ from lemur.reporting.cli import manager as report_manager
from lemur.endpoints.cli import manager as endpoint_manager
from lemur.certificates.cli import manager as certificate_manager
from lemur.notifications.cli import manager as notification_manager
from lemur.pending_certificates.cli import manager as pending_certificate_manager
from lemur import database
from lemur.users import service as user_service
@ -44,6 +45,7 @@ from lemur.sources.models import Source # noqa
from lemur.logs.models import Log # noqa
from lemur.endpoints.models import Endpoint # noqa
from lemur.policies.models import RotationPolicy # noqa
from lemur.pending_certificates.models import PendingCertificate # noqa
manager = Manager(create_app)
@ -106,6 +108,9 @@ LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
# Authentication Providers
ACTIVE_PROVIDERS = []
# Metrics Providers
METRIC_PROVIDERS = []
# Logging
LOG_LEVEL = "DEBUG"
@ -203,16 +208,16 @@ class InitializeApp(Command):
if operator_role:
sys.stdout.write("[-] Operator role already created, skipping...!\n")
else:
# we create an admin role
# we create an operator role
operator_role = role_service.create('operator', description='This is the Lemur operator role.')
sys.stdout.write("[+] Created 'operator' role\n")
read_only_role = role_service.get_by_name('read-only')
if read_only_role:
sys.stdout.write("[-] Operator role already created, skipping...!\n")
sys.stdout.write("[-] Read only role already created, skipping...!\n")
else:
# we create an admin role
# we create an read only role
read_only_role = role_service.create('read-only', description='This is the Lemur read only role.')
sys.stdout.write("[+] Created 'read-only' role\n")
@ -232,9 +237,6 @@ class InitializeApp(Command):
else:
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
sys.stdout.write("[+] Creating expiration email notifications!\n")
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
sys.stdout.write(
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
@ -244,14 +246,21 @@ class InitializeApp(Command):
)
recipients = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
sys.stdout.write("[+] Creating expiration email notifications!\n")
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format(recipients))
notification_service.create_default_expiration_notifications("DEFAULT_SECURITY", recipients=recipients)
days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 30)
sys.stdout.write("[+] Creating default certificate rotation policy of {days} days before issuance.\n".format(
days=days
))
_DEFAULT_ROTATION_INTERVAL = 'default'
default_rotation_interval = policy_service.get_by_name(_DEFAULT_ROTATION_INTERVAL)
if default_rotation_interval:
sys.stdout.write("[-] Default rotation interval policy already created, skipping...!\n")
else:
days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 30)
sys.stdout.write("[+] Creating default certificate rotation policy of {days} days before issuance.\n".format(
days=days))
policy_service.create(days=days, name=_DEFAULT_ROTATION_INTERVAL)
policy_service.create(days=days, name='default')
sys.stdout.write("[/] Done!\n")
@ -513,19 +522,6 @@ def publish_verisign_units():
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
@manager.command
def publish_unapproved_verisign_certificates():
"""
Query the Verisign for any certificates that need to be approved.
:return:
"""
from lemur.plugins import plugins
from lemur.extensions import metrics
v = plugins.get('verisign-issuer')
certs = v.get_pending_certificates()
metrics.send('pending_certificates', 'gauge', certs)
def main():
manager.add_command("start", LemurServer())
manager.add_command("runserver", Server(host='127.0.0.1', threaded=True))
@ -542,6 +538,7 @@ def main():
manager.add_command("endpoint", endpoint_manager)
manager.add_command("report", report_manager)
manager.add_command("policy", policy_manager)
manager.add_command("pending_certs", pending_certificate_manager)
manager.run()

View File

@ -1,6 +1,6 @@
"""
.. module: lemur.metrics
: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.
"""
from flask import current_app

View File

@ -0,0 +1,105 @@
"""Create tables and columns for the acme issuer.
Revision ID: 3adfdd6598df
Revises: 556ceb3e3c3e
Create Date: 2018-04-10 13:25:47.007556
"""
# revision identifiers, used by Alembic.
revision = '3adfdd6598df'
down_revision = '556ceb3e3c3e'
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy_utils import ArrowType
from lemur.utils import Vault
def upgrade():
# create provider table
print("Creating dns_providers table")
op.create_table(
'dns_providers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=256), nullable=True),
sa.Column('description', sa.String(length=1024), nullable=True),
sa.Column('provider_type', sa.String(length=256), nullable=True),
sa.Column('credentials', Vault(), nullable=True),
sa.Column('api_endpoint', sa.String(length=256), nullable=True),
sa.Column('date_created', ArrowType(), server_default=sa.text('now()'), nullable=False),
sa.Column('status', sa.String(length=128), nullable=True),
sa.Column('options', JSON),
sa.Column('domains', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
print("Adding dns_provider_id column to certificates")
op.add_column('certificates', sa.Column('dns_provider_id', sa.Integer(), nullable=True))
print("Adding dns_provider_id column to pending_certs")
op.add_column('pending_certs', sa.Column('dns_provider_id', sa.Integer(), nullable=True))
print("Adding options column to pending_certs")
op.add_column('pending_certs', sa.Column('options', JSON))
print("Creating pending_dns_authorizations table")
op.create_table(
'pending_dns_authorizations',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('account_number', sa.String(length=128), nullable=True),
sa.Column('domains', JSON, nullable=True),
sa.Column('dns_provider_type', sa.String(length=128), nullable=True),
sa.Column('options', JSON, nullable=True),
)
print("Creating certificates_dns_providers_fk foreign key")
op.create_foreign_key('certificates_dns_providers_fk', 'certificates', 'dns_providers', ['dns_provider_id'], ['id'],
ondelete='cascade')
print("Altering column types in the api_keys table")
op.alter_column('api_keys', 'issued_at',
existing_type=sa.BIGINT(),
nullable=True)
op.alter_column('api_keys', 'revoked',
existing_type=sa.BOOLEAN(),
nullable=True)
op.alter_column('api_keys', 'ttl',
existing_type=sa.BIGINT(),
nullable=True)
op.alter_column('api_keys', 'user_id',
existing_type=sa.INTEGER(),
nullable=True)
print("Creating dns_providers_id foreign key on pending_certs table")
op.create_foreign_key(None, 'pending_certs', 'dns_providers', ['dns_provider_id'], ['id'], ondelete='CASCADE')
def downgrade():
print("Removing dns_providers_id foreign key on pending_certs table")
op.drop_constraint(None, 'pending_certs', type_='foreignkey')
print("Reverting column types in the api_keys table")
op.alter_column('api_keys', 'user_id',
existing_type=sa.INTEGER(),
nullable=False)
op.alter_column('api_keys', 'ttl',
existing_type=sa.BIGINT(),
nullable=False)
op.alter_column('api_keys', 'revoked',
existing_type=sa.BOOLEAN(),
nullable=False)
op.alter_column('api_keys', 'issued_at',
existing_type=sa.BIGINT(),
nullable=False)
print("Reverting certificates_dns_providers_fk foreign key")
op.drop_constraint('certificates_dns_providers_fk', 'certificates', type_='foreignkey')
print("Dropping pending_dns_authorizations table")
op.drop_table('pending_dns_authorizations')
print("Undoing modifications to pending_certs table")
op.drop_column('pending_certs', 'options')
op.drop_column('pending_certs', 'dns_provider_id')
print("Undoing modifications to certificates table")
op.drop_column('certificates', 'dns_provider_id')
print("Deleting dns_providers table")
op.drop_table('dns_providers')

View File

@ -0,0 +1,28 @@
"""Add unique constraint to certificate_notification_associations table.
Revision ID: 449c3d5c7299
Revises: 5770674184de
Create Date: 2018-02-24 22:51:35.369229
"""
# revision identifiers, used by Alembic.
revision = '449c3d5c7299'
down_revision = '5770674184de'
from alembic import op
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
CONSTRAINT_NAME = "uq_dest_not_ids"
TABLE = "certificate_notification_associations"
COLUMNS = ["notification_id", "certificate_id"]
def upgrade():
op.create_unique_constraint(CONSTRAINT_NAME, TABLE, COLUMNS)
def downgrade():
op.drop_constraint(CONSTRAINT_NAME, TABLE)

View File

@ -0,0 +1,99 @@
"""Add Pending Certificates models and relations
Revision ID: 556ceb3e3c3e
Revises: 47baffaae1a7
Create Date: 2018-01-05 01:18:45.571595
"""
# revision identifiers, used by Alembic.
revision = '556ceb3e3c3e'
down_revision = '449c3d5c7299'
from alembic import op
import sqlalchemy as sa
from lemur.utils import Vault
from sqlalchemy.dialects import postgresql
from sqlalchemy_utils import ArrowType
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pending_certs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('external_id', sa.String(length=128), nullable=True),
sa.Column('owner', sa.String(length=128), nullable=False),
sa.Column('name', sa.String(length=256), nullable=True),
sa.Column('description', sa.String(length=1024), nullable=True),
sa.Column('notify', sa.Boolean(), nullable=True),
sa.Column('number_attempts', sa.Integer(), nullable=True),
sa.Column('rename', sa.Boolean(), nullable=True),
sa.Column('cn', sa.String(length=128), nullable=True),
sa.Column('csr', sa.Text(), nullable=False),
sa.Column('chain', sa.Text(), nullable=True),
sa.Column('private_key', Vault(), nullable=True),
sa.Column('date_created', ArrowType(), server_default=sa.text('now()'), nullable=False),
sa.Column('status', sa.String(length=128), nullable=True),
sa.Column('rotation', sa.Boolean(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('authority_id', sa.Integer(), nullable=True),
sa.Column('root_authority_id', sa.Integer(), nullable=True),
sa.Column('rotation_policy_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['root_authority_id'], ['authorities.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['rotation_policy_id'], ['rotation_policies.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('pending_cert_destination_associations',
sa.Column('destination_id', sa.Integer(), nullable=True),
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['destination_id'], ['destinations.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
)
op.create_index('pending_cert_destination_associations_ix', 'pending_cert_destination_associations', ['destination_id', 'pending_cert_id'], unique=False)
op.create_table('pending_cert_notification_associations',
sa.Column('notification_id', sa.Integer(), nullable=True),
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
)
op.create_index('pending_cert_notification_associations_ix', 'pending_cert_notification_associations', ['notification_id', 'pending_cert_id'], unique=False)
op.create_table('pending_cert_replacement_associations',
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
)
op.create_index('pending_cert_replacement_associations_ix', 'pending_cert_replacement_associations', ['replaced_certificate_id', 'pending_cert_id'], unique=False)
op.create_table('pending_cert_role_associations',
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
)
op.create_index('pending_cert_role_associations_ix', 'pending_cert_role_associations', ['pending_cert_id', 'role_id'], unique=False)
op.create_table('pending_cert_source_associations',
sa.Column('source_id', sa.Integer(), nullable=True),
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ondelete='cascade')
)
op.create_index('pending_cert_source_associations_ix', 'pending_cert_source_associations', ['source_id', 'pending_cert_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('pending_cert_source_associations_ix', table_name='pending_cert_source_associations')
op.drop_table('pending_cert_source_associations')
op.drop_index('pending_cert_role_associations_ix', table_name='pending_cert_role_associations')
op.drop_table('pending_cert_role_associations')
op.drop_index('pending_cert_replacement_associations_ix', table_name='pending_cert_replacement_associations')
op.drop_table('pending_cert_replacement_associations')
op.drop_index('pending_cert_notification_associations_ix', table_name='pending_cert_notification_associations')
op.drop_table('pending_cert_notification_associations')
op.drop_index('pending_cert_destination_associations_ix', table_name='pending_cert_destination_associations')
op.drop_table('pending_cert_destination_associations')
op.drop_table('pending_certs')
# ### end Alembic commands ###

View File

@ -0,0 +1,44 @@
"""Remove duplicates from certificate_notification_associations.
Revision ID: 5770674184de
Revises: ce547319f7be
Create Date: 2018-02-23 15:27:30.335435
"""
# revision identifiers, used by Alembic.
revision = '5770674184de'
down_revision = 'ce547319f7be'
from flask_sqlalchemy import SQLAlchemy
from lemur.models import certificate_notification_associations
db = SQLAlchemy()
session = db.session()
def upgrade():
print("Querying for all entries in certificate_notification_associations.")
# Query for all entries in table
results = session.query(certificate_notification_associations).with_entities(
certificate_notification_associations.c.certificate_id,
certificate_notification_associations.c.notification_id,
certificate_notification_associations.c.id,
)
seen = {}
# Iterate through all entries and mark as seen for each certificate_id and notification_id pair
for x in results:
# If we've seen a pair already, delete the duplicates
if seen.get("{}-{}".format(x.certificate_id, x.notification_id)):
print("Deleting duplicate: {}".format(x))
d = session.query(certificate_notification_associations).filter(certificate_notification_associations.c.id==x.id)
d.delete(synchronize_session=False)
seen["{}-{}".format(x.certificate_id, x.notification_id)] = True
db.session.commit()
db.session.flush()
def downgrade():
# No way to downgrade this
pass

View File

@ -0,0 +1,36 @@
"""Add id column to certificate_notification_associations.
Revision ID: ce547319f7be
Revises: 5bc47fa7cac4
Create Date: 2018-02-23 11:00:02.150561
"""
# revision identifiers, used by Alembic.
revision = 'ce547319f7be'
down_revision = '5bc47fa7cac4'
import sqlalchemy as sa
from alembic import op
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
TABLE = "certificate_notification_associations"
def upgrade():
print("Adding id column")
op.add_column(
TABLE,
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True)
)
db.session.commit()
db.session.flush()
def downgrade():
op.drop_column(TABLE, "id")
db.session.commit()
db.session.flush()

View File

@ -4,11 +4,11 @@
:synopsis: This module contains all of the associative tables
that help define the many to many relationships established 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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import Column, Integer, ForeignKey, Index
from sqlalchemy import Column, Integer, ForeignKey, Index, UniqueConstraint
from lemur.database import db
@ -41,7 +41,9 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
Column('notification_id', Integer,
ForeignKey('notifications.id', ondelete='cascade')),
Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade'))
ForeignKey('certificates.id', ondelete='cascade')),
Column('id', Integer, primary_key=True, autoincrement=True),
UniqueConstraint('notification_id', 'certificate_id', name='uq_dest_not_ids')
)
Index('certificate_notification_associations_ix', certificate_notification_associations.c.notification_id, certificate_notification_associations.c.certificate_id)
@ -53,7 +55,7 @@ certificate_replacement_associations = db.Table('certificate_replacement_associa
ForeignKey('certificates.id', ondelete='cascade'))
)
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.replaced_certificate_id, certificate_replacement_associations.c.certificate_id)
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.replaced_certificate_id, certificate_replacement_associations.c.certificate_id, unique=True)
roles_authorities = db.Table('roles_authorities',
Column('authority_id', Integer, ForeignKey('authorities.id')),
@ -83,3 +85,48 @@ policies_ciphers = db.Table('policies_ciphers',
Column('policy_id', Integer, ForeignKey('policy.id')))
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
pending_cert_destination_associations = db.Table('pending_cert_destination_associations',
Column('destination_id', Integer,
ForeignKey('destinations.id', ondelete='cascade')),
Column('pending_cert_id', Integer,
ForeignKey('pending_certs.id', ondelete='cascade'))
)
Index('pending_cert_destination_associations_ix', pending_cert_destination_associations.c.destination_id, pending_cert_destination_associations.c.pending_cert_id)
pending_cert_notification_associations = db.Table('pending_cert_notification_associations',
Column('notification_id', Integer,
ForeignKey('notifications.id', ondelete='cascade')),
Column('pending_cert_id', Integer,
ForeignKey('pending_certs.id', ondelete='cascade'))
)
Index('pending_cert_notification_associations_ix', pending_cert_notification_associations.c.notification_id, pending_cert_notification_associations.c.pending_cert_id)
pending_cert_source_associations = db.Table('pending_cert_source_associations',
Column('source_id', Integer,
ForeignKey('sources.id', ondelete='cascade')),
Column('pending_cert_id', Integer,
ForeignKey('pending_certs.id', ondelete='cascade'))
)
Index('pending_cert_source_associations_ix', pending_cert_source_associations.c.source_id, pending_cert_source_associations.c.pending_cert_id)
pending_cert_replacement_associations = db.Table('pending_cert_replacement_associations',
Column('replaced_certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade')),
Column('pending_cert_id', Integer,
ForeignKey('pending_certs.id', ondelete='cascade'))
)
Index('pending_cert_replacement_associations_ix', pending_cert_replacement_associations.c.replaced_certificate_id, pending_cert_replacement_associations.c.pending_cert_id)
pending_cert_role_associations = db.Table('pending_cert_role_associations',
Column('pending_cert_id', Integer, ForeignKey('pending_certs.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
Index('pending_cert_role_associations_ix', pending_cert_role_associations.c.pending_cert_id, pending_cert_role_associations.c.role_id)

View File

@ -1,12 +1,14 @@
"""
.. module: lemur.notifications.cli
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask_script import Manager
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
from lemur.extensions import sentry, metrics
from lemur.notifications.messaging import send_expiration_notifications
manager = Manager(usage="Handles notification related tasks.")
@ -25,11 +27,18 @@ def expirations(exclude):
:return:
"""
print("Starting to notify subscribers about expiring certificates!")
success, failed = send_expiration_notifications(exclude)
print(
"Finished notifying subscribers about expiring certificates! Sent: {success} Failed: {failed}".format(
success=success,
failed=failed
status = FAILURE_METRIC_STATUS
try:
print("Starting to notify subscribers about expiring certificates!")
success, failed = send_expiration_notifications(exclude)
print(
"Finished notifying subscribers about expiring certificates! Sent: {success} Failed: {failed}".format(
success=success,
failed=failed
)
)
)
status = SUCCESS_METRIC_STATUS
except Exception as e:
sentry.captureException()
metrics.send('expiration_notification_job', 'counter', 1, metric_tags={'status': status})

View File

@ -2,7 +2,7 @@
.. module: lemur.notifications.messaging
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -17,8 +17,9 @@ from flask import current_app
from sqlalchemy import and_
from lemur import database, metrics
from lemur.extensions import sentry
from lemur import database
from lemur.constants import FAILURE_METRIC_STATUS, SUCCESS_METRIC_STATUS
from lemur.extensions import metrics, sentry
from lemur.common.utils import windowed_query
from lemur.certificates.schemas import certificate_notification_output_schema
@ -94,14 +95,17 @@ def send_notification(event_type, data, targets, notification):
:param notification:
:return:
"""
status = FAILURE_METRIC_STATUS
try:
notification.plugin.send(event_type, data, targets, notification.options)
metrics.send('{0}_notification_sent'.format(event_type), 'counter', 1)
return True
status = SUCCESS_METRIC_STATUS
except Exception as e:
sentry.captureException()
metrics.send('{0}_notification_failure'.format(event_type), 'counter', 1)
current_app.logger.exception(e)
metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': event_type})
if status == SUCCESS_METRIC_STATUS:
return True
def send_expiration_notifications(exclude):
@ -147,8 +151,10 @@ def send_rotation_notification(certificate, notification_plugin=None):
rotated.
:param certificate:
:param notification_plugin:
:return:
"""
status = FAILURE_METRIC_STATUS
if not notification_plugin:
notification_plugin = plugins.get(current_app.config.get('LEMUR_DEFAULT_NOTIFICATION_PLUGIN'))
@ -156,12 +162,14 @@ def send_rotation_notification(certificate, notification_plugin=None):
try:
notification_plugin.send('rotation', data, [data['owner']])
metrics.send('rotation_notification_sent', 'counter', 1)
return True
status = SUCCESS_METRIC_STATUS
except Exception as e:
sentry.captureException()
metrics.send('rotation_notification_failure', 'counter', 1)
current_app.logger.exception(e)
metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': 'rotation'})
if status == SUCCESS_METRIC_STATUS:
return True
def needs_notification(certificate):

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.notifications.models
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
@ -11,7 +11,8 @@ from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
from lemur.models import certificate_notification_associations
from lemur.models import certificate_notification_associations, \
pending_cert_notification_associations
class Notification(db.Model):
@ -29,6 +30,13 @@ class Notification(db.Model):
backref="notification",
cascade='all,delete'
)
pending_certificates = relationship(
"PendingCertificate",
secondary=pending_cert_notification_associations,
passive_deletes=True,
backref="notification",
cascade='all,delete'
)
@property
def plugin(self):

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.notifications.schemas
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

@ -2,7 +2,7 @@
.. module: lemur.notifications.service
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -12,6 +12,7 @@ from flask import current_app
from lemur import database
from lemur.certificates.models import Certificate
from lemur.common.utils import truthiness
from lemur.notifications.models import Notification
@ -169,10 +170,8 @@ def render(args):
if filt:
terms = filt.split(';')
if terms[0] == 'active' and terms[1] == 'false':
query = query.filter(Notification.active == False) # noqa
elif terms[0] == 'active' and terms[1] == 'true':
query = query.filter(Notification.active == True) # noqa
if terms[0] == 'active':
query = query.filter(Notification.active == truthiness(terms[1]))
else:
query = database.filter(query, Notification, terms)

View File

@ -2,7 +2,7 @@
.. module: lemur.notifications.views
:platform: Unix
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""

View File

View File

@ -0,0 +1,98 @@
"""
.. module: lemur.pending_certificates.cli
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
"""
from flask_script import Manager
from lemur.authorities.service import get as get_authority
from lemur.pending_certificates import service as pending_certificate_service
from lemur.plugins.base import plugins
from lemur.users import service as user_service
manager = Manager(usage="Handles pending certificate related tasks.")
@manager.option('-i', dest='ids', action='append', help='IDs of pending certificates to fetch')
def fetch(ids):
"""
Attempt to get full certificates for each pending certificate listed.
Args:
ids: a list of ids of PendingCertificates (passed in by manager options when run as CLI)
`python manager.py pending_certs fetch -i 123 321 all`
"""
pending_certs = pending_certificate_service.get_pending_certs(ids)
user = user_service.get_by_username('lemur')
new = 0
failed = 0
for cert in pending_certs:
authority = plugins.get(cert.authority.plugin_name)
real_cert = authority.get_ordered_certificate(cert)
if real_cert:
# If a real certificate was returned from issuer, then create it in Lemur and delete
# the pending certificate
pending_certificate_service.create_certificate(cert, real_cert, user)
pending_certificate_service.delete(cert)
# add metrics to metrics extension
new += 1
else:
pending_certificate_service.increment_attempt(cert)
failed += 1
print(
"[+] Certificates: New: {new} Failed: {failed}".format(
new=new,
failed=failed,
)
)
@manager.command
def fetch_all_acme():
"""
Attempt to get full certificates for each pending certificate listed with the acme-issuer. This is more efficient
for acme-issued certificates because it will configure all of the DNS challenges prior to resolving any
certificates.
"""
pending_certs = pending_certificate_service.get_pending_certs('all')
user = user_service.get_by_username('lemur')
new = 0
failed = 0
wrong_issuer = 0
acme_certs = []
# We only care about certs using the acme-issuer plugin
for cert in pending_certs:
cert_authority = get_authority(cert.authority_id)
if cert_authority.plugin_name == 'acme-issuer':
acme_certs.append(cert)
else:
wrong_issuer += 1
authority = plugins.get("acme-issuer")
resolved_certs = authority.get_ordered_certificates(acme_certs)
for cert in resolved_certs:
real_cert = cert.get("cert")
# It's necessary to reload the pending cert due to detached instance: http://sqlalche.me/e/bhk3
pending_cert = pending_certificate_service.get(cert.get("pending_cert").id)
if real_cert:
# If a real certificate was returned from issuer, then create it in Lemur and delete
# the pending certificate
pending_certificate_service.create_certificate(pending_cert, real_cert, user)
pending_certificate_service.delete_by_id(pending_cert.id)
# add metrics to metrics extension
new += 1
else:
pending_certificate_service.increment_attempt(pending_cert)
failed += 1
print(
"[+] Certificates: New: {new} Failed: {failed} Not using ACME: {wrong_issuer}".format(
new=new,
failed=failed,
wrong_issuer=wrong_issuer
)
)

View File

@ -0,0 +1,102 @@
"""
.. module: lemur.pending_certificates.models
Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved.
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
"""
from datetime import datetime as dt
from sqlalchemy.orm import relationship
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils.types.arrow import ArrowType
from sqlalchemy_utils import JSONType
import lemur.common.utils
from lemur.certificates.models import get_or_increase_name
from lemur.common import defaults
from lemur.database import db
from lemur.utils import Vault
from lemur.models import pending_cert_source_associations, \
pending_cert_destination_associations, pending_cert_notification_associations, \
pending_cert_replacement_associations, pending_cert_role_associations
class PendingCertificate(db.Model):
__tablename__ = 'pending_certs'
id = Column(Integer, primary_key=True)
external_id = Column(String(128))
owner = Column(String(128), nullable=False)
name = Column(String(256), unique=True)
description = Column(String(1024))
notify = Column(Boolean, default=True)
number_attempts = Column(Integer)
rename = Column(Boolean, default=True)
cn = Column(String(128))
csr = Column(Text(), nullable=False)
chain = Column(Text())
private_key = Column(Vault, nullable=True)
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)
dns_provider_id = Column(Integer, ForeignKey('dns_providers.id', ondelete="CASCADE"))
status = Column(String(128))
rotation = Column(Boolean, default=False)
user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))
notifications = relationship('Notification', secondary=pending_cert_notification_associations, backref='pending_cert', passive_deletes=True)
destinations = relationship('Destination', secondary=pending_cert_destination_associations, backref='pending_cert', passive_deletes=True)
sources = relationship('Source', secondary=pending_cert_source_associations, backref='pending_cert', passive_deletes=True)
roles = relationship('Role', secondary=pending_cert_role_associations, backref='pending_cert', passive_deletes=True)
replaces = relationship('Certificate',
secondary=pending_cert_replacement_associations,
backref='pending_cert',
passive_deletes=True)
options = Column(JSONType)
rotation_policy = relationship("RotationPolicy")
sensitive_fields = ('private_key',)
def __init__(self, **kwargs):
self.csr = kwargs.get('csr')
self.private_key = kwargs.get('private_key', "")
if self.private_key:
# If the request does not send private key, the key exists but the value is None
self.private_key = self.private_key.strip()
self.external_id = kwargs.get('external_id')
# 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']), 0)
self.rename = False
else:
# TODO: Fix auto-generated name, it should be renamed on creation
self.name = get_or_increase_name(
defaults.certificate_name(kwargs['common_name'], kwargs['authority'].name,
dt.now(), dt.now(), False), self.external_id)
self.rename = True
self.cn = defaults.common_name(lemur.common.utils.parse_csr(self.csr))
self.owner = kwargs['owner']
self.number_attempts = 0
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')
try:
self.dns_provider_id = kwargs.get('dns_provider').id
except (AttributeError, KeyError, TypeError, Exception):
pass

View File

@ -0,0 +1,101 @@
from marshmallow import fields, post_load
from lemur.schemas import (
AssociatedCertificateSchema,
AssociatedDestinationSchema,
AssociatedNotificationSchema,
AssociatedRoleSchema,
EndpointNestedOutputSchema,
ExtensionSchema
)
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.users.schemas import UserNestedOutputSchema
from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.certificates.schemas import CertificateNestedOutputSchema
from lemur.destinations.schemas import DestinationNestedOutputSchema
from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.roles.schemas import RoleNestedOutputSchema
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
from lemur.notifications import service as notification_service
class PendingCertificateSchema(LemurInputSchema):
owner = fields.Email(required=True)
description = fields.String(missing='', allow_none=True)
class PendingCertificateOutputSchema(LemurOutputSchema):
id = fields.Integer()
external_id = fields.String()
csr = fields.String()
chain = fields.String()
deleted = fields.Boolean(default=False)
description = fields.String()
issuer = fields.String()
name = fields.String()
number_attempts = fields.Integer()
date_created = fields.Date()
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')
owner = fields.Email()
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 PendingCertificateEditInputSchema(PendingCertificateSchema):
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)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
@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 PendingCertificateCancelSchema(LemurInputSchema):
note = fields.String()
pending_certificate_output_schema = PendingCertificateOutputSchema()
pending_certificate_edit_input_schema = PendingCertificateEditInputSchema()
pending_certificate_cancel_schema = PendingCertificateCancelSchema()

View File

@ -0,0 +1,224 @@
"""
.. module: lemur.pending_certificates.service
Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved.
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
"""
import arrow
from sqlalchemy import or_, cast, Integer
from lemur import database
from lemur.common.utils import truthiness
from lemur.plugins.base import plugins
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.notifications.models import Notification
from lemur.pending_certificates.models import PendingCertificate
from lemur.certificates import service as certificate_service
from lemur.users import service as user_service
from lemur.certificates.schemas import CertificateUploadInputSchema
def get(pending_cert_id):
"""
Retrieve pending certificate by ID
"""
return database.get(PendingCertificate, pending_cert_id)
def get_by_external_id(issuer, external_id):
"""
Retrieves a pending certificate by its issuer and external_id
Since external_id is not necessarily unique between CAs
:param issuer:
:param external_id:
:return: PendingCertificate or None
"""
if isinstance(external_id, int):
external_id = str(external_id)
return PendingCertificate.query \
.filter(PendingCertificate.authority_id == issuer.id) \
.filter(PendingCertificate.external_id == external_id) \
.one_or_none()
def get_by_name(pending_cert_name):
"""
Retrieve pending certificate by name
"""
return database.get(PendingCertificate, pending_cert_name, field='name')
def delete(pending_certificate):
database.delete(pending_certificate)
def delete_by_id(id):
database.delete(get(id))
def get_pending_certs(pending_ids):
"""
Retrieve a list of pending certs given a list of ids
Filters out non-existing pending certs
"""
pending_certs = []
if 'all' in pending_ids:
query = database.session_query(PendingCertificate)
return database.find_all(query, PendingCertificate, {}).all()
else:
for pending_id in pending_ids:
pending_cert = get(pending_id)
if pending_cert:
pending_certs.append(pending_cert)
return pending_certs
def create_certificate(pending_certificate, certificate, user):
"""
Create and store a certificate with pending certificate's info
Args:
pending_certificate: PendingCertificate which will populate the certificate
certificate: dict from Authority, which contains the body, chain and external id
user: User that called this function, used as 'creator' of the certificate if it does
not have an owner
"""
certificate['owner'] = pending_certificate.owner
data, errors = CertificateUploadInputSchema().load(certificate)
if errors:
raise Exception("Unable to create certificate: {reasons}".format(reasons=errors))
data.update(vars(pending_certificate))
# Copy relationships, vars doesn't copy this without explicit fields
data['notifications'] = list(pending_certificate.notifications)
data['destinations'] = list(pending_certificate.destinations)
data['sources'] = list(pending_certificate.sources)
data['roles'] = list(pending_certificate.roles)
data['replaces'] = list(pending_certificate.replaces)
data['rotation_policy'] = pending_certificate.rotation_policy
# Replace external id and chain with the one fetched from source
data['external_id'] = certificate['external_id']
data['chain'] = certificate['chain']
creator = user_service.get_by_email(pending_certificate.owner)
if not creator:
# Owner of the pending certificate is not the creator, so use the current user who called
# this as the creator (usually lemur)
creator = user
if pending_certificate.rename:
# If generating name from certificate, remove the one from pending certificate
del data['name']
data['creator'] = creator
cert = certificate_service.import_certificate(**data)
database.update(cert)
return cert
def increment_attempt(pending_certificate):
"""
Increments pending certificate attempt counter and updates it in the database.
"""
pending_certificate.number_attempts += 1
database.update(pending_certificate)
return pending_certificate.number_attempts
def update(pending_cert_id, **kwargs):
"""
Updates a pending certificate. The allowed fields are validated by
PendingCertificateEditInputSchema.
"""
pending_cert = get(pending_cert_id)
for key, value in kwargs.items():
setattr(pending_cert, key, value)
return database.update(pending_cert)
def cancel(pending_certificate, **kwargs):
"""
Cancel a pending certificate. A check should be done prior to this function to decide to
revoke the certificate or just abort cancelling.
Args:
pending_certificate: PendingCertificate to be cancelled
Returns: the pending certificate if successful, raises Exception if there was an issue
"""
plugin = plugins.get(pending_certificate.authority.plugin_name)
plugin.cancel_ordered_certificate(pending_certificate, **kwargs)
pending_certificate.status = 'Cancelled'
database.update(pending_certificate)
return pending_certificate
def render(args):
query = database.session_query(PendingCertificate)
time_range = args.pop('time_range')
destination_id = args.pop('destination_id')
notification_id = args.pop('notification_id', None)
show = args.pop('show')
# owner = args.pop('owner')
# creator = args.pop('creator') # TODO we should enabling filtering by owner
filt = args.pop('filter')
if filt:
terms = filt.split(';')
if 'issuer' in terms:
# we can't rely on issuer being correct in the cert directly so we combine queries
sub_query = database.session_query(Authority.id)\
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
.subquery()
query = query.filter(
or_(
PendingCertificate.issuer.ilike('%{0}%'.format(terms[1])),
PendingCertificate.authority_id.in_(sub_query)
)
)
elif 'destination' in terms:
query = query.filter(PendingCertificate.destinations.any(Destination.id == terms[1]))
elif 'notify' in filt:
query = query.filter(PendingCertificate.notify == truthiness(terms[1]))
elif 'active' in filt:
query = query.filter(PendingCertificate.active == truthiness(terms[1]))
elif 'cn' in terms:
query = query.filter(
or_(
PendingCertificate.cn.ilike('%{0}%'.format(terms[1])),
PendingCertificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
)
)
elif 'id' in terms:
query = query.filter(PendingCertificate.id == cast(terms[1], Integer))
else:
query = database.filter(query, PendingCertificate, terms)
if show:
sub_query = database.session_query(Role.name).filter(Role.user_id == args['user'].id).subquery()
query = query.filter(
or_(
PendingCertificate.user_id == args['user'].id,
PendingCertificate.owner.in_(sub_query)
)
)
if destination_id:
query = query.filter(PendingCertificate.destinations.any(Destination.id == destination_id))
if notification_id:
query = query.filter(PendingCertificate.notifications.any(Notification.id == notification_id))
if time_range:
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
now = arrow.now().format('YYYY-MM-DD')
query = query.filter(PendingCertificate.not_after <= to).filter(PendingCertificate.not_after >= now)
return database.sort_and_page(query, PendingCertificate, args)

View File

@ -0,0 +1,424 @@
"""
.. module: lemur.pending_certificates.views
:platform: Unix
:license: Apache, see LICENSE for more details.
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
"""
from flask import Blueprint, g, make_response, jsonify
from flask_restful import Api, reqparse
from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import CertificatePermission
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
from lemur.pending_certificates import service
from lemur.roles import service as role_service
from lemur.pending_certificates.schemas import (
pending_certificate_output_schema,
pending_certificate_edit_input_schema,
pending_certificate_cancel_schema,
)
mod = Blueprint('pending_certificates', __name__)
api = Api(mod)
class PendingCertificatesList(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(PendingCertificatesList, self).__init__()
@validate_schema(None, pending_certificate_output_schema)
def get(self):
"""
.. http:get:: /pending_certificates
List of pending certificates
**Example request**:
.. sourcecode:: http
GET /pending_certificates 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
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"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"
}],
"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
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
parser.add_argument('owner', type=bool, location='args')
parser.add_argument('id', type=str, location='args')
parser.add_argument('active', type=bool, location='args')
parser.add_argument('destinationId', type=int, dest="destination_id", location='args')
parser.add_argument('creator', type=str, location='args')
parser.add_argument('show', type=str, location='args')
args = parser.parse_args()
args['user'] = g.user
return service.render(args)
class PendingCertificates(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(PendingCertificates, self).__init__()
@validate_schema(None, pending_certificate_output_schema)
def get(self, pending_certificate_id):
"""
.. http:get:: /pending_certificates/1
One pending certificate
**Example request**:
.. sourcecode:: http
GET /pending_certificates/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
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 1,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"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"
}],
"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
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.get(pending_certificate_id)
@validate_schema(pending_certificate_edit_input_schema, pending_certificate_output_schema)
def put(self, pending_certificate_id, data=None):
"""
.. http:put:: /pending_certificates/1
Update a pending certificate
**Example request**:
.. sourcecode:: http
PUT /pending certificates/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"owner": "jimbob@example.com",
"active": false
"notifications": [],
"destinations": [],
"replacements": []
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"destinations": [],
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"number_attempts": 1,
"csr": "-----BEGIN CERTIFICATE REQUEST-----...",
"external_id": 12345,
"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"},
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
pending_cert = service.get(pending_certificate_id)
if not pending_cert:
return dict(message="Cannot find specified pending certificate"), 404
# allow creators
if g.current_user != pending_cert.user:
owner_role = role_service.get_by_name(pending_cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
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 pending_cert.private_key:
return dict(
message='Unable to add destination: {0}. Certificate does not have required private key.'.format(
destination.label
)
), 400
pending_cert = service.update(pending_certificate_id, **data)
return pending_cert
@validate_schema(pending_certificate_cancel_schema, None)
def delete(self, pending_certificate_id, data=None):
"""
.. http:delete:: /pending_certificates/1
Cancel and delete a pending certificate
**Example request**:
.. sourcecode:: http
DELETE /pending certificates/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"note": "Why I am cancelling this order"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
:reqheader Authorization: OAuth token to authenticate
:statuscode 204: no error
:statuscode 401: unauthenticated
:statuscode 403: unauthorized
:statuscode 404: pending certificate id not found
:statuscode 500: internal error
"""
pending_cert = service.get(pending_certificate_id)
if not pending_cert:
return dict(message="Cannot find specified pending certificate"), 404
# allow creators
if g.current_user != pending_cert.user:
owner_role = role_service.get_by_name(pending_cert.owner)
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
if not permission.can():
return dict(message='You are not authorized to update this certificate'), 403
if service.cancel(pending_cert, **data):
service.delete(pending_cert)
return('', 204)
else:
# service.cancel raises exception if there was an issue, but this will ensure something
# is relayed to user in case of something unexpected (unsuccessful update somehow).
return dict(message="Unexpected error occurred while trying to cancel this certificate"), 500
class PendingCertificatePrivateKey(AuthenticatedResource):
def __init__(self):
super(PendingCertificatePrivateKey, self).__init__()
def get(self, pending_certificate_id):
"""
.. http:get:: /pending_certificates/1/key
Retrieves the private key for a given pneding certificate
**Example request**:
.. sourcecode:: http
GET /pending_certificates/1/key 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
{
"key": "-----BEGIN ..."
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
cert = service.get(pending_certificate_id)
if not cert:
return dict(message="Cannot find specified pending 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 view this key'), 403
response = make_response(jsonify(key=cert.private_key), 200)
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
response.headers['pragma'] = 'no-cache'
return response
api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates')
api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate')
api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.base
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,6 +1,6 @@
"""
.. module: lemur.plugins.base.manager
: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.
.. moduleauthor:: Kevin Glisson (kglisson@netflix.com)
@ -34,6 +34,10 @@ class PluginManager(InstanceManager):
for plugin in self.all(version=2):
if plugin.slug == slug:
return plugin
current_app.logger.error(
"Unable to find slug: {} in self.all version 1: {} or version 2: {}".format(
slug, self.all(version=1), self.all(version=2))
)
raise KeyError(slug)
def first(self, func_name, *args, **kwargs):

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.base.v1
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.bases.destination
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.bases.export
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.bases.issuer
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
@ -24,3 +24,9 @@ class IssuerPlugin(Plugin):
def revoke_certificate(self, certificate, comments):
raise NotImplementedError
def get_ordered_certificate(self, certificate):
raise NotImplementedError
def cancel_ordered_certificate(self, pending_cert, **kwargs):
raise NotImplementedError

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.bases.metric
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.bases.notification
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

View File

@ -1,7 +1,7 @@
"""
.. module: lemur.plugins.bases.source
: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.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>

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