Compare commits

..

434 Commits
0.1 ... 0.3.0

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

* Clarifying comment.

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

* adding some fields

* Adding Source Plugin change.

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

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

* Fixing python3

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

* Fixing some lint.

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

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

* Adding migration scripts.

* Adding endpoints field for future use.

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

* Fixing tests

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

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

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

Workaround for this error:
2015-12-30 13:58:48,073 ERROR: Internal Error [in /www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py:299]
Traceback (most recent call last):
  File "/www/lemur/local/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/www/lemur/local/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py", line 462, in wrapper
    resp = resource(*args, **kwargs)
  File "/www/lemur/local/lib/python2.7/site-packages/flask/views.py", line 84, in view
    return self.dispatch_request(*args, **kwargs)
  File "/www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py", line 572, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/www/lemur/lemur/auth/views.py", line 276, in get
    for provider in current_app.config.get("ACTIVE_PROVIDERS"):
TypeError: 'NoneType' object is not iterable
2015-12-30 14:56:59 +01:00
00cb66484b Merge pull request #188 from kevgliss/csr
Adding the ability to submit a third party CSR
2015-12-29 12:11:11 -08:00
cabe2ae18d Adding the ability to issue third party created CSRs 2015-12-29 10:49:33 -08:00
665a3f3180 Merge pull request #187 from kevgliss/sso
Fixing some issues with dynamically supporting multiple SSO providers
2015-12-27 22:06:31 -05:00
3b5d7eaab6 More Linting 2015-12-27 18:08:17 -05:00
aa2358aa03 Fixing linting 2015-12-27 18:02:38 -05:00
a7decc1948 Fixing some issues with dynamically supporting multiple SSO providers 2015-12-27 17:54:11 -05:00
38b48604f3 Merge pull request #186 from rpicard/master
Add Google SSO
2015-12-27 15:30:52 -05:00
60856cb7b9 Add an endpoint to return active authentication providers
This endpoint can be used by Angular to figure out what authentication
options to display to the user. It returns a dictionary of configuration
details that the front-end needs for each provider.
2015-12-22 18:03:56 -05:00
350d013043 Add Google SSO
This pull request adds Google SSO support. There are two main changes:

1. Add the Google auth view resource
2. Make passwords optional when creating a new user. This allows an admin
to create a user without a password so that they can only login via Google.
2015-12-22 13:44:30 -05:00
70c92fea15 Merge pull request #183 from kevgliss/rotate
Adding rotate command
2015-12-18 12:05:52 -05:00
6211b126a9 Fixing py3 syntax error 2015-12-18 11:01:08 -05:00
54c3fcc72a Adding rotate command 2015-12-17 23:17:27 -05:00
27c9088ddb Merge pull request #182 from kevgliss/176-p12
Closes #176
2015-12-17 15:01:54 -08:00
b8c2d42cad Closes #176 2015-12-17 14:52:20 -08:00
1f5ddd9530 Merge pull request #181 from kevgliss/172-export
Closes #172
2015-12-17 14:35:55 -08:00
2896ce0dad Closes #172 2015-12-16 08:18:01 -08:00
29bcde145c 0.2.1 release 2015-12-14 10:42:51 -08:00
11db429bcc adding OSSMETADATA for NetflixOSS tracking 2015-12-11 15:57:28 -08:00
75aea9f885 Merge pull request #179 from rpicard/master
Update example supervisor configuration file
2015-12-10 18:31:02 -08:00
c80559005f Update example supervisor configuration file
supervisord should run as root and spawn the lemur process as the lemur
user. I also added the LEMUR_CONF environment variable because it was
not reading the configuration file in by default.
2015-12-10 17:39:49 -08:00
9b927cfcc2 Merge pull request #177 from kevgliss/docs
clarifying upgrade process
2015-12-09 17:29:13 -08:00
4db7931aa0 clarifying upgrade process 2015-12-09 17:18:01 -08:00
1e67329c64 Merge pull request #175 from kevgliss/notifications
Fixing templates
2015-12-04 09:57:42 -08:00
6d17e4d538 Fixing templates 2015-12-04 09:51:38 -08:00
350f58ec9d Merge pull request #174 from kevgliss/binding
Disabling one-time binding
2015-12-03 17:16:19 -08:00
de9478a992 Disabling one-time binding 2015-12-03 16:57:37 -08:00
70a2c985cf Merge pull request #171 from kevgliss/packaging
Fixing the startup port
2015-12-02 17:14:24 -08:00
78037dc9ec Fixing the startup port 2015-12-02 17:13:52 -08:00
9b11efd1e5 Merge pull request #170 from kevgliss/export
Adding export plugin docs
2015-12-02 16:05:36 -08:00
3c2ee8fbb3 Adding export plugin docs 2015-12-02 16:04:40 -08:00
163cc3f795 Merge pull request #169 from kevgliss/bump
Version bump
2015-12-02 14:54:17 -08:00
041382b02f Version bump 2015-12-02 14:53:46 -08:00
837bfc3aa5 Merge pull request #167 from forkd/master
minor changes in quickstart guide
2015-12-02 14:51:07 -08:00
5ba1176f14 Merge pull request #168 from kevgliss/changelog
Changelog
2015-12-02 14:50:42 -08:00
f08649b02d Updating change log 2015-12-02 14:50:14 -08:00
edbe5a254b minor changes in quickstart guide 2015-12-02 14:34:22 +00:00
cfedb30628 Merge pull request #166 from kevgliss/template
Making the notification email template cleaner
2015-12-01 18:16:54 -08:00
aa18b88a61 Making the notification email template cleaner 2015-12-01 17:13:43 -08:00
05962e71e3 Merge branch 'forkd-master' 2015-12-01 13:03:23 -08:00
bafc3d0082 minor adjustments 2015-12-01 13:03:08 -08:00
308f1b44c3 Merge branch 'master' of git://github.com/forkd/lemur into forkd-master 2015-12-01 13:01:54 -08:00
cd17789529 Removing unneeded import 2015-12-01 11:51:39 -08:00
bf988d89c4 updated quickstart guide 2015-12-01 19:03:17 +00:00
b1e842ae47 Merge pull request #162 from kevgliss/160-startup
Closes #160
2015-12-01 10:08:03 -08:00
fcc3c35ae2 Merge pull request #163 from kevgliss/docs
Updating docs
2015-12-01 10:07:37 -08:00
e2524e43cf adding exports 2015-12-01 09:44:41 -08:00
6aac2d62be Closes #160 2015-12-01 09:40:27 -08:00
95e2636f23 Updating docs 2015-12-01 09:15:53 -08:00
7565492bb9 Merge pull request #161 from kevgliss/docs
adding version.py
2015-12-01 08:34:25 -08:00
89f7f12f92 adding version.py 2015-12-01 08:33:37 -08:00
cdd15ca818 Merge pull request #159 from kevgliss/migrations
Adding current migration files.
2015-11-30 15:44:14 -08:00
11f2d88b16 Adding current migration files. 2015-11-30 15:43:38 -08:00
8066d540e0 Merge pull request #158 from kevgliss/fix
Fix
2015-11-30 14:27:23 -08:00
c3091a7346 Adding missing files. 2015-11-30 14:08:17 -08:00
9cadebcd50 adding example requests 2015-11-30 13:51:27 -08:00
3e54eb7520 adding closed 2015-11-30 12:51:28 -08:00
068f19c895 Merge pull request #156 from kevgliss/bump
adding automatic versioning
2015-11-30 12:09:33 -08:00
3651cce542 adding automatic versioning 2015-11-30 10:43:41 -08:00
9e0b9d9dda Merge pull request #154 from kevgliss/125-output-plugins
Initial work on #125
2015-11-30 10:31:25 -08:00
f194e2a1be Linting 2015-11-30 10:24:53 -08:00
f56c6f2836 Downgrading req to pass tests. 2015-11-30 10:10:50 -08:00
ec896461a7 Adding final touches to #125 2015-11-30 09:47:36 -08:00
8eeed821d3 Adding UI elements 2015-11-27 13:27:14 -08:00
80c1689b24 Merge pull request #155 from Joe8Bit/master
Fix requires.io badge
2015-11-27 10:21:33 -08:00
7c29b566be Update README.rst
Update README.rst to point to correct requires.io badge
2015-11-27 14:37:23 +00:00
920d595c12 Initial work on #125 2015-11-25 14:54:08 -08:00
5d7174b2a7 Merge pull request #153 from Netflix/requires-io-master
[requires.io] dependency update on master branch
2015-11-25 14:44:31 -08:00
3c60f47e3f [requires.io] dependency update 2015-11-25 14:18:01 -08:00
c4abc59673 [requires.io] dependency update 2015-11-25 14:18:00 -08:00
ff4cdd82ee Merge pull request #152 from kevgliss/144-search
Closes #144
2015-11-24 16:13:05 -08:00
1c6e9caa40 Closes #144 2015-11-24 16:07:44 -08:00
07ec04ddc6 Merge pull request #151 from kevgliss/122-replacement
Closes #122
2015-11-24 15:01:39 -08:00
d6b3f5af81 Closes #122 2015-11-24 14:53:22 -08:00
ce1fe9321c Merge pull request #150 from kevgliss/121-validation
121 validation
2015-11-23 16:48:40 -08:00
2c88e4e3ba fixing conflict 2015-11-23 16:42:14 -08:00
fed37c9dc0 Fixes badge 2015-11-23 16:41:31 -08:00
e14eefdc31 Added the ability to find an authority even if a user only types the name in and does not select it. 2015-11-23 16:41:31 -08:00
2525d369d4 Merge pull request #149 from kevgliss/requirements2
Updating requirements
2015-11-23 15:59:58 -08:00
0600481a67 Updating requirements 2015-11-23 15:41:11 -08:00
f0324e4755 Merge pull request #148 from kevgliss/120-error-length
Closes #120
2015-11-23 15:25:30 -08:00
00f0f957c0 Lint again 2015-11-23 15:13:18 -08:00
9c652d784d Merge pull request #143 from kevgliss/requirements
Updating requirements
2015-11-23 14:59:31 -08:00
eb2fa74661 Fixing test 2015-11-23 14:49:05 -08:00
146c599deb Lint cleanup 2015-11-23 14:47:34 -08:00
574c4033ab Closes #120 2015-11-23 14:30:23 -08:00
9f122eec18 Merge pull request #145 from kevgliss/140-permalink
Closes #140
2015-11-23 12:02:49 -08:00
eb0f6a04d8 Closes #140 2015-11-23 10:43:07 -08:00
c7230befe4 Merge pull request #142 from kevgliss/139-description
Closes #139
2015-11-23 10:27:04 -08:00
9a316ae1a9 Updating requirements 2015-11-23 10:23:23 -08:00
df4364714e Closes #139 2015-11-23 09:53:55 -08:00
a1cd2b39eb Merge pull request #135 from mikegrima/travis-tweak
Removed un-needed build step.
2015-10-29 13:34:50 -07:00
8a7a15a361 Merge pull request #132 from cloughrm/master
Use american english for consistency
2015-10-29 13:34:31 -07:00
2cdeecb6e0 Merge pull request #134 from Netflix/monkeysecurity-patch-1
Removing hyphen from in-active.
2015-10-29 13:33:49 -07:00
638e4a5ac1 Removed un-needed build step.
I hope they have size 'M'.
2015-10-29 13:02:58 -07:00
93b4ef5f17 Removing hyphen from in-active.
`inactive` is a word.  in-active is ... something else.
2015-10-29 11:54:00 -07:00
26d490f74a Merge pull request #133 from belladzaster/grammer
Fixing grammar
2015-10-28 21:53:19 -07:00
01a1190524 Fixing grammer 2015-10-28 19:55:08 -07:00
2073090628 Use american english for consistency 2015-10-28 19:39:10 -07:00
6d00cb208d Merge pull request #131 from belladzaster/master
Fixing Typos
2015-10-28 19:32:08 -07:00
13b9bf687d Fixing Typos 2015-10-28 18:24:31 -07:00
56f7da34d7 Merge pull request #129 from kevgliss/version
Version bump and needed documentation.
2015-10-26 11:59:16 -07:00
0f34440b64 Merge pull request #130 from kevgliss/createUser
Fixing issue where roles were not added correctly to user.
2015-10-26 11:58:36 -07:00
bbcc7cca4e Merge pull request #128 from kevgliss/fernet_migration
[WIP] Adding aes - fernet migration
2015-10-26 10:59:41 -07:00
0453afcb0e Fixing issuer where roles were not added correctly to user. 2015-10-26 10:59:20 -07:00
cafecd1e19 Version bump and needed documentation. 2015-10-24 11:18:27 -07:00
4b968a9474 Adding aes - fernet migration 2015-10-23 16:47:17 -07:00
9244945e69 Merge pull request #127 from monkeysecurity/update_cryptography_for_el_capitan
Updating cryptography to 1.0.2 for el capitan
2015-10-21 18:54:43 -07:00
78819c1733 Updating cryptography to 1.0.2 for el capitan 2015-10-21 18:45:50 -07:00
394e18f76e Merge pull request #123 from rpicard/master
Use MultiFernet for encryption
2015-10-14 15:50:33 -07:00
40eb950e94 Use MultiFernet for encryption
Facilitates key rotation and uses more secure encryption than what
sqlalchemy-utils does.

Fixes #117 and #119.
2015-10-13 16:58:58 -07:00
90636a5329 Merge pull request #118 from rpicard/master
Fix a handful of typos in documentation
2015-10-06 15:30:09 -07:00
2fc6d4cd21 Fix a handful of typos in documentation
As I was reading through the docs I made note of grammar issues and
typos I saw. Not a huge deal but might as well fix what I noticed.
2015-10-06 15:05:05 -07:00
b20bdf3c4e Merge pull request #116 from kevgliss/algo
Adding the ability to track a certificates signing key algorithm
2015-10-06 13:25:34 -07:00
a20726a301 Fixing python 3.x syntax error 2015-10-06 13:11:24 -07:00
39727a1c9f Fixing tests 2015-10-06 13:00:06 -07:00
168f46a436 Adding the ability to track a certificates signing key algorithm 2015-10-06 12:51:59 -07:00
4ec07a6dc7 Merge pull request #115 from kevgliss/destinations
Fixes destination stat
2015-10-06 09:48:03 -07:00
798a6295ee Fixes destination stat 2015-10-06 09:43:31 -07:00
73cb8da8c1 Merge pull request #114 from kevgliss/clipboard
Add clipboard functionality
2015-10-05 16:14:16 -07:00
3167ce9785 removing unneeded dep 2015-10-05 16:08:12 -07:00
63b7b71b49 adding clipboard functionality 2015-10-05 16:06:56 -07:00
9965af9ccd fixing links, and adding zeroclipboard 2015-10-05 09:48:52 -07:00
ba5d2c925a Merge pull request #113 from kevgliss/perma
Adding ui router and perma links to certificates and authorities
2015-10-05 09:30:52 -07:00
867be09e29 more double quotes 2015-10-05 09:24:11 -07:00
8362a92898 fixing double quotes 2015-10-05 09:19:14 -07:00
162482dbc4 Adding ui router and perma links to certificates and authorities 2015-10-05 09:00:51 -07:00
c0f14db5bb Merge pull request #112 from kevgliss/uitweaks
UI tweaks
2015-10-02 16:31:50 -07:00
3c561914c6 removing angular tour 2015-10-02 16:29:59 -07:00
34c6f1bf4d Merge pull request #111 from kevgliss/105
Closes #105
2015-10-02 16:26:02 -07:00
2187898494 adding copy and a better profile picture for non-sso users 2015-10-02 15:36:50 -07:00
d4bc6ae7a1 Fixes #105 2015-10-02 13:46:13 -07:00
81cdb15353 Merge pull request #108 from kevgliss/description
description should be optional
2015-09-29 16:56:58 -07:00
5cfa9d4bc5 description should be optional 2015-09-29 16:37:32 -07:00
92da453233 Merge pull request #107 from waffle-iron/master
waffle.io Badge
2015-09-29 13:39:36 -07:00
2aedfedbd3 add waffle.io badge 2015-09-29 13:50:33 -06:00
64c9b11c09 Merge pull request #106 from kevgliss/release
version bump
2015-09-28 14:55:24 -07:00
5f87c87751 version bump 2015-09-28 14:54:58 -07:00
70f9022aae Merge pull request #104 from kevgliss/guide
Adding connections in user guides
2015-09-24 16:28:52 -07:00
43683fe554 changing readme language 2015-09-24 16:09:34 -07:00
002de6f5e4 adding docker Link 2015-09-24 16:03:15 -07:00
63a388236e adding a link to our techblog 2015-09-24 14:36:14 -07:00
9560791002 Merge pull request #99 from pandragoq/patch-1
Update index.rst
2015-09-24 14:28:06 -07:00
ed93b5a2c5 SSL 2015-09-24 09:36:11 -07:00
21e4cc9f4d Adding connections in user guides 2015-09-24 09:21:08 -07:00
73e628cbdf Merge pull request #103 from kevgliss/required
Marking fields as required
2015-09-24 08:52:46 -07:00
7ebd0bf5d4 making fields required 2015-09-24 08:42:31 -07:00
3f1902e0fe Merge pull request #100 from ivuk/fix-typo
Fix typos in docs/administration/index.rst
2015-09-23 16:44:49 -07:00
3e546eaa21 Fix typos in docs/administration/index.rst 2015-09-23 21:00:52 +02:00
e70deb155d Update index.rst
Right package for postgres is postgresql in ubuntu.
2015-09-22 16:57:53 -07:00
4f289c790b Merge pull request #98 from stacybird/grammar_fix
Fix grammar in index.rst
2015-09-22 15:48:37 -07:00
c15f525167 Fix grammar in index.rst 2015-09-22 15:33:37 -07:00
bcbf642122 Merge pull request #96 from kevgliss/install
clearing up docs based on feedback
2015-09-22 14:54:11 -07:00
1559727f2d Making make build the static assets 2015-09-22 14:49:37 -07:00
a596793a9a clearing up docs based on feedback 2015-09-22 14:18:38 -07:00
862bf3f619 Merge pull request #94 from kevgliss/notifications
Notifications
2015-09-22 13:37:51 -07:00
83a86c06a4 Merge pull request #93 from pandragoq/patch-1
Update index.rst
2015-09-22 13:37:40 -07:00
06a69c09a0 Fixing a bug where notifications associated during certificate creation would not be respected. 2015-09-22 13:01:05 -07:00
6a24e88d9a removing pip install instructions until available 2015-09-22 10:22:12 -07:00
be6a5b859e adding notification example 2015-09-22 09:46:54 -07:00
2444191bf2 Update index.rst
Typo on nginx spelling
2015-09-21 17:43:56 -07:00
9226b1eb4a Merge pull request #92 from konklone/patch-1
Rename SSL to TLS in many places
2015-09-21 15:25:00 -07:00
3f53629175 Re 2015-09-21 18:16:40 -04:00
baef329a4d Rename SSL to TLS 2015-09-21 18:16:19 -04:00
b103fc7bfb Rename SSL to TLS 2015-09-21 18:16:04 -04:00
a3385bd2ac Rename SSL to TLS 2015-09-21 18:15:25 -04:00
7cb50c654b Rename SSL to TLS 2015-09-21 18:15:06 -04:00
52ba538037 Rename SSL to TLS 2015-09-21 18:14:12 -04:00
0a0460529f Merge pull request #89 from kevgliss/cleanup
Cleaning up unneed/unused files
2015-09-20 10:21:04 -07:00
fc0a884d5f Cleaning up unneed/unused files 2015-09-20 09:49:16 -07:00
dbbea29e75 Merge pull request #88 from kevgliss/requirements
adding additional requirements so rtd can build the documation correctly
2015-09-19 11:32:57 -07:00
bcd0aae8c6 adding additional requirements so rtd can build the documation correctly 2015-09-19 11:31:31 -07:00
50d3e6aff2 Merge pull request #87 from kevgliss/typo
fixing typo
2015-09-19 10:25:52 -07:00
1d45926122 fixing typo 2015-09-19 10:24:56 -07:00
45626c947c Merge pull request #86 from kevgliss/docs
More documentation fixes
2015-09-19 10:21:56 -07:00
d7ca6d4327 More documentation fixes 2015-09-19 10:12:12 -07:00
6411bd56e9 Merge pull request #85 from kevgliss/documentation
Documentation
2015-09-19 09:48:25 -07:00
1486e7b8f6 adding information about sub commands 2015-09-19 09:41:50 -07:00
e73f2bcb2b setting default theme 2015-09-19 09:38:39 -07:00
a412569ff7 aligning doc version with tagged version 2015-09-19 09:34:48 -07:00
387194d651 Merge pull request #84 from kevgliss/docs
Adding flask sphinx auto-docs
2015-09-18 17:29:17 -07:00
13d0359041 Adding flask sphinx auto-docs 2015-09-18 17:28:48 -07:00
365d927efb Update README.rst 2015-09-18 16:28:45 -07:00
aa76379d6c Merge pull request #81 from kevgliss/pipy
Minor fixes
2015-09-18 15:56:06 -07:00
ef72de89b3 Minor fixes 2015-09-18 15:50:59 -07:00
e2962a4b8d Merge pull request #79 from kevgliss/order
fixing an error where dates components were not replaced in logical o…
2015-09-16 11:14:47 -07:00
a563986ce4 fixing an error where dates components were not replaced in logical order 2015-09-16 11:10:09 -07:00
a4e294634a Merge pull request #78 from kevgliss/docs
Doc fixes
2015-09-14 15:28:42 -07:00
067122f8f4 improving docs 2015-09-14 13:46:39 -07:00
6a1a744eff removing duplicate route 2015-09-12 10:05:58 -07:00
d3cf273a45 Merge pull request #72 from kevgliss/docker
[WIP] Docker
2015-09-11 15:36:25 -07:00
52e267468a Merge pull request #76 from kevgliss/verisignSource
Verisign source
2015-09-11 08:49:11 -07:00
e80b58899d following RFC 2015-09-11 08:39:27 -07:00
25f652c1eb fixing merge conflict 2015-09-11 08:38:48 -07:00
0dde4c9f80 removing source plugin form being installed atm 2015-09-11 08:36:55 -07:00
a512b82196 Merge pull request #75 from kevgliss/fixes
Fixes
2015-09-11 08:36:24 -07:00
7f119e95e1 making the verisign urls more generic 2015-09-11 08:27:34 -07:00
bf957d2509 moving to hotfix version of cryptography 2015-09-11 08:19:35 -07:00
1e314b505f fixing keyerror 2015-09-08 18:18:14 -07:00
ef9a80ebfd adding actual recipients 2015-09-08 18:03:18 -07:00
84d0afae4c fixing email internvals 2015-09-08 17:56:20 -07:00
48a53ad436 fixing error in default password creation 2015-09-08 17:42:57 -07:00
2f4aee49e2 adding logging 2015-09-08 10:56:23 -07:00
f3f5b9eeb3 adding password commandline option 2015-09-08 10:56:23 -07:00
541d5420bb Merge pull request #73 from kevgliss/cleanup
Cleanup
2015-09-08 08:43:56 -07:00
0383e2a1e1 adding manifest 2015-09-07 22:14:18 -07:00
084604cf3c fixing setup.py 2015-09-07 21:54:23 -07:00
8ab9c06778 removing more netflix 2015-09-04 15:54:52 -07:00
0afd4c94b4 removing more netflix 2015-09-04 15:54:02 -07:00
aaae4d5a1f unifying lemur defaults 2015-09-04 15:52:56 -07:00
9da713ab06 cleaning up references to netflix 2015-09-04 15:29:57 -07:00
540d56f0b8 Merge pull request #71 from kevgliss/notifications
Fixing issue with expiration emails not being sent
2015-09-04 09:46:06 -07:00
160eaa6901 Fixing issue with expiration emails not being sent 2015-09-04 09:24:55 -07:00
180c8228e1 adding verisign source 2015-09-02 14:37:07 -07:00
c182055dbe Merge pull request #70 from jeremy-h/master
Adding me to authors
2015-09-02 10:48:27 -07:00
97a289f02a Adding me to authors 2015-09-02 09:48:28 -07:00
089c0b2b1b Merge pull request #68 from kevgliss/crons
Crons
2015-09-02 09:35:46 -07:00
3b109ec578 Cleaning up temporary file creation, and revocation checking 2015-09-02 09:19:06 -07:00
45158c64a2 cleaning up temporary file creation 2015-09-02 09:19:06 -07:00
fe7b075f7b rely on stable version of cryptography instead of dev 2015-09-02 09:19:06 -07:00
a350940cd1 Adding command to fetch and publish verisign units 2015-09-02 09:19:06 -07:00
efec79d8de removing silly description validation from lemur and enforcing it on the cloudca plugin (who actually cares) 2015-09-02 09:15:12 -07:00
62950128a2 Adding a better error message for really long common names Fixes #38 2015-09-02 09:15:11 -07:00
aca69ce03c Closes #53 2015-09-02 09:15:11 -07:00
bf8ce354e5 Closes #55 2015-09-02 09:13:47 -07:00
8d09d865b1 Closes #57 2015-09-02 09:13:47 -07:00
8848146e8d Merge pull request #67 from kevgliss/authority
Authority fixes
2015-09-01 15:02:51 -07:00
480078da42 Removing str casting for role permission 2015-09-01 14:15:40 -07:00
46a5355377 Allows authorities to have editable owners and descriptions 2015-09-01 14:15:40 -07:00
3fb226ec11 Merge pull request #64 from kevgliss/validation
Validation of common name field
2015-08-29 14:01:31 -07:00
7471984ecf removing silly description validation from lemur and enforcing it on the cloudca plugin (who actually cares) 2015-08-29 13:57:07 -07:00
df9b345541 Adding a better error message for really long common names Fixes #38 2015-08-29 13:57:07 -07:00
d75c641848 Merge pull request #62 from kevgliss/notifications
Closes #53
2015-08-29 13:39:11 -07:00
a484a6e24d Closes #53 2015-08-29 13:07:30 -07:00
a7fd74396c Merge pull request #61 from kevgliss/editOwner
Closes #55
2015-08-29 12:09:09 -07:00
8977c5ddbf Ensuring notifications follow owner 2015-08-29 12:02:50 -07:00
bbb63b4aa6 Merge pull request #60 from kevgliss/customName
Closes #57
2015-08-29 12:00:53 -07:00
f492e9ec1b Closes #55 2015-08-29 11:53:46 -07:00
03e2991ced Closes #57 2015-08-29 11:48:39 -07:00
80136834b5 Merge pull request #59 from kevgliss/cleanup
Cleanup
2015-08-29 10:30:03 -07:00
3b2f71cc8a Merge pull request #58 from kevgliss/configBasedNames
Adding ability to define distinguished names in config
2015-08-29 10:23:21 -07:00
572c44b78b Adding a some more docs around oauth2 2015-08-29 10:15:31 -07:00
783acf6d8c Removing Meechum specific code 2015-08-29 10:11:03 -07:00
fc22f76708 Updating supported versions 2015-08-29 09:54:56 -07:00
6ec5d26f0c Merge pull request #51 from jeremy-h/elb-ssl-automation
Elb ssl automation
2015-08-29 09:52:35 -07:00
53ce9cac4c Fix a typo, add a typo 2015-08-27 15:55:39 -07:00
51800d5e4b Added better error handling
Added a "dry run" option
2015-08-27 15:48:49 -07:00
627b36d2a5 Adding method to get existing listeners 2015-08-27 15:45:00 -07:00
70ccd137e1 removing netflix specific code from auth flow 2015-08-27 13:09:02 -07:00
9a04371680 Adding ability to define distinguished names in config 2015-08-27 12:59:40 -07:00
f799ff3af1 Seeing if using decode explicity this helps py3 problem 2015-08-24 20:10:03 -07:00
6db1d0b031 fixing unicode support 2015-08-24 16:37:24 -07:00
d599aaa410 Updating to handle unicode in python 2 and 3$
added retry with backoff for the SSL cert to show up after it is added (CAP, ftw)$
2015-08-24 16:17:04 -07:00
09bc79ef84 Merge remote-tracking branch 'upstream/master' into elb-ssl-automation 2015-08-24 12:18:40 -07:00
6e39a1e666 Finished glue code to push ELBs. 2015-08-24 12:18:15 -07:00
bb51b59400 Merge pull request #49 from kevgliss/orgname
Orgname
2015-08-24 10:00:09 -07:00
75de814b15 Adding new verisign error 2015-08-24 09:43:30 -07:00
b4c348aef7 switching out default orgname 2015-08-24 09:41:03 -07:00
3476d3bcf3 Merge pull request #48 from kevgliss/fixes
Fixes
2015-08-22 13:04:02 -07:00
45c442000e Fixing some unfortunate casting that prevent creators from viewing/updating their certs 2015-08-22 10:56:15 -07:00
a07db5625b Fixing an issue were extensions were implicitly required 2015-08-22 10:22:36 -07:00
3df50f15f7 Merge pull request #47 from kevgliss/keys
Fixing issue with a certificate with no role not being viewable
2015-08-21 16:18:13 -07:00
4b7a55c89f Fixing issue with a certificate with no role not being viewable 2015-08-21 16:08:53 -07:00
3ff5cdf43f Merge remote-tracking branch 'upstream/master' into elb-ssl-automation 2015-08-21 14:29:03 -07:00
dbfd6b1e17 Fixing this so it pulls the named option 2015-08-21 13:09:29 -07:00
4b9a05198c Merge pull request #46 from kevgliss/b64fix
Fixing an issue with futures
2015-08-20 16:02:31 -07:00
d62f57eab3 Fixing an issue with futures, unicode and b64 not being able to handle the unicode values 2015-08-20 15:49:08 -07:00
96c3ab7f9d Merge remote-tracking branch 'upstream/master' into elb-ssl-automation 2015-08-20 15:46:11 -07:00
38ebeab163 Refactoring.. with pep8 fixes 2015-08-20 15:45:53 -07:00
fcfaa21a24 Refactoring 2015-08-20 15:45:42 -07:00
0f0d11a828 Merge pull request #45 from kevgliss/authByOwner
Fixes #35
2015-08-19 18:08:55 -07:00
6b2da2fe6b Fixes #35 2015-08-19 18:05:18 -07:00
74525e8e8e Merge pull request #44 from kevgliss/domains
Fixing bug were domains would not have correct pagination
2015-08-19 16:58:56 -07:00
cbcc8af3bd Fixing bug were domains would not have correct pagination 2015-08-19 16:42:56 -07:00
ab7b0c442c provisionelb creates certs. needs some cleanup and the rest of the glue 2015-08-19 16:10:45 -07:00
39c022dbf3 Merge pull request #43 from kevgliss/decryption
Ensure there are no accidental newlines when fetching the ENCRYPTION_KEY
2015-08-19 15:49:07 -07:00
b00917aa60 Ensure there are no accidental newlines when fetching the ENCRYPTION_KEY 2015-08-19 15:46:10 -07:00
4a0328cd8f Merge pull request #42 from kevgliss/notify
Minor fixes in notifications
2015-08-19 10:29:14 -07:00
b96af3a1f1 Editing footer text 2015-08-19 10:10:19 -07:00
28e12a973f Misc fixed around certificate notifications 2015-08-19 10:07:22 -07:00
1883f3c0e7 Merge pull request #41 from kevgliss/sync
Misc fixed around certificate syncing
2015-08-18 16:21:11 -07:00
c6747439fb Misc fixed around certificate syncing 2015-08-18 16:17:20 -07:00
0b9c814ea5 Merge pull request #40 from kevgliss/privateKey
Fixing issue with creating roles
2015-08-18 09:30:25 -07:00
f09f5eb0f1 Fixing issue with creating roles 2015-08-17 22:51:29 -07:00
95ac5245e1 Merge pull request #39 from kevgliss/notificationInterval
Notification interval
2015-08-17 20:52:32 -07:00
dd607e5c07 Making CLOUDCA_API_ENDPOINT configurable 2015-08-17 17:09:31 -07:00
eb55d5465f Making LEMUR_DEFAULT_SECURITY_EMAIL optional 2015-08-17 16:03:57 -07:00
500b212a25 Adding a few default expiration intervals 2015-08-17 15:49:16 -07:00
7554a86d23 Merge pull request #37 from kevgliss/fixes
General fixes around build time issues
2015-08-14 10:11:46 -07:00
43d4dbbfbd Fixing the paths related to javascript dependecies 2015-08-14 10:05:30 -07:00
90e49613f9 develop doesn't need to build the static files, the make develop will do that 2015-08-11 16:21:00 -07:00
d3ff79d800 Getting correct path to readme so that it doesn't matter where setup.py is run from 2015-08-11 15:46:54 -07:00
bfcbd1b065 Fixes issue where client authentication was not displaying in the UI 2015-08-11 15:43:59 -07:00
b488c349e8 Look for compiled static files, to see if they need to be created 2015-08-11 14:53:28 -07:00
590f43297f Merge pull request #34 from Netflix/0.1.x
Joining 0.1.x with master
2015-08-09 17:01:31 -07:00
b8720566d7 fixing merge conflict 2015-08-09 16:52:14 -07:00
d0d3e06c81 fixing merge conflicts 2015-08-09 16:51:25 -07:00
48f38a8625 Fixing bad cherry pick 2015-08-09 16:49:18 -07:00
13f34fc600 Merge pull request #21 from kevgliss/buildfixes
Build Fixes
2015-08-09 16:47:39 -07:00
f679392c61 Fixing bad cherry pick 2015-07-19 19:28:49 -07:00
f78e9d47d1 Merge pull request #21 from kevgliss/buildfixes
Build Fixes
2015-07-19 19:21:46 -07:00
92a3c1a5a0 Merge pull request #14 from kevgliss/master
A few misc fixes found during testing.
2015-07-02 14:14:49 -07:00
223 changed files with 10297 additions and 5783 deletions

View File

@ -1,3 +1,3 @@
{
"directory": "lemur/static/app/vendor/bower_components"
"directory": "bower_components"
}

3
.gitattributes vendored
View File

@ -1 +1,2 @@
* text=auto
* text=auto
version.py export-subst

1
.gitignore vendored
View File

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

View File

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

View File

@ -9,10 +9,8 @@ matrix:
include:
- python: "2.7"
env: TOXENV=py27
- python: "3.3"
env: TOXENV=py33
- python: "3.4"
env: TOXENV=py34
- python: "3.5"
env: TOXENV=py35
cache:
directories:
@ -23,9 +21,6 @@ env:
global:
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
install:
- make dev-postgres
before_script:
- psql -c "create database lemur;" -U postgres
- psql -c "create user lemur with password 'lemur;'" -U postgres
@ -36,4 +31,4 @@ script:
notifications:
email:
kglisson@netflix.com
kglisson@netflix.com

View File

@ -1 +1,3 @@
- Kevin Glisson (kglisson@netflix.com)
- Kevin Glisson <kglisson@netflix.com>
- Jeremy Heffner <jheffner@netflix.com>

109
CHANGELOG.rst Normal file
View File

@ -0,0 +1,109 @@
Changelog
=========
0.3.1 - `master`
~~~~~~~~~~~~~~~~
.. note:: This version is not yet released and is under active development
0.3.0 - `2016-06-06`
~~~~~~~~~~~~~~~~
This is quite a large upgrade, it is highly advised you backup your database before attempting to upgrade as this release
requires the migration of database structure as well as data.
Upgrading
---------
Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
Source Plugin Owners
--------------------
The dictionary returned from a source plugin has changed keys from `public_certificate` to `body` and `intermediate_certificate` to chain.
Issuer Plugin Owners
--------------------
This release may break your plugins, the keys in `issuer_options` have been changed from `camelCase` to `under_score`.
This change was made to break a undue reliance on downstream options maintains a more pythonic naming convention. Renaming
these keys should be fairly trivial, additionally pull requests have been submitted to affected plugins to help ease the transition.
.. note:: This change only affects issuer plugins and does not affect any other types of plugins.
* Closed `#63 <https://github.com/Netflix/lemur/issues/63>`_ - Validates all endpoints with Marshmallow schemas, this allows for
stricter input validation and better error messages when validation fails.
* Closed `#146 <https://github.com/Netflix/lemur/issues/146>`_ - Moved authority type to first pane of authority creation wizard.
* Closed `#147 <https://github.com/Netflix/lemur/issues/147>`_ - Added and refactored the relationship between authorities and their
root certificates. Displays the certificates (and chains) next the the authority in question.
* Closed `#199 <https://github.com/Netflix/lemur/issues/199>`_ - Ensures that the dates submitted to Lemur during authority and
certificate creation are actually dates.
* Closed `#230 <https://github.com/Netflix/lemur/issues/230>`_ - Migrated authority dropdown to a ui-select based dropdown, this
should be easier to determine what authorities are available and when an authority has actually been selected.
* Closed `#254 <https://github.com/Netflix/lemur/issues/254>`_ - Forces certificate names to be generally unique. If a certificate name
(generated or otherwise) is found to be a duplicate we increment by appending a counter.
* Closed `#254 <https://github.com/Netflix/lemur/issues/275>`_ - Switched to using Fernet generated passphrases for exported items.
These are more sounds that pseudo random passphrases generated before and have the nice property of being in base64.
* Closed `#278 <https://github.com/Netflix/lemur/issues/278>`_ - Added ability to specify a custom name to certificate creation, previously
this was only available in the certificate import wizard.
* Closed `#281 <https://github.com/Netflix/lemur/issues/281>`_ - Fixed an issue where notifications could not be removed from a certificate
via the UI.
* Closed `#289 <https://github.com/Netflix/lemur/issues/289>`_ - Fixed and issue where intermediates were not being properly exported.
* Closed `#315 <https://github.com/Netflix/lemur/issues/315>`_ - Made how roles are associated with certificates and authorities much more
explict, including adding the ability to add roles directly to certificates and authorities on creation.
0.2.2 - 2016-02-05
~~~~~~~~~~~~~~~~~~
* Closed `#234 <https://github.com/Netflix/lemur/issues/234>`_ - Allows export plugins to define whether they need
private key material (default is True)
* Closed `#231 <https://github.com/Netflix/lemur/issues/231>`_ - Authorities were not respecting 'owning' roles and their
users
* Closed `#228 <https://github.com/Netflix/lemur/issues/228>`_ - Fixed documentation with correct filter values
* Closed `#226 <https://github.com/Netflix/lemur/issues/226>`_ - Fixes issue were `import_certificate` was requiring
replacement certificates to be specified
* Closed `#224 <https://github.com/Netflix/lemur/issues/224>`_ - Fixed an issue where NPM might not be globally available (thanks AlexClineBB!)
* Closed `#221 <https://github.com/Netflix/lemur/issues/234>`_ - Fixes several reported issues where older migration scripts were
missing tables, this change removes pre 0.2 migration scripts
* Closed `#218 <https://github.com/Netflix/lemur/issues/234>`_ - Fixed an issue where export passphrases would not validate
0.2.1 - 2015-12-14
~~~~~~~~~~~~~~~~~~
* Fixed bug with search not refreshing values
* Cleaned up documentation, including working supervisor example (thanks rpicard!)
* Closed #165 - Fixed an issue with email templates
* Closed #188 - Added ability to submit third party CSR
* Closed #176 - Java-export should allow user to specify truststore/keystore
* Closed #176 - Extended support for exporting certificate in P12 format
0.2.0 - 2015-12-02
~~~~~~~~~~~~~~~~~~
* Closed #120 - Error messages not displaying long enough
* Closed #121 - Certificate create form should not be valid until a Certificate Authority object is available
* Closed #122 - Certificate API should allow for the specification of preceding certificates
You can now target a certificate(s) for replacement. When specified the replaced certificate will be marked as
'inactive'. This means that there will be no notifications for that certificate.
* Closed #139 - SubCA autogenerated descriptions for their certs are incorrect
* Closed #140 - Permalink does not change with filtering
* Closed #144 - Should be able to search certificates by domains covered, included wildcards
* Closed #165 - Cleaned up expiration notification template
* Closed #160 - Cleaned up quickstart documentation (thanks forkd!)
* Closed #144 - Now able to search by all domains in a given certificate, not just by common name
0.1.5 - 2015-10-26
~~~~~~~~~~~~~~~~~~
* **SECURITY ISSUE**: Switched from use a AES static key to Fernet encryption.
Affects all versions prior to 0.1.5. If upgrading this will require a data migration.
see: `Upgrading Lemur <https://lemur.readthedocs.com/adminstration#UpgradingLemur>`_

View File

@ -1,4 +1,3 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include setup.py version.py package.json bower.json gulpfile.js README.rst MANIFEST.in LICENSE AUTHORS
recursive-include lemur/plugins/lemur_email/templates *
recursive-include lemur/static *
global-exclude *~

View File

@ -6,11 +6,11 @@ develop: update-submodules setup-git
npm install
pip install "setuptools>=0.9.8"
# order matters here, base package must install first
# this is temporary until the version we need is released
pip install -e 'git+https://git@github.com/pyca/cryptography.git#egg=cryptography-1.0.dev1'
pip install -e .
pip install "file://`pwd`#egg=lemur[dev]"
pip install "file://`pwd`#egg=lemur[tests]"
node_modules/.bin/gulp build
node_modules/.bin/gulp package
@echo ""
dev-docs:

1
OSSMETADATA Normal file
View File

@ -0,0 +1 @@
osslifecycle=active

View File

@ -1,31 +1,32 @@
Lemur
=====
.. image:: https://img.shields.io/pypi/v/lemur.svg
:target: https://pypi.python.org/pypi/lemur/
:alt: Latest Version
.. image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/Netflix/lemur
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. image:: https://readthedocs.org/projects/lemur/badge/?version=latest
:target: https://lemur.readthedocs.org
:alt: Latest Docs
.. image:: https://magnum.travis-ci.com/Netflix/lemur.svg?branch=master
:target: https://magnum.travis-ci.com/Netflix/lemur
.. image:: https://travis-ci.org/Netflix/lemur.svg
:target: https://travis-ci.org/Netflix/lemur
.. image:: https://requires.io/github/Netflix/lemur/requirements.svg?branch=master
:target: https://requires.io/github/Netflix/lemur/requirements/?branch=master
:alt: Requirements Status
Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults.
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.
It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on OS X.
It works on CPython 2.7. It is known
to work on Ubuntu Linux and OS X.
Project resources
=================
- `Lemur Blog Post <http://techblog.netflix.com/2015/09/introducing-lemur.html>`_
- `Documentation <http://lemur.readthedocs.org/>`_
- `Source code <https://github.com/netflix/lemur>`_
- `Issue tracker <https://github.com/netflix/lemur/issues>`_
.. image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/Netflix/lemur
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
- `Docker <https://github.com/Netflix/lemur-docker>`_

View File

@ -6,38 +6,46 @@
},
"private": true,
"dependencies": {
"angular": "1.3",
"json3": "~3.3",
"es5-shim": "~4.0",
"jquery": "~2.1",
"angular-resource": "1.2.15",
"angular-cookies": "1.2.15",
"angular-sanitize": "1.2.15",
"angular-route": "1.2.15",
"angular-strap": "~2.0.2",
"restangular": "~1.4.0",
"ng-table": "~0.5.4",
"ngAnimate": "*",
"moment": "~2.6.0",
"angular-animate": "~1.4.0",
"angular-loading-bar": "~0.6.0",
"fontawesome": "~4.2.0",
"jquery": "~2.2.0",
"angular-wizard": "~0.4.0",
"bootswatch": "3.3.1+2",
"angular": "1.4.9",
"json3": "~3.3",
"es5-shim": "~4.5.0",
"bootstrap": "~3.3.6",
"angular-bootstrap": "~1.1.1",
"angular-animate": "~1.4.9",
"restangular": "~1.5.1",
"ng-table": "~0.8.3",
"moment": "~2.11.1",
"angular-loading-bar": "~0.8.0",
"angular-moment": "~0.10.3",
"moment-range": "~2.1.0",
"angular-spinkit": "~0.3.3",
"angular-bootstrap": "~0.12.0",
"angular-clipboard": "~1.3.0",
"angularjs-toaster": "~1.0.0",
"angular-chart.js": "~0.8.8",
"ngletteravatar": "~4.0.0",
"bootswatch": "~3.3.6",
"fontawesome": "~4.5.0",
"satellizer": "~0.13.4",
"angular-ui-router": "~0.2.15",
"font-awesome": "~4.5.0",
"lodash": "~4.0.1",
"underscore": "~1.8.3",
"angular-smart-table": "~2.1.6",
"angular-strap": ">= 2.2.2",
"angular-underscore": "^0.5.0",
"angular-translate": "^2.9.0",
"angular-ui-switch": "~0.1.0",
"angular-chart.js": "~0.7.1",
"satellizer": "~0.9.4",
"angularjs-toaster": "~0.4.14"
},
"devDependencies": {
"angular-mocks": "~1.3",
"angular-scenario": "~1.3"
"angular-sanitize": "^1.5.0",
"angular-file-saver": "~1.0.1",
"angular-ui-select": "~0.17.1",
"d3": "^3.5.17"
},
"resolutions": {
"bootstrap": "~3.3.1",
"angular": "1.3"
"moment": ">=2.8.0 <2.11.0",
"lodash": ">=1.3.0 <2.5.0",
"angular": "1.4.9"
},
"ignore": [
"**/.*",

View File

@ -2,7 +2,7 @@ Configuration
=============
.. warning::
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configruation
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configuration
file. It is highly advised that you do not store your secrets in this file! Lemur provides functions
that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management <CredentialManagement>`
for more information.
@ -24,7 +24,6 @@ Basic Configuration
LOG_FILE = "/logs/lemur/lemur-test.log"
.. data:: debug
:noindex:
@ -34,7 +33,6 @@ Basic Configuration
debug = False
.. warning::
This should never be used in a production environment as it exposes Lemur to
remote code execution through the debug console.
@ -72,9 +70,7 @@ Basic Configuration
.. data:: LEMUR_TOKEN_SECRET
:noindex:
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and be kept private.
See `SECRET_KEY` for methods on secure secret generation.
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and kept private.
::
@ -89,31 +85,85 @@ Basic Configuration
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))
.. data:: LEMUR_ENCRYPTION_KEY
.. data:: LEMUR_ENCRYPTION_KEYS
:noindex:
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
to start.
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
to start. Multiple keys can be provided to facilitate key rotation. The first key in the list is used for
encryption and all keys are tried for decryption until one works. Each key must be 32 URL safe base-64 encoded bytes.
See `LEMUR_TOKEN_SECRET` for methods of secure secret generation.
Running lemur create_config will securely generate a key for your configuration file.
If you would like to generate your own, we recommend the following method:
>>> import os
>>> import base64
>>> base64.urlsafe_b64encode(os.urandom(32))
::
LEMUR_ENCRYPTION_KEY = 'supersupersecret'
LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']
Certificate Default Options
---------------------------
Lemur allows you to fine tune your certificates to your organization. The following defaults are presented in the UI
and are used when Lemur creates the CSR for your certificates.
.. data:: LEMUR_DEFAULT_COUNTRY
:noindex:
::
LEMUR_DEFAULT_COUNTRY = "US"
.. data:: LEMUR_DEFAULT_STATE
:noindex:
::
LEMUR_DEFAULT_STATE = "California"
.. data:: LEMUR_DEFAULT_LOCATION
:noindex:
::
LEMUR_DEFAULT_LOCATION = "Los Gatos"
.. data:: LEMUR_DEFAULT_ORGANIZATION
:noindex:
::
LEMUR_DEFAULT_ORGANIZATION = "Netflix"
.. data:: LEMUR_DEFAULT_ORGANIZATION_UNIT
:noindex:
::
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations"
Notification Options
--------------------
Lemur currently has very basic support for notifications. Notifications are sent to the certificate creator, owner and
security team as specified by the `SECURITY_TEAM_EMAIL` configuration parameter.
Lemur currently has very basic support for notifications. Currently only expiration notifications are supported. Actual notification
is handled by the notification plugins that you have configured. Lemur ships with the 'Email' notification that allows expiration emails
to be sent to subscribers.
The template for all of these notifications lives under lemur/template/event.html and can be easily modified to fit your
needs.
Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs.
Notifications are sent to the certificate creator, owner and security team as specified by the `LEMUR_SECURITY_TEAM_EMAIL` configuration parameter.
Certificates marked as in-active will **not** be notified of upcoming expiration. This enables a user to essentially
silence the expiration. If a certificate is active and is expiring the above will be notified at 30, 15, 5, 2 days
respectively.
Certificates marked as inactive will **not** be notified of upcoming expiration. This enables a user to essentially
silence the expiration. If a certificate is active and is expiring the above will be notified according to the `LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS` or
30, 15, 2 days before expiration if no intervals are set.
Lemur supports sending certification expiration notifications through SES and SMTP.
@ -121,124 +171,189 @@ Lemur supports sending certification expiration notifications through SES and SM
.. data:: LEMUR_EMAIL_SENDER
:noindex:
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
.. note::
If using STMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
.. note::
If using SMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
you can send any mail. See: `Verifying Email Address in Amazon SES <http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html>`_
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
you can send any mail. See: `Verifying Email Address in Amazon SES <http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html>`_
.. data:: LEMUR_MAIL
:noindex:
Lemur sender's email
Lemur sender's email
::
LEMUR_MAIL = 'lemur.example.com'
LEMUR_MAIL = 'lemur.example.com'
.. data:: LEMUR_SECURITY_TEAM_EMAIL
:noindex:
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
This is an email or list of emails that should be notified when a certificate is expiring. It is also the contact email address for any discovered certificate.
::
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
Authority Options
-----------------
Authorities will each have their own configuration options. There are currently two plugins bundled with Lemur,
Verisign/Symantec and CloudCA
.. data:: VERISIGN_URL
.. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS
:noindex:
This is the url for the verisign API
Lemur notification intervals
::
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
.. data:: VERISIGN_PEM_PATH
Authentication Options
----------------------
Lemur currently supports Basic Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
If you are not using an authentication provider you do not need to configure any of these options.
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
.. data:: ACTIVE_PROVIDERS
:noindex:
This is the path to the mutual SSL certificate used for communicating with Verisign
::
.. data:: CLOUDCA_URL
:noindex:
This is the URL for CLoudCA API
.. data:: CLOUDCA_PEM_PATH
:noindex:
This is the path to the mutual SSL Certificate use for communicating with CLOUDCA
.. data:: CLOUDCA_BUNDLE
:noindex:
This is the path to the CLOUDCA certificate bundle
Authentication
--------------
Lemur currently supports Basic Authentication and Ping OAuth2, additional flows can be added relatively easily
If you are not using PING you do not need to configure any of these options
ACTIVE_PROVIDERS = ["ping", "google"]
.. data:: PING_SECRET
:noindex:
::
::
PING_SECRET = 'somethingsecret'
PING_SECRET = 'somethingsecret'
.. data:: PING_ACCESS_TOKEN_URL
:noindex:
::
::
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
PING_ACCESS_TOKEN_URL = "https://<yourpingserver>/as/token.oauth2"
.. data:: PING_USER_API_URL
:noindex:
::
::
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
PING_USER_API_URL = "https://<yourpingserver>/idp/userinfo.openid"
.. data:: PING_JWKS_URL
:noindex:
::
::
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
PING_JWKS_URL = "https://<yourpingserver>/pf/JWKS"
.. data:: PING_NAME
:noindex:
::
PING_NAME = "Example Oauth2 Provider"
.. data:: PING_CLIENT_ID
:noindex:
::
PING_CLIENT_ID = "client-id"
.. data:: GOOGLE_CLIENT_ID
:noindex:
::
GOOGLE_CLIENT_ID = "client-id"
.. data:: GOOGLE_SECRET
:noindex:
::
GOOGLE_SECRET = "somethingsecret"
Plugin Specific Options
-----------------------
Verisign Issuer Plugin
^^^^^^^^^^^^^^^^^^^^^^
Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur,
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
for those plugins.
.. data:: VERISIGN_URL
:noindex:
This is the url for the Verisign API
.. data:: VERISIGN_PEM_PATH
:noindex:
This is the path to the mutual TLS certificate used for communicating with Verisign
.. data:: VERISIGN_FIRST_NAME
:noindex:
This is the first name to be used when requesting the certificate
.. data:: VERISIGN_LAST_NAME
:noindex:
This is the last name to be used when requesting the certificate
.. data:: VERISIGN_EMAIL
:noindex:
This is the email to be used when requesting the certificate
.. data:: VERISIGN_INTERMEDIATE
:noindex:
This is the intermediate to be used for your CA chain
.. data:: VERISIGN_ROOT
:noindex:
This is the root to be used for your CA chain
AWS Plugin Configuration
========================
AWS Source/Destination Plugin
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In order for Lemur to manage it's own account and other accounts we must ensure it has the correct AWS permissions.
In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions.
.. note:: AWS usage is completely optional. Lemur can upload, find and manage SSL certificates in AWS. But is not required to do so.
.. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so.
Setting up IAM roles
--------------------
""""""""""""""""""""
Lemur's aws plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
Lemur's AWS plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
In order to limit the permissions we will create a new two IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
In order to limit the permissions, we will create two new IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
Lemur uses to STS to talk to different accounts. For managing one account this isn't necessary but we will still use it so that we can easily add new accounts.
LemurInstanceProfile is the IAM role you will launch your instance with. It actually has almost no rights. In fact it should really only be able to use STS to assume role to the Lemur role.
Here is are example polices for the LemurInstanceProfile:
Here are example policies for the LemurInstanceProfile:
SES-SendEmail
@ -276,7 +391,12 @@ STS-AssumeRole
Next we will create the the Lemur IAM role. Lemur
Next we will create the the Lemur IAM role.
.. note::
The default IAM role that Lemur assumes into is called `Lemur`, if you need to change this ensure you set `LEMUR_INSTANCE_PROFILE` to your role name in the configuration.
Here is an example policy for Lemur:
@ -328,7 +448,8 @@ IAM-ServerCertificate
Setting up STS access
---------------------
"""""""""""""""""""""
Once we have setup our accounts we need to ensure that we create a trust relationship so that LemurInstanceProfile can assume the Lemur role.
In the AWS console select the Lemur IAM role and select the Trust Relationships tab and click Edit Trust Relationship
@ -355,7 +476,7 @@ Below is an example policy:
Adding N+1 accounts
-------------------
"""""""""""""""""""
To add another account we go to the new account and create a new Lemur IAM role with the same policy as above.
@ -383,7 +504,7 @@ An example policy:
}
Setting up SES
--------------
""""""""""""""
Lemur has built in support for sending it's certificate notifications via Amazon's simple email service (SES). To force
Lemur to use SES ensure you are the running as the IAM role defined above and that you have followed the steps outlined
@ -393,23 +514,11 @@ The configuration::
LEMUR_MAIL = 'lemur.example.com'
Will be sender of all notifications, so ensure that it is verified with AWS.
Will be the sender of all notifications, so ensure that it is verified with AWS.
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
settings.
Upgrading Lemur
===============
Lemur provides an easy way to upgrade between versions. Simply download the newest
version of Lemur from pypi and then apply any schema cahnges with the following command.
.. code-block:: bash
$ lemur db upgrade
.. note:: Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
.. _CommandLineInterface:
Command Line Interface
@ -475,69 +584,144 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
lemur db upgrade
.. data:: create_user
Creates new users within Lemur.
::
lemur create_user -u jim -e jim@example.com
.. data:: create_role
Creates new roles within Lemur.
::
lemur create_role -n example -d "a new role"
.. data:: check_revoked
Traverses every certificate that Lemur is aware of and attempts to understand it's validity.
Traverses every certificate that Lemur is aware of and attempts to understand its validity.
It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates
validity it's status is marked 'unknown'
validity its status is marked 'unknown'.
.. data:: sync
Sync attempts to discover certificates in the environment that were not created by Lemur. If you wish to only sync
a few sources you can pass a comma delimited list of sources to sync
a few sources you can pass a comma delimited list of sources to sync.
::
lemur sync source1,source2
lemur sync -s source1,source2
Additionally you can also list the available sources that Lemur can sync
Additionally you can also list the available sources that Lemur can sync.
::
lemur sync -list
lemur sync
.. data:: notify
Will traverse all current notifications and see if any of them need to be triggered.
::
lemur notify
Sub-commands
------------
Lemur includes several sub-commands for interacting with Lemur such as creating new users, creating new roles and even
issuing certificates.
The best way to discover these commands is by using the built in help pages
::
lemur --help
and to get help on sub-commands
::
lemur certificates --help
Upgrading Lemur
===============
To upgrade Lemur to the newest release you will need to ensure you have the lastest code and have run any needed
database migrations.
To get the latest code from github run
::
cd <lemur-source-directory>
git pull -t <version>
python setup.py develop
.. note::
It's important to grab the latest release by specifying the release tag. This tags denote stable versions of Lemur.
If you want to try the bleeding edge version of Lemur you can by using the master branch.
After you have the latest version of the Lemur code base you must run any needed database migrations. To run migrations
::
cd <lemur-source-directory>/lemur
lemur db upgrade
This will ensure that any needed tables or columns are created or destroyed.
.. note::
Internally, this uses `Alembic <https://alembic.readthedocs.org/en/latest/>`_ to manage database migrations.
.. note::
By default Alembic looks for the `migrations` folder in the current working directory.The migrations folder is
located under `<LEMUR_HOME>/lemur/migrations` if you are running the lemur command from any location besides
`<LEMUR_HOME>/lemur` you will need to pass the `-d` flag to specify the absolute file path to the `migrations` folder.
Plugins
=======
There are several interfaces currently available to extend Lemur. These are a work in
progress and the API is not frozen.
Bundled Plugins
---------------
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
3rd Party Extensions
--------------------
The following extensions are available and maintained by members of the Lemur community:
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
get it added.
Want to create your own extension? See :doc:`../developer/plugins/index` to get started.
Identity and Access Management
==============================
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
user is first created in Lemur the can be assigned one or more roles. These roles are typically dynamically created
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
meaning.
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
as ViewPrivateKeyPermission are compositions of these three main Permissions.
Lets take a look at how these permissions used:
Lets take a look at how these permissions are used:
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
that the `Authority` is associated with it Lemur allows that user to user/view/update that `Authority`.
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
private key.
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
private key information.
These permissions are applied to the user upon login and refreshed on every request.
.. seealso::
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_

View File

@ -1,2 +1 @@
Change Log
==========
.. include:: ../CHANGELOG.rst

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# security_monkey documentation build configuration file, created by
# lemur documentation build configuration file, created by
# sphinx-quickstart on Sat Jun 7 18:43:48 2014.
#
# This file is execfile()d with the current directory set to its
@ -11,7 +11,6 @@
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
@ -54,10 +53,12 @@ copyright = u'2015, Netflix Inc.'
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1.1'
base_dir = os.path.join(os.path.dirname(__file__), os.pardir)
about = {}
with open(os.path.join(base_dir, "lemur", "__about__.py")) as f:
exec(f.read(), about)
version = release = about["__version__"]
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -100,9 +101,13 @@ pygments_style = 'sphinx'
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View File

@ -144,6 +144,17 @@ If you've made changes and need to compile them by hand for any reason, you can
The minified and processed files should be committed alongside the unprocessed changes.
It's also important to note that Lemur's frontend and API are not tied together. The API does not serve any of the static assets, we rely on nginx or some other file server to server all of the static assets.
During development that means we need an additional server to serve those static files for the GUI.
This is accomplished with a Gulp task:
::
./node_modules/.bin/gulp serve
The gulp task compiles all the JS/CSS/HTML files and opens the Lemur welcome page in your default browsers. Additionally any changes to made to the JS/CSS/HTML with be reloaded in your browsers.
Developing with Flask
----------------------
@ -180,19 +191,92 @@ You can see a list of open pull requests (pending changes) by visiting https://g
Pull requests should be against **master** and pass all TravisCI checks
Plugins
=======
Writing a Plugin
================
.. toctree::
:maxdepth: 1
:maxdepth: 2
plugins/index
REST API
========
Lemur's front end is entirely API driven. Any action that you can accomplish via the UI can also be accomplished by the
UI. The following is documents and provides examples on how to make requests to the Lemur API.
Authentication
--------------
.. automodule:: lemur.auth.views
:members:
:undoc-members:
:show-inheritance:
Destinations
------------
.. automodule:: lemur.destinations.views
:members:
:undoc-members:
:show-inheritance:
Notifications
-------------
.. automodule:: lemur.notifications.views
:members:
:undoc-members:
:show-inheritance:
Users
-----
.. automodule:: lemur.users.views
:members:
:undoc-members:
:show-inheritance:
Roles
-----
.. automodule:: lemur.roles.views
:members:
:undoc-members:
:show-inheritance:
Certificates
------------
.. automodule:: lemur.certificates.views
:members:
:undoc-members:
:show-inheritance:
Authorities
-----------
.. automodule:: lemur.authorities.views
:members:
:undoc-members:
:show-inheritance:
Domains
-------
.. automodule:: lemur.domains.views
:members:
:undoc-members:
:show-inheritance:
Internals
=========
.. toctree::
:maxdepth: 1
:maxdepth: 2
internals/lemur

View File

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

View File

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

View File

@ -96,5 +96,4 @@ Subpackages
lemur.notifications
lemur.plugins
lemur.roles
lemur.status
lemur.users

View File

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

View File

@ -1,6 +1,3 @@
Writing a Plugin
================
Several interfaces exist for extending Lemur:
* Issuer (lemur.plugins.base.issuer)
@ -8,7 +5,7 @@ Several interfaces exist for extending Lemur:
* Source (lemur.plugins.base.source)
* Notification (lemur.plugins.base.notification)
Each interface has its own function that will need to be defined in order for
Each interface has its own functions that will need to be defined in order for
your plugin to work correctly. See :ref:`Plugin Interfaces <PluginInterfaces>` for details.
@ -91,7 +88,7 @@ Issuer
Issuer plugins are used when you have an external service that creates certificates or authorities.
In the simple case the third party only issues certificates (Verisign, DigiCert, etc.).
If you have a third party or internal service that creates authorities (CloudCA, EJBCA, etc.), Lemur has you covered,
If you have a third party or internal service that creates authorities (EJBCA, etc.), Lemur has you covered,
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
@ -214,8 +211,8 @@ certificate Lemur does not know about and adding the certificate to it's invento
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
.. warning::
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarentee that ]
.. warning::
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron
job if you need a higher resolution sync job.
@ -226,10 +223,31 @@ The `SourcePlugin` object requires implementation of one function::
# request.get("some source of certificates")
.. Note::
.. note::
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
Export
------
Formats, formats and more formats. That's the current PKI landscape. See the always relevant `xkcd <https://xkcd.com/927/>`_.
Thankfully Lemur supports the ability to output your certificates into whatever format you want. This integration comes by the way
of Export plugins. Support is still new and evolving, the goal of these plugins is to return raw data in a new format that
can then be used by any number of applications. Included in Lemur is the `JavaExportPlugin` which currently supports generating
a Java Key Store (JKS) file for use in Java based applications.
The `ExportPlugin` object requires the implementation of one function::
def export(self, body, chain, key, options, **kwargs):
# sys.call('openssl hokuspocus')
# return "extension", passphrase, raw
.. note::
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to these calls.
Testing
=======

View File

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

53
docs/doing-a-release.rst Normal file
View File

@ -0,0 +1,53 @@
Doing a release
===============
Doing a release of ``lemur`` requires a few steps.
Bumping the version number
--------------------------
The next step in doing a release is bumping the version number in the
software.
* Update the version number in ``lemur/__about__.py``.
* Set the release date in the :doc:`/changelog`.
* Do a commit indicating this.
* Send a pull request with this.
* Wait for it to be merged.
Performing the release
----------------------
The commit that merged the version number bump is now the official release
commit for this release. You will need to have ``gpg`` installed and a ``gpg``
key in order to do a release. Once this has happened:
* Run ``invoke release {version}``.
The release should now be available on PyPI and a tag should be available in
the repository.
Verifying the release
---------------------
You should verify that ``pip install lemur`` works correctly:
.. code-block:: pycon
>>> import lemur
>>> lemur.__version__
'...'
Verify that this is the version you just released.
Post-release tasks
------------------
* Update the version number to the next major (e.g. ``0.5.dev1``) in
``lemur/__about__.py`` and
* Add new :doc:`/changelog` entry with next version and note that it is under
active development
* Send a pull request with these items
* Check for any outstanding code undergoing a deprecation cycle by looking in
``lemur.utils`` for ``DeprecatedIn**`` definitions. If any exist open
a ticket to increment them for the next release.

View File

@ -4,8 +4,8 @@ Frequently Asked Questions
Common Problems
---------------
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
:doc:`administration/index` for more information.
@ -14,6 +14,27 @@ I am seeing Lemur's javascript load in my browser but not the CSS.
:doc:`production/index` for example configurations.
After installing Lemur I am unable to login
Ensure that you are trying to login with the credentials you entered during `lemur init`. These are separate
from the postgres database credentials.
Running 'lemur db upgrade' seems stuck.
Most likely, the upgrade is stuck because an existing query on the database is holding onto a lock that the
migration needs.
To resolve, login to your lemur database and run:
SELECT * FROM pg_locks l INNER JOIN pg_stat_activity s ON (l.pid = s.pid) WHERE waiting AND NOT granted;
This will give you a list of queries that are currently waiting to be executed. From there attempt to idenity the PID
of the query blocking the migration. Once found execute:
select pg_terminate_backend(<blocking-pid>);
See `<http://stackoverflow.com/questions/22896496/alembic-migration-stuck-with-postgresql>`_ for more.
How do I
--------

View File

@ -1,261 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
sphinx-autopackage-script
This script parses a directory tree looking for python modules and packages and
creates ReST files appropriately to create code documentation with Sphinx.
It also creates a modules index (named modules.<suffix>).
"""
# Copyright 2008 Société des arts technologiques (SAT), http://www.sat.qc.ca/
# Copyright 2010 Thomas Waldmann <tw AT waldmann-edv DOT de>
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import optparse
# automodule options
OPTIONS = ['members',
'undoc-members',
# 'inherited-members', # disabled because there's a bug in sphinx
'show-inheritance',
]
INIT = '__init__.py'
def makename(package, module):
"""Join package and module with a dot."""
# Both package and module can be None/empty.
if package:
name = package
if module:
name += '.' + module
else:
name = module
return name
def write_file(name, text, opts):
"""Write the output file for module/package <name>."""
if opts.dryrun:
return
fname = os.path.join(opts.destdir, "%s.%s" % (name, opts.suffix))
if not opts.force and os.path.isfile(fname):
print 'File %s already exists, skipping.' % fname
else:
print 'Creating file %s.' % fname
f = open(fname, 'w')
f.write(text)
f.close()
def format_heading(level, text):
"""Create a heading of <level> [1, 2 or 3 supported]."""
underlining = ['=', '-', '~', ][level-1] * len(text)
return '%s\n%s\n\n' % (text, underlining)
def format_directive(module, package=None):
"""Create the automodule directive and add the options."""
directive = '.. automodule:: %s\n' % makename(package, module)
for option in OPTIONS:
directive += ' :%s:\n' % option
return directive
def create_module_file(package, module, opts):
"""Build the text of the file and write the file."""
text = format_heading(1, '%s Module' % module)
text += format_heading(2, ':mod:`%s` Module' % module)
text += format_directive(module, package)
write_file(makename(package, module), text, opts)
def create_package_file(root, master_package, subroot, py_files, opts, subs):
"""Build the text of the file and write the file."""
package = os.path.split(root)[-1]
text = format_heading(1, '%s Package' % package)
# add each package's module
for py_file in py_files:
if shall_skip(os.path.join(root, py_file)):
continue
is_package = py_file == INIT
py_file = os.path.splitext(py_file)[0]
py_path = makename(subroot, py_file)
if is_package:
heading = ':mod:`%s` Package' % package
else:
heading = ':mod:`%s` Module' % py_file
text += format_heading(2, heading)
text += format_directive(is_package and subroot or py_path, master_package)
text += '\n'
# build a list of directories that are packages (they contain an INIT file)
subs = [sub for sub in subs if os.path.isfile(os.path.join(root, sub, INIT))]
# if there are some package directories, add a TOC for theses subpackages
if subs:
text += format_heading(2, 'Subpackages')
text += '.. toctree::\n\n'
for sub in subs:
text += ' %s.%s\n' % (makename(master_package, subroot), sub)
text += '\n'
write_file(makename(master_package, subroot), text, opts)
def create_modules_toc_file(master_package, modules, opts, name='modules'):
"""
Create the module's index.
"""
text = format_heading(1, '%s Modules' % opts.header)
text += '.. toctree::\n'
text += ' :maxdepth: %s\n\n' % opts.maxdepth
modules.sort()
prev_module = ''
for module in modules:
# look if the module is a subpackage and, if yes, ignore it
if module.startswith(prev_module + '.'):
continue
prev_module = module
text += ' %s\n' % module
write_file(name, text, opts)
def shall_skip(module):
"""
Check if we want to skip this module.
"""
# skip it, if there is nothing (or just \n or \r\n) in the file
return os.path.getsize(module) < 3
def recurse_tree(path, excludes, opts):
"""
Look for every file in the directory tree and create the corresponding
ReST files.
"""
# use absolute path for root, as relative paths like '../../foo' cause
# 'if "/." in root ...' to filter out *all* modules otherwise
path = os.path.abspath(path)
# check if the base directory is a package and get is name
if INIT in os.listdir(path):
package_name = path.split(os.path.sep)[-1]
else:
package_name = None
toc = []
tree = os.walk(path, False)
for root, subs, files in tree:
# keep only the Python script files
py_files = sorted([f for f in files if os.path.splitext(f)[1] == '.py'])
if INIT in py_files:
py_files.remove(INIT)
py_files.insert(0, INIT)
# remove hidden ('.') and private ('_') directories
subs = sorted([sub for sub in subs if sub[0] not in ['.', '_']])
# check if there are valid files to process
# TODO: could add check for windows hidden files
if "/." in root or "/_" in root \
or not py_files \
or is_excluded(root, excludes):
continue
if INIT in py_files:
# we are in package ...
if (# ... with subpackage(s)
subs
or
# ... with some module(s)
len(py_files) > 1
or
# ... with a not-to-be-skipped INIT file
not shall_skip(os.path.join(root, INIT))
):
subroot = root[len(path):].lstrip(os.path.sep).replace(os.path.sep, '.')
create_package_file(root, package_name, subroot, py_files, opts, subs)
toc.append(makename(package_name, subroot))
elif root == path:
# if we are at the root level, we don't require it to be a package
for py_file in py_files:
if not shall_skip(os.path.join(path, py_file)):
module = os.path.splitext(py_file)[0]
create_module_file(package_name, module, opts)
toc.append(makename(package_name, module))
# create the module's index
if not opts.notoc:
create_modules_toc_file(package_name, toc, opts)
def normalize_excludes(rootpath, excludes):
"""
Normalize the excluded directory list:
* must be either an absolute path or start with rootpath,
* otherwise it is joined with rootpath
* with trailing slash
"""
sep = os.path.sep
f_excludes = []
for exclude in excludes:
if not os.path.isabs(exclude) and not exclude.startswith(rootpath):
exclude = os.path.join(rootpath, exclude)
if not exclude.endswith(sep):
exclude += sep
f_excludes.append(exclude)
return f_excludes
def is_excluded(root, excludes):
"""
Check if the directory is in the exclude list.
Note: by having trailing slashes, we avoid common prefix issues, like
e.g. an exlude "foo" also accidentally excluding "foobar".
"""
sep = os.path.sep
if not root.endswith(sep):
root += sep
for exclude in excludes:
if root.startswith(exclude):
return True
return False
def main():
"""
Parse and check the command line arguments.
"""
parser = optparse.OptionParser(usage="""usage: %prog [options] <package path> [exclude paths, ...]
Note: By default this script will not overwrite already created files.""")
parser.add_option("-n", "--doc-header", action="store", dest="header", help="Documentation Header (default=Project)", default="Project")
parser.add_option("-d", "--dest-dir", action="store", dest="destdir", help="Output destination directory", default="")
parser.add_option("-s", "--suffix", action="store", dest="suffix", help="module suffix (default=txt)", default="txt")
parser.add_option("-m", "--maxdepth", action="store", dest="maxdepth", help="Maximum depth of submodules to show in the TOC (default=4)", type="int", default=4)
parser.add_option("-r", "--dry-run", action="store_true", dest="dryrun", help="Run the script without creating the files")
parser.add_option("-f", "--force", action="store_true", dest="force", help="Overwrite all the files")
parser.add_option("-t", "--no-toc", action="store_true", dest="notoc", help="Don't create the table of content file")
(opts, args) = parser.parse_args()
if not args:
parser.error("package path is required.")
else:
rootpath, excludes = args[0], args[1:]
if os.path.isdir(rootpath):
# check if the output destination is a valid directory
if opts.destdir and os.path.isdir(opts.destdir):
excludes = normalize_excludes(rootpath, excludes)
recurse_tree(rootpath, excludes, opts)
else:
print '%s is not a valid output destination directory.' % opts.destdir
else:
print '%s is not a valid directory.' % rootpath
if __name__ == '__main__':
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
docs/guide/create.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/guide/create_role.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
docs/guide/create_user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,14 +1,103 @@
Creating Users
==============
User Guide
==========
These guides are quick tutorials on how to perform basic tasks in Lemur.
Creating Roles
==============
Create a New Authority
~~~~~~~~~~~~~~~~~~~~~~
Before Lemur can issue certificates you must configure the authority you wish use. Lemur itself does
not issue certificates, it relies on external CAs and the plugins associated with those CAs to create the certificate
that Lemur can then manage.
Creating Authorities
====================
.. figure:: create.png
In the authority table select "Create"
.. figure:: create_authority.png
Enter a authority name and short description about the authority. Enter an owner,
and certificate common name. Depending on the authority and the authority/issuer plugin
these values may or may not be used.
.. figure:: create_authority_options.png
Again how many of these values get used largely depends on the underlying plugin. It
is important to make sure you select the right plugin that you wish to use.
Create a New Certificate
~~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: create.png
In the certificate table select "Create"
.. figure:: create_certificate.png
Enter an owner, short description and the authority you wish to issue this certificate.
Enter a common name into the certificate, if no validity range is selected two years is
the default.
You can add notification options and upload the created certificate to a destination, both
of these are editable features and can be changed after the certificate has been created.
.. figure:: certificate_extensions.png
These options are typically for advanced users, the one exception is the `Subject Alternate Names` or SAN.
For certificates that need to include more than one domains, the first domain is the Common Name and all
other domains are added here as DNSName entries.
Import an Existing Certificate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. figure:: upload_certificate.png
Enter a owner, short description and public certificate. If there are intermediates and private keys
Lemur will track them just as it does if the certificate were created through Lemur. Lemur generates
a certificate name but you can override that by passing a value to the `Custom Name` field.
You can add notification options and upload the created certificate to a destination, both
of these are editable features and can be changed after the certificate has been created.
Create a New User
~~~~~~~~~~~~~~~~~
.. figure:: settings.png
From the settings dropdown select "Users"
.. figure:: create.png
In the user table select "Create"
.. figure:: create_user.png
Enter the username, email and password for the user. You can also assign any
roles that the user will need when they login. While there is no deletion
(we want to track creators forever) you can mark a user as 'Inactive' that will
not allow them to login to Lemur.
Create a New Role
~~~~~~~~~~~~~~~~~
.. figure:: settings.png
From the settings dropdown select "Roles"
.. figure:: create.png
In the role table select "Create"
.. figure:: create_role.png
Enter a role name and short description about the role. You can optionally store
a user/password on the role. This is useful if your authority require specific roles.
You can then accurately map those roles onto Lemur users. Also optional you can assign
users to your new role.
Creating Certificates
=====================

BIN
docs/guide/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -1,8 +1,8 @@
Lemur
=====
Lemur is a SSL management service. It attempts to help track and create certificates. By removing common issues with
CSR creation it gives normal developers 'sane' SSL defaults and helps security teams push SSL usage throughout an organization.
Lemur is a TLS management service. It attempts to help track and create certificates. By removing common issues with
CSR creation it gives normal developers 'sane' TLS defaults and helps security teams push TLS usage throughout an organization.
Installation
------------
@ -27,8 +27,7 @@ Administration
.. toctree::
:maxdepth: 2
administration/index
plugins/index
administration
Developers
----------
@ -38,14 +37,21 @@ Developers
developer/index
REST API
Security
--------
.. toctree::
:maxdepth: 2
developer/rest
security
Doing a Release
---------------
.. toctree::
:maxdepth: 1
doing-a-release
FAQ
----

View File

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

View File

@ -6,21 +6,22 @@ There are several steps needed to make Lemur production ready. Here we focus on
Basics
======
Because of the sensitivity of the information stored and maintain by Lemur it is important that you follow standard host hardening practices:
Because of the sensitivity of the information stored and maintained by Lemur it is important that you follow standard host hardening practices:
- Run Lemur with a limited user
- Disabled any unneeded service
- Disabled any unneeded services
- Enable remote logging
- Restrict access to host
.. _CredentialManagement:
Credential Management
---------------------
Lemur often contains credentials such as mutual SSL keys that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
Lemur often contains credentials such as mutual TLS keys or API tokens that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
to automatically encrypt these keys such that your keys not be in clear text.
The keys are located within lemur/keys and broken down by environment
The keys are located within lemur/keys and broken down by environment.
To utilize this ability use the following commands:
@ -30,7 +31,7 @@ and
``lemur unlock``
If you choose to use this feature ensure that the KEY are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
If you choose to use this feature ensure that the keys are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
Entropy
-------
@ -56,8 +57,8 @@ For additional information about OpenSSL entropy issues:
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
SSL
====
TLS/SSL
=======
Nginx
-----
@ -71,7 +72,7 @@ Nginx is a very popular choice to serve a Python project:
Nginx doesn't run any Python process, it only serves requests from outside to
the Python server.
Therefor there are two steps:
Therefore there are two steps:
- Run the Python process.
- Run Nginx.
@ -89,7 +90,7 @@ You must create a Nginx configuration file for Lemur. On GNU/Linux, they usually
go into /etc/nginx/conf.d/. Name it lemur.conf.
`proxy_pass` just passes the external request to the Python process.
The port much match the one used by the 0bin process of course.
The port must match the one used by the Lemur process of course.
You can make some adjustments to get a better user experience::
@ -109,7 +110,7 @@ You can make some adjustments to get a better user experience::
error_log /var/log/nginx/log/lemur.error.log;
location /api {
proxy_pass http://127.0.0.1:5000;
proxy_pass http://127.0.0.1:8000;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off;
proxy_buffering off;
@ -127,10 +128,10 @@ You can make some adjustments to get a better user experience::
}
This makes Nginx serve the favicon and static files which is is much better at than python.
This makes Nginx serve the favicon and static files which it is much better at than python.
It is highly recommended that you deploy SSL when deploying Lemur. This may be obvious given Lemur's purpose but the
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates SSL::
It is highly recommended that you deploy TLS when deploying Lemur. This may be obvious given Lemur's purpose but the
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates TLS::
server_tokens off;
add_header X-Frame-Options DENY;
@ -175,7 +176,7 @@ sensitive nature of Lemur and what it controls makes this essential. This is a s
resolver <IP DNS resolver>;
location /api {
proxy_pass http://127.0.0.1:5000;
proxy_pass http://127.0.0.1:8000;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off;
proxy_buffering off;
@ -218,7 +219,7 @@ An example apache config::
...
</VirtualHost>
Also included in the configurations above are several best practices when it comes to deploying SSL. Things like enabling
Also included in the configurations above are several best practices when it comes to deploying TLS. Things like enabling
HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment.
.. note::
@ -256,13 +257,12 @@ Create a configuration file named supervisor.ini::
nodaemon=false
minfds=1024
minprocs=200
user=lemur
[program:lemur]
command=python /path/to/lemur/manage.py manage.py start
directory=/path/to/lemur/
environment=PYTHONPATH='/path/to/lemur/'
environment=PYTHONPATH='/path/to/lemur/',LEMUR_CONF='/home/lemur/.lemur/lemur.conf.py'
user=lemur
autostart=true
autorestart=true
@ -270,7 +270,7 @@ Create a configuration file named supervisor.ini::
The 4 first entries are just boiler plate to get you started, you can copy
them verbatim.
The last one define one (you can have many) process supervisor should manage.
The last one defines one (you can have many) process supervisor should manage.
It means it will run the command::
@ -292,6 +292,28 @@ Then you can manage the process by running::
supervisorctl -c /path/to/supervisor.ini
It will start a shell from were you can start/stop/restart the service
It will start a shell from which you can start/stop/restart the service.
You can read all errors that might occurs from /tmp/lemur.log.
You can read all errors that might occur from /tmp/lemur.log.
Periodic Tasks
==============
Lemur contains a few tasks that are run and scheduled basis, currently the recommend way to run these tasks is to create
a cron job that runs the commands.
There are currently three commands that could/should be run on a periodic basis:
- `notify`
- `check_revoked`
- `sync`
How often you run these commands is largely up to the user. `notify` and `check_revoked` are typically run at least once a day.
`sync` is typically run every 15 minutes.
Example cron entries::
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur notify
*/15 * * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur sync -s all
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur check_revoked

View File

@ -1,95 +1,93 @@
Quickstart
**********
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service.
This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service. This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
Pressed for time? See the Lemur docker file on `Github <https://github.com/Netflix/lemur-docker>`_.
Dependencies
------------
Some basic prerequisites which you'll need in order to run Lemur:
* A UNIX-based operating system. We test on Ubuntu, develop on OS X
* A UNIX-based operating system (we test on Ubuntu, develop on OS X)
* Python 2.7
* PostgreSQL
* Ngnix
* Nginx
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and TLS (ELB), are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be be as generic as possible and are not intended to document every step of launching Lemur into a given environment.
Installing Build Dependencies
-----------------------------
If installing Lemur on a bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's dependencies:
.. code-block:: bash
$ sudo apt-get update
$ sudo apt-get install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql
.. note:: PostgreSQL is only required if your database is going to be on the same host as the webserver. npm is needed if you're installing Lemur from the source (e.g., from git).
Now, install Python ``virtualenv`` package:
.. code-block:: bash
$ sudo pip install -U virtualenv
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and SSL (ELB),
are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be
be as generic as possible and are not intended to document every step of launching Lemur into a given environment.
Setting up an Environment
-------------------------
The first thing you'll need is the Python ``virtualenv`` package. You probably already
have this, but if not, you can install it with::
pip install -U virtualenv
Once that's done, choose a location for the environment, and create it with the ``virtualenv``
command. For our guide, we're going to choose ``/www/lemur/``::
virtualenv /www/lemur/
Finally, activate your virtualenv::
source /www/lemur/bin/activate
.. note:: Activating the environment adjusts your PATH, so that things like pip now
install into the virtualenv by default.
Installing build dependencies
-----------------------------
If installing Lemur on truely bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's
dependencies::
$ sudo apt-get update
$ sudo apt-get install nodejs-legacy python-pip libpq-dev python-dev build-essential libssl-dev libffi-dev nginx git supervisor
And optionally if your database is going to be on the same host as the webserver::
$ sudo apt-get install postgres
Installing Lemur
----------------
Once you've got the environment setup, you can install Lemur and all its dependencies with
the same command you used to grab virtualenv::
pip install -U lemur
Once everything is installed, you should be able to execute the Lemur CLI, via ``lemur``, and get something
like the following:
In this guide, Lemur will be installed in ``/www``, so you need to create that structure first:
.. code-block:: bash
$ lemur
usage: lemur [--config=/path/to/settings.py] [command] [options]
$ sudo mkdir /www
$ cd /www
Clone Lemur inside the just created directory and give yourself write permission (we assume ``lemur`` is the user):
.. code-block:: bash
$ sudo git clone https://github.com/Netflix/lemur
$ sudo chown -R lemur lemur/
Create the virtual environment, activate it and enter the Lemur's directory:
.. code-block:: bash
$ virtualenv lemur
$ source /www/lemur/bin/activate
$ cd lemur
.. note:: Activating the environment adjusts your PATH, so that things like pip now install into the virtualenv by default.
Installing from Source
~~~~~~~~~~~~~~~~~~~~~~
If you're installing the Lemur source (e.g. from git), you'll also need to install **npm**.
Once your system is prepared, ensure that you are in the virtualenv:
Once your system is prepared, symlink your source into the virtualenv:
.. code-block:: bash
$ which python
And then run:
.. code-block:: bash
$ make develop
.. Note:: This command will install npm dependencies as well as compile static assets.
.. note:: This command will install npm dependencies as well as compile static assets.
Creating a configuration
------------------------
Before we run Lemur we must create a valid configuration file for it.
The Lemur cli comes with a simple command to get you up and running quickly.
Before we run Lemur, we must create a valid configuration file for it. The Lemur command line interface comes with a simple command to get you up and running quickly.
Simply run:
@ -97,89 +95,82 @@ Simply run:
$ lemur create_config
.. Note:: This command will create a default configuration under `~/.lemur/lemur.conf.py` you
can specify this location by passing the `config_path` parameter to the `create_config` command.
.. note:: This command will create a default configuration under ``~/.lemur/lemur.conf.py`` you can specify this location by passing the ``config_path`` parameter to the ``create_config`` command.
You can specify ``-c`` or ``--config`` to any Lemur command to specify the current environment you are working in. Lemur will also look under the environmental variable ``LEMUR_CONF`` should that be easier to setup in your environment.
You can specify `-c` or `--config` to any Lemur command to specify the current environment
you are working in. Lemur will also look under the environmental variable `LEMUR_CONF` should
that be easier to setup in your environment.
Update your configuration
-------------------------
Once created you will need to update the configuration file with information about your environment,
such as which database to talk to, where keys are stores etc..
Once created, you will need to update the configuration file with information about your environment, such as which database to talk to, where keys are stored etc.
.. note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
``postgresql://userame:password@<database-fqdn>:<database-port>/<database-name>``
.. Note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
postgresql://userame:password@databasefqdn:databaseport/databasename
Setup Postgres
--------------
For production a dedicated database is recommended, for this guide we will assume postgres has been installed and is on
the same machine that Lemur is installed on.
For production, a dedicated database is recommended, for this guide we will assume postgres has been installed and is on the same machine that Lemur is installed on.
First, set a password for the postgres user. For this guide, we will use **lemur** as an example but you should use the database password generated for by Lemur::
First, set a password for the postgres user. For this guide, we will use ``lemur`` as an example but you should use the database password generated by Lemur:
$ sudo -u postgres psql postgres
# \password postgres
Enter new password: lemur
Enter it again: lemur
.. code-block:: bash
Type CTRL-D to exit psql once you have changed the password.
$ sudo -u postgres -i
# \password postgres
Enter new password: lemur
Enter it again: lemur
Next, we will create our a new database::
Once successful, type CTRL-D to exit the Postgres shell.
$ sudo -u postgres createdb lemur
Next, we will create our new database:
.. code-block:: bash
$ sudo -u postgres createdb lemur
.. _InitializingLemur:
.. note::
For this guide we assume you will use the `postgres` user to connect to your database, when deploying to a VM or container this is often all you will need. If you have a shared database it is recommend you give Lemur it's own user.
Initializing Lemur
------------------
Lemur provides a helpful command that will initialize your database for you. It creates a default user (lemur) that is
used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when
Lemur has discovered certificates from a third party source. This is also a default user that can be used to
administer Lemur.
Lemur provides a helpful command that will initialize your database for you. It creates a default user (``lemur``) that is used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when Lemur has discovered certificates from a third party source. This is also a default user that can be used to administer Lemur.
In addition to create a new User, Lemur also creates a few default email notifications. These notifications are based
on a few configuration options such as `LEMUR_SECURITY_TEAM_EMAIL` they basically garentee that every cerificate within
Lemur will send one expiration notification to the security team.
In addition to creating a new user, Lemur also creates a few default email notifications. These notifications are based on a few configuration options such as ``LEMUR_SECURITY_TEAM_EMAIL``. They basically guarantee that every certificate within Lemur will send one expiration notification to the security team.
Additional notifications can be created through the UI or API.
See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
Additional notifications can be created through the UI or API. See :ref:`Creating Notifications <CreatingNotifications>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
**Make note of the password used as this will be used during first login to the Lemur UI**
.. code-block:: bash
$ lemur db init
**Make note of the password used as this will be used during first login to the Lemur UI.**
.. code-block:: bash
$ cd /www/lemur/lemur
$ lemur init
.. note:: It is recommended that once the 'lemur' user is created that you create individual users for every day access.
There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account
for them or be enrolled automatically through SSO. This can be done through the CLI or UI.
See :ref:`Creating Users <CreatingUsers>` and :ref:`Command Line Interface <CommandLineInterface>` for details
.. note:: It is recommended that once the ``lemur`` user is created that you create individual users for every day access. There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account for them or be enrolled automatically through SSO. This can be done through the CLI or UI. See :ref:`Creating Users <CreatingUsers>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
Setup a Reverse Proxy
---------------------
By default, Lemur runs on port 5000. Even if you change this, under normal conditions you won't be able to bind to
port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we recommend
you setup a simple web proxy.
By default, Lemur runs on port 8000. Even if you change this, under normal conditions you won't be able to bind to port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we need setup a simple web proxy. There are many different web servers you can use for this, we like and recommend Nginx.
Proxying with Nginx
~~~~~~~~~~~~~~~~~~~
You'll use the builtin HttpProxyModule within Nginx to handle proxying
You'll use the builtin ``HttpProxyModule`` within Nginx to handle proxying. Edit the ``/etc/nginx/sites-available/default`` file according to the lines below
::
location /api {
proxy_pass http://127.0.0.1:5000;
proxy_pass http://127.0.0.1:8000;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off;
proxy_buffering off;
@ -194,16 +185,22 @@ You'll use the builtin HttpProxyModule within Nginx to handle proxying
index index.html;
}
See :doc:`../production/index` for more details on using Nginx.
.. note:: See :doc:`../production/index` for more details on using Nginx.
After making these changes, restart Nginx service to apply them:
.. code-block:: bash
$ sudo service nginx restart
Starting the Web Service
------------------------
Lemur provides a built-in webserver (powered by gunicorn and eventlet) to get you off the ground quickly.
Lemur provides a built-in web server (powered by gunicorn and eventlet) to get you off the ground quickly.
To start the webserver, you simply use ``lemur start``. If you opted to use an alternative configuration path
you can pass that via the --config option.
To start the web server, you simply use ``lemur start``. If you opted to use an alternative configuration path
you can pass that via the ``--config`` option.
.. note::
You can login with the default user created during :ref:`Initializing Lemur <InitializingLemur>` or any other
@ -211,23 +208,23 @@ you can pass that via the --config option.
::
# Lemur's server runs on port 5000 by default. Make sure your client reflects
# Lemur's server runs on port 8000 by default. Make sure your client reflects
# the correct host and port!
lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:5000
lemur --config=/etc/lemur.conf.py start -b 127.0.0.1:8000
You should now be able to test the web service by visiting ``http://localhost:8000/``.
You should now be able to test the web service by visiting `http://localhost:5000/`.
Running Lemur as a Service
---------------------------
--------------------------
We recommend using whatever software you are most familiar with for managing Lemur processes. One option is `Supervisor <http://supervisord.org/>`_.
We recommend using whatever software you are most familiar with for managing Lemur processes. One option is
`Supervisor <http://supervisord.org/>`_.
Configure ``supervisord``
~~~~~~~~~~~~~~~~~~~~~~~~~
Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's bin/
folder and you're good to go.
Configuring Supervisor couldn't be more simple. Just point it to the ``lemur`` executable in your virtualenv's ``bin/`` folder and you're good to go.
::
@ -242,42 +239,43 @@ folder and you're good to go.
See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervisor.
Syncing
-------
Lemur uses periodic sync tasks to make sure it is up-to-date with it's environment. As always things can change outside
of Lemur, but we do our best to reconcile those changes.
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. Things change outside of Lemur we do our best to reconcile those changes. The recommended method is to use CRON:
.. code-block:: bash
$ crontab -e
* 3 * * * lemur sync --all
* 3 * * * lemur check_revoked
*/15 * * * * lemur sync -s all
0 22 * * * lemur check_revoked
0 22 * * * lemur notify
Additional Utilities
--------------------
If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The
``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the
power and flexibility that goes with it.
If you're familiar with Python you'll quickly find yourself at home, and even more so if you've used Flask. The ``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the power and flexibility that goes with it.
Some of the features which you'll likely find useful are listed below.
Some of those which you'll likely find useful are:
lock
~~~~
Encrypts sensitive key material - This is most useful for storing encrypted secrets in source code.
Encrypts sensitive key material - this is most useful for storing encrypted secrets in source code.
unlock
~~~~~~
Decrypts sensitive key material - Used to decrypt the secrets stored in source during deployment.
Decrypts sensitive key material - used to decrypt the secrets stored in source during deployment.
What's Next?
------------
The above gets you going, but for production there are several different security considerations to take into account,
remember Lemur is handling sensitive data and security is imperative.
Get familiar with how Lemur works by reviewing the :doc:`../guide/index`. When you're ready see :doc:`../production/index` for more details on how to configure Lemur for production.
See :doc:`../production/index` for more details on how to configure Lemur for production.
The above just gets you going, but for production there are several different security considerations to take into account. Remember, Lemur is handling sensitive data and security is imperative.

View File

@ -2,4 +2,28 @@ Jinja2>=2.3
Pygments>=1.2
Sphinx>=1.3
docutils>=0.7
markupsafe
markupsafe
sphinxcontrib-httpdomain
Flask==0.10.1
Flask-RESTful==0.3.3
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
Flask-Migrate==1.7.0
Flask-Bcrypt==0.7.1
Flask-Principal==0.4.0
Flask-Mail==0.9.1
SQLAlchemy-Utils==0.31.4
BeautifulSoup4
requests==2.9.1
psycopg2==2.6.1
arrow==0.7.0
boto==2.38.0 # we might make this optional
six==1.10.0
gunicorn==19.4.4
pycrypto==2.6.1
cryptography==1.1.2
pyopenssl==0.15.1
pyjwt==1.4.0
xmltodict==0.9.2
lockfile==0.12.2
future==0.15.2

66
docs/security.rst Normal file
View File

@ -0,0 +1,66 @@
Security
========
We take the security of ``lemur`` seriously. The following are a set of
policies we have adopted to ensure that security issues are addressed in a
timely fashion.
Reporting a security issue
--------------------------
We ask that you do not report security issues to our normal GitHub issue
tracker.
If you believe you've identified a security issue with ``lemur``, please
report it to ``cloudsecurity@netflix.com``.
Once you've submitted an issue via email, you should receive an acknowledgment
within 48 hours, and depending on the action to be taken, you may receive
further follow-up emails.
Supported Versions
------------------
At any given time, we will provide security support for the `master`_ branch
as well as the 2 most recent releases.
Disclosure Process
------------------
Our process for taking a security issue from private discussion to public
disclosure involves multiple steps.
Approximately one week before full public disclosure, we will send advance
notification of the issue to a list of people and organizations, primarily
composed of operating-system vendors and other distributors of
``lemur``. This notification will consist of an email message
containing:
* A full description of the issue and the affected versions of
``lemur``.
* The steps we will be taking to remedy the issue.
* The patches, if any, that will be applied to ``lemur``.
* The date on which the ``lemur`` team will apply these patches, issue
new releases, and publicly disclose the issue.
Simultaneously, the reporter of the issue will receive notification of the date
on which we plan to make the issue public.
On the day of disclosure, we will take the following steps:
* Apply the relevant patches to the ``lemur`` repository. The commit
messages for these patches will indicate that they are for security issues,
but will not describe the issue in any detail; instead, they will warn of
upcoming disclosure.
* Issue the relevant releases.
If a reported issue is believed to be particularly time-sensitive due to a
known exploit in the wild, for example the time between advance notification
and public disclosure may be shortened considerably.
The list of people and organizations who receives advanced notification of
security issues is not, and will not, be made public. This list generally
consists of high profile downstream distributors and is entirely at the
discretion of the ``lemur`` team.
.. _`master`: https://github.com/Netflix/lemur

View File

@ -48,8 +48,8 @@ gulp.task('test', function (done) {
gulp.task('dev:fonts', function () {
var fileList = [
'lemur/static/app/vendor/bower_components/bootstrap/dist/fonts/*',
'lemur/static/app/vendor/bower_components/fontawesome/fonts/*'
'bower_components/bootstrap/dist/fonts/*',
'bower_components/fontawesome/fonts/*'
];
return gulp.src(fileList)
@ -57,7 +57,7 @@ gulp.task('dev:fonts', function () {
});
gulp.task('dev:styles', function () {
var baseContent = '@import "lemur/static/app/vendor/bower_components/bootstrap/less/bootstrap.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/variables.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/bootswatch.less";@import "lemur/static/app/vendor/bower_components/bootstrap/less/utilities.less";';
var baseContent = '@import "bower_components/bootstrap/less/bootstrap.less";@import "bower_components/bootswatch/$theme$/variables.less";@import "bower_components/bootswatch/$theme$/bootswatch.less";@import "bower_components/bootstrap/less/utilities.less";';
var isBootswatchFile = function (file) {
var suffix = 'bootswatch.less';
@ -72,16 +72,17 @@ gulp.task('dev:styles', function () {
};
var fileList = [
'lemur/static/app/styles/lemur.css',
'lemur/static/app/vendor/bower_components/bootswatch/sandstone/bootswatch.less',
'lemur/static/app/vendor/bower_components/fontawesome/css/font-awesome.css',
'lemur/static/app/vendor/bower_components/angular-spinkit/src/angular-spinkit.css',
'lemur/static/app/vendor/bower_components/angular-chart.js/dist/angular-chart.css',
'lemur/static/app/vendor/bower_components/angular-loading-bar/src/loading-bar.css',
'lemur/static/app/vendor/bower_components/angular-ui-switch/angular-ui-switch.css',
'lemur/static/app/vendor/bower_components/angular-wizard/dist/angular-wizard.css',
'lemur/static/app/vendor/bower_components/ng-table/ng-table.css',
'lemur/static/app/vendor/bower_components/angularjs-toaster/toaster.css'
'bower_components/bootswatch/sandstone/bootswatch.less',
'bower_components/fontawesome/css/font-awesome.css',
'bower_components/angular-spinkit/src/angular-spinkit.css',
'bower_components/angular-chart.js/dist/angular-chart.css',
'bower_components/angular-loading-bar/src/loading-bar.css',
'bower_components/angular-ui-switch/angular-ui-switch.css',
'bower_components/angular-wizard/dist/angular-wizard.css',
'bower_components/ng-table/ng-table.css',
'bower_components/angularjs-toaster/toaster.css',
'bower_components/angular-ui-select/dist/select.css',
'lemur/static/app/styles/lemur.css'
];
return gulp.src(fileList)
@ -238,8 +239,8 @@ gulp.task('build:images', function () {
gulp.task('package:strip', function () {
return gulp.src(['lemur/static/dist/scripts/main*'])
.pipe(replace('http:\/\/localhost:5000', ''))
.pipe(replace('http:\/\/localhost:3000', ''))
.pipe(replace('http:\/\/localhost:8000', ''))
.pipe(useref())
.pipe(revReplace())
.pipe(gulp.dest('lemur/static/dist/scripts'))

View File

@ -27,7 +27,10 @@ function browserSyncInit(baseDir, files, browser) {
browserSync.instance = browserSync.init(files, {
startPath: '/index.html',
server: {
baseDir: baseDir
baseDir: baseDir,
routes: {
'/bower_components': './bower_components'
}
},
browser: browser,
ghostMode: false

18
lemur/__about__.py Normal file
View File

@ -0,0 +1,18 @@
from __future__ import absolute_import, division, print_function
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
]
__title__ = "lemur"
__summary__ = ("Certificate management and orchestration service")
__uri__ = "https://github.com/Netflix/lemur"
__version__ = "0.3.0"
__author__ = "The Lemur developers"
__email__ = "security@netflix.com"
__license__ = "Apache License, Version 2.0"
__copyright__ = "Copyright 2015 {0}".format(__author__)

View File

@ -8,7 +8,10 @@
"""
from __future__ import absolute_import, division, print_function
from lemur import factory
from lemur.extensions import metrics
from lemur.users.views import mod as users_bp
from lemur.roles.views import mod as roles_bp
@ -17,11 +20,21 @@ from lemur.domains.views import mod as domains_bp
from lemur.destinations.views import mod as destinations_bp
from lemur.authorities.views import mod as authorities_bp
from lemur.certificates.views import mod as certificates_bp
from lemur.status.views import mod as status_bp
from lemur.defaults.views import mod as defaults_bp
from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp
from lemur.__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__,
__uri__, __version__
)
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
]
LEMUR_BLUEPRINTS = (
users_bp,
@ -31,7 +44,7 @@ LEMUR_BLUEPRINTS = (
destinations_bp,
authorities_bp,
certificates_bp,
status_bp,
defaults_bp,
plugins_bp,
notifications_bp,
sources_bp
@ -54,12 +67,21 @@ def configure_hook(app):
from lemur.decorators import crossdomain
if app.config.get('CORS'):
@app.after_request
@crossdomain(origin="http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
def after(response):
return response
@app.errorhandler(500)
def internal_error(error):
metrics.send('500_status_code', 'counter', 1)
@app.errorhandler(400)
def response_error(error):
metrics.send('400_status_code', 'counter', 1)
@app.errorhandler(PermissionDenied)
def handle_invalid_usage(error):
def permission_denied_error(error):
metrics.send('403_status_code', 'counter', 1)
response = {'message': 'You are not allow to access this resource'}
response.status_code = 403
return response

View File

@ -16,24 +16,36 @@ operator_permission = Permission(RoleNeed('operator'))
admin_permission = Permission(RoleNeed('admin'))
CertificateCreator = namedtuple('certificate', ['method', 'value'])
CertificateCreatorNeed = partial(CertificateCreator, 'certificateView')
CertificateCreatorNeed = partial(CertificateCreator, 'key')
CertificateOwner = namedtuple('certificate', ['method', 'value'])
CertificateOwnerNeed = partial(CertificateOwner, 'certificateView')
CertificateOwnerNeed = partial(CertificateOwner, 'role')
class SensitiveDomainPermission(Permission):
def __init__(self):
super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
class ViewKeyPermission(Permission):
def __init__(self, role_id, certificate_id):
c_need = CertificateCreatorNeed(str(certificate_id))
o_need = CertificateOwnerNeed(str(role_id))
super(ViewKeyPermission, self).__init__(o_need, c_need, RoleNeed('admin'))
def __init__(self, certificate_id, owner):
c_need = CertificateCreatorNeed(certificate_id)
super(ViewKeyPermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
class UpdateCertificatePermission(Permission):
def __init__(self, role_id, certificate_id):
c_need = CertificateCreatorNeed(str(certificate_id))
o_need = CertificateOwnerNeed(str(role_id))
super(UpdateCertificatePermission, self).__init__(o_need, c_need, RoleNeed('admin'))
def __init__(self, certificate_id, owner):
c_need = CertificateCreatorNeed(certificate_id)
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
class CertificatePermission(Permission):
def __init__(self, certificate_id, roles):
needs = [RoleNeed('admin'), CertificateCreatorNeed(certificate_id)]
for r in roles:
needs.append(CertificateOwnerNeed(str(r)))
super(CertificatePermission, self).__init__(*needs)
RoleUser = namedtuple('role', ['method', 'value'])
@ -42,7 +54,7 @@ ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
class ViewRoleCredentialsPermission(Permission):
def __init__(self, role_id):
need = ViewRoleCredentialsNeed(str(role_id))
need = ViewRoleCredentialsNeed(role_id)
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin'))

View File

@ -8,11 +8,12 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from __future__ import unicode_literals
from builtins import bytes
import jwt
import json
import base64
import binascii
from builtins import str
from functools import wraps
from datetime import datetime, timedelta
@ -29,24 +30,21 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from lemur.users import service as user_service
from lemur.auth.permissions import CertificateOwnerNeed, CertificateCreatorNeed, \
from lemur.auth.permissions import CertificateCreatorNeed, \
AuthorityCreatorNeed, ViewRoleCredentialsNeed
def base64url_decode(data):
if isinstance(data, str):
data = str(data)
rem = len(data) % 4
if rem > 0:
data += b'=' * (4 - rem)
data += '=' * (4 - rem)
return base64.urlsafe_b64decode(data)
return base64.urlsafe_b64decode(bytes(data.encode('latin-1')))
def base64url_encode(data):
return base64.urlsafe_b64encode(data).replace(b'=', b'')
return base64.urlsafe_b64encode(data).replace('=', '')
def get_rsa_public_key(n, e):
@ -141,9 +139,11 @@ def fetch_token_header(token):
try:
return json.loads(base64url_decode(header_segment))
except TypeError:
except TypeError as e:
current_app.logger.exception(e)
raise jwt.DecodeError('Invalid header padding')
except binascii.Error:
except binascii.Error as e:
current_app.logger.exception(e)
raise jwt.DecodeError('Invalid header padding')
@ -165,8 +165,7 @@ def on_identity_loaded(sender, identity):
# identity with the roles that the user provides
if hasattr(user, 'roles'):
for role in user.roles:
identity.provides.add(CertificateOwnerNeed(role.id))
identity.provides.add(ViewRoleCredentialsNeed(role.id))
identity.provides.add(ViewRoleCredentialsNeed(role.name))
identity.provides.add(RoleNeed(role.name))
# apply ownership for authorities

View File

@ -9,11 +9,12 @@ import jwt
import base64
import requests
from flask import g, Blueprint, current_app
from flask import Blueprint, current_app
from flask.ext.restful import reqparse, Resource, Api
from flask.ext.principal import Identity, identity_changed
from lemur.extensions import metrics
from lemur.common.utils import get_psuedo_random_string
from lemur.users import service as user_service
@ -35,7 +36,7 @@ class Login(Resource):
Authorization:Bearer <token>
Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting
Tokens have a set expiration date. You can inspect the token expiration by base64 decoding the token and inspecting
it's contents.
.. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \
@ -96,13 +97,12 @@ class Login(Resource):
# Tell Flask-Principal the identity changed
identity_changed.send(current_app._get_current_object(),
identity=Identity(user.id))
metrics.send('successful_login', 'counter', 1)
return dict(token=create_token(user))
metrics.send('invalid_login', 'counter', 1)
return dict(message='The supplied credentials are invalid'), 401
def get(self):
return {'username': g.current_user.username, 'roles': [r.name for r in g.current_user.roles]}
class Ping(Resource):
"""
@ -125,7 +125,7 @@ class Ping(Resource):
args = self.reqparse.parse_args()
# take the information we have received from Meechum to create a new request
# take the information we have received from the provider to create a new request
params = {
'client_id': args['clientId'],
'grant_type': 'authorization_code',
@ -138,7 +138,7 @@ class Ping(Resource):
access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL')
user_api_url = current_app.config.get('PING_USER_API_URL')
# the secret and cliendId will be given to you when you signup for meechum
# the secret and cliendId will be given to you when you signup for the provider
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
headers = {'Authorization': 'Basic {0}'.format(basic)}
@ -179,14 +179,11 @@ class Ping(Resource):
profile = r.json()
user = user_service.get_by_email(profile['email'])
metrics.send('successful_login', 'counter', 1)
# update their google 'roles'
roles = []
# Legacy edge case - 'admin' has some special privileges associated with it
if 'secops@netflix.com' in profile['googleGroups']:
roles.append(role_service.get_by_name('admin'))
for group in profile['googleGroups']:
role = role_service.get_by_name(group)
if not role:
@ -196,10 +193,12 @@ class Ping(Resource):
# if we get an sso user create them an account
# we still pick a random password in case sso is down
if not user:
# every user is an operator (tied to the verisignCA)
v = role_service.get_by_name('verisign')
if v:
roles.append(v)
# every user is an operator (tied to a default role)
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
v = role_service.get_by_name(current_app.config.get('LEMUR_DEFAULT_ROLE'))
if v:
roles.append(v)
user = user_service.create(
profile['email'],
@ -222,7 +221,7 @@ class Ping(Resource):
profile['email'],
profile['email'],
True,
profile.get('thumbnailPhotoUrl'), # Encase profile isn't google+ enabled
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
roles
)
@ -232,5 +231,78 @@ class Ping(Resource):
return dict(token=create_token(user))
class Google(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Google, self).__init__()
def post(self):
access_token_url = 'https://accounts.google.com/o/oauth2/token'
people_api_url = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect'
self.reqparse.add_argument('clientId', type=str, required=True, location='json')
self.reqparse.add_argument('redirectUri', type=str, required=True, location='json')
self.reqparse.add_argument('code', type=str, required=True, location='json')
args = self.reqparse.parse_args()
# Step 1. Exchange authorization code for access token
payload = {
'client_id': args['clientId'],
'grant_type': 'authorization_code',
'redirect_uri': args['redirectUri'],
'code': args['code'],
'client_secret': current_app.config.get('GOOGLE_SECRET')
}
r = requests.post(access_token_url, data=payload)
token = r.json()
# Step 2. Retrieve information about the current user
headers = {'Authorization': 'Bearer {0}'.format(token['access_token'])}
r = requests.get(people_api_url, headers=headers)
profile = r.json()
user = user_service.get_by_email(profile['email'])
if user:
metrics.send('successful_login', 'counter', 1)
return dict(token=create_token(user))
class Providers(Resource):
def get(self):
active_providers = []
for provider in current_app.config.get("ACTIVE_PROVIDERS"):
provider = provider.lower()
if provider == "google":
active_providers.append({
'name': 'google',
'clientId': current_app.config.get("GOOGLE_CLIENT_ID"),
'url': api.url_for(Google)
})
elif provider == "ping":
active_providers.append({
'name': current_app.config.get("PING_NAME"),
'url': current_app.config.get('PING_REDIRECT_URI'),
'redirectUri': current_app.config.get("PING_REDIRECT_URI"),
'clientId': current_app.config.get("PING_CLIENT_ID"),
'responseType': 'code',
'scope': ['openid', 'email', 'profile', 'address'],
'scopeDelimiter': ' ',
'authorizationEndpoint': current_app.config.get("PING_AUTH_ENDPOINT"),
'requiredUrlParams': ['scope'],
'type': '2.0'
})
return active_providers
api.add_resource(Login, '/auth/login', endpoint='login')
api.add_resource(Ping, '/auth/ping', endpoint='ping')
api.add_resource(Google, '/auth/google', endpoint='google')
api.add_resource(Providers, '/auth/providers', endpoint='providers')

View File

@ -6,53 +6,35 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean
from sqlalchemy.dialects.postgresql import JSON
from lemur.database import db
from lemur.certificates.models import cert_get_cn, cert_get_not_after, cert_get_not_before
from lemur.models import roles_authorities
class Authority(db.Model):
__tablename__ = 'authorities'
id = Column(Integer, primary_key=True)
owner = Column(String(128))
owner = Column(String(128), nullable=False)
name = Column(String(128), unique=True)
body = Column(Text())
chain = Column(Text())
bits = Column(Integer())
cn = Column(String(128))
not_before = Column(DateTime)
not_after = Column(DateTime)
active = Column(Boolean, default=True)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
plugin_name = Column(String(64))
description = Column(Text)
options = Column(JSON)
roles = relationship('Role', backref=db.backref('authority'), lazy='dynamic')
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
roles = relationship('Role', secondary=roles_authorities, passive_deletes=True, backref=db.backref('authority'), lazy='dynamic')
user_id = Column(Integer, ForeignKey('users.id'))
certificates = relationship("Certificate", backref='authority')
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
def __init__(self, name, owner, plugin_name, body, roles=None, chain=None, description=None):
self.name = name
self.body = body
self.chain = chain
self.owner = owner
self.plugin_name = plugin_name
cert = x509.load_pem_x509_certificate(str(body), default_backend())
self.cn = cert_get_cn(cert)
self.not_before = cert_get_not_before(cert)
self.not_after = cert_get_not_after(cert)
self.roles = roles
self.description = description
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
def serialize(self):
blob = self.as_dict()
return blob
def __init__(self, **kwargs):
self.owner = kwargs['owner']
self.roles = kwargs.get('roles', [])
self.name = kwargs.get('name')
self.description = kwargs.get('description')
self.authority_certificate = kwargs['authority_certificate']
self.plugin_name = kwargs['plugin']['slug']

View File

@ -0,0 +1,112 @@
"""
.. module: lemur.authorities.schemas
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import current_app
from marshmallow import fields, validates_schema
from marshmallow import validate
from marshmallow.exceptions import ValidationError
from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema
from lemur.users.schemas import UserNestedOutputSchema
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.common import validators
class AuthorityInputSchema(LemurInputSchema):
name = fields.String(required=True)
owner = fields.Email(required=True)
description = fields.String()
common_name = fields.String(required=True, validate=validators.sensitive_domain)
validity_start = fields.Date()
validity_end = fields.Date()
validity_years = fields.Integer()
# certificate body fields
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
plugin = fields.Nested(PluginInputSchema)
# signing related options
type = fields.String(validate=validate.OneOf(['root', 'subca']), missing='root')
parent = fields.Nested(AssociatedAuthoritySchema)
signing_algorithm = fields.String(validate=validate.OneOf(['sha256WithRSA', 'sha1WithRSA']), missing='sha256WithRSA')
key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048')
key_name = fields.String()
sensitivity = fields.String(validate=validate.OneOf(['medium', 'high']), missing='medium')
serial_number = fields.Integer()
first_serial = fields.Integer(missing=1)
extensions = fields.Nested(ExtensionSchema)
roles = fields.Nested(AssociatedRoleSchema(many=True))
@validates_schema
def validate_dates(self, data):
validators.dates(data)
@validates_schema
def validate_subca(self, data):
if data['type'] == 'subca':
if not data.get('parent'):
raise ValidationError("If generating a subca parent 'authority' must be specified.")
class AuthorityUpdateSchema(LemurInputSchema):
owner = fields.Email(required=True)
description = fields.String()
active = fields.Boolean()
roles = fields.Nested(AssociatedRoleSchema(many=True))
class RootAuthorityCertificateOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
active = fields.Boolean()
bits = fields.Integer()
body = fields.String()
chain = fields.String()
description = fields.String()
name = fields.String()
cn = fields.String()
not_after = fields.DateTime()
not_before = fields.DateTime()
owner = fields.Email()
status = fields.Boolean()
user = fields.Nested(UserNestedOutputSchema)
class AuthorityOutputSchema(LemurOutputSchema):
id = fields.Integer()
description = fields.String()
name = fields.String()
owner = fields.Email()
plugin = fields.Nested(PluginOutputSchema)
active = fields.Boolean()
options = fields.Dict()
roles = fields.List(fields.Nested(AssociatedRoleSchema))
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
class AuthorityNestedOutputSchema(LemurOutputSchema):
id = fields.Integer()
description = fields.String()
name = fields.String()
owner = fields.Email()
plugin = fields.Nested(PluginOutputSchema)
active = fields.Boolean()
authority_update_schema = AuthorityUpdateSchema()
authority_input_schema = AuthorityInputSchema()
authority_output_schema = AuthorityOutputSchema()
authorities_output_schema = AuthorityOutputSchema(many=True)

View File

@ -9,69 +9,61 @@
"""
from flask import g
from flask import current_app
from lemur import database
from lemur.extensions import metrics
from lemur.authorities.models import Authority
from lemur.roles import service as role_service
from lemur.notifications import service as notification_service
from lemur.roles.models import Role
from lemur.certificates.models import Certificate
from lemur.plugins.base import plugins
from lemur.certificates.service import upload
def update(authority_id, active=None, roles=None):
def update(authority_id, description=None, owner=None, active=None, roles=None):
"""
Update a an authority with new values.
:param authority_id:
:param roles: roles that are allowed to use this authority
:rtype : Authority
:return:
"""
authority = get(authority_id)
if roles:
authority = database.update_list(authority, 'roles', Role, roles)
authority.roles = roles
if active:
authority.active = active
authority.description = description
authority.owner = owner
return database.update(authority)
def create(kwargs):
def mint(**kwargs):
"""
Create a new authority.
Creates the authority based on the plugin provided.
"""
issuer = kwargs['plugin']['plugin_object']
body, chain, roles = issuer.create_authority(kwargs)
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title)
return body, chain, roles
:rtype : Authority
def create_authority_roles(roles, owner, plugin_title):
"""
Creates all of the necessary authority roles.
:param roles:
:return:
"""
issuer = plugins.get(kwargs.get('pluginName'))
kwargs['creator'] = g.current_user.email
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
cert = Certificate(cert_body, chain=intermediate)
cert.owner = kwargs['ownerEmail']
cert.description = "This is the ROOT certificate for the {0} certificate authority".format(kwargs.get('caName'))
cert.user = g.current_user
cert.notifications = notification_service.create_default_expiration_notifications(
'DEFAULT_SECURITY',
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
)
# we create and attach any roles that the issuer gives us
role_objs = []
for r in issuer_roles:
role = role_service.create(
r['name'],
password=r['password'],
description="{0} auto generated role".format(kwargs.get('pluginName')),
username=r['username'])
for r in roles:
role = role_service.get_by_name(r['name'])
if not role:
role = role_service.create(
r['name'],
password=r['password'],
description="Auto generated role for {0}".format(plugin_title),
username=r['username'])
# the user creating the authority should be able to administer it
if role.username == 'admin':
@ -79,21 +71,51 @@ def create(kwargs):
role_objs.append(role)
authority = Authority(
kwargs.get('caName'),
kwargs['ownerEmail'],
kwargs['pluginName'],
cert_body,
description=kwargs['caDescription'],
chain=intermediate,
roles=role_objs
)
# create an role for the owner and assign it
owner_role = role_service.get_by_name(owner)
if not owner_role:
owner_role = role_service.create(
owner,
description="Auto generated role based on owner: {0}".format(owner)
)
database.update(cert)
role_objs.append(owner_role)
return role_objs
def create(**kwargs):
"""
Creates a new authority.
"""
kwargs['creator'] = g.user.email
body, chain, roles = mint(**kwargs)
kwargs['body'] = body
kwargs['chain'] = chain
if kwargs.get('roles'):
kwargs['roles'] += roles
else:
kwargs['roles'] = roles
if kwargs['type'] == 'subca':
description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
authority is {1}.".format(kwargs.get('name'), kwargs.get('parent'))
else:
description = "This is the ROOT certificate for the {0} certificate authority.".format(
kwargs.get('name')
)
kwargs['description'] = description
cert = upload(**kwargs)
kwargs['authority_certificate'] = cert
authority = Authority(**kwargs)
authority = database.create(authority)
g.user.authorities.append(authority)
g.current_user.authorities.append(authority)
metrics.send('authority_created', 'counter', 1, metric_tags=dict(owner=authority.owner))
return authority
@ -112,7 +134,6 @@ def get(authority_id):
"""
Retrieves an authority given it's ID
:rtype : Authority
:param authority_id:
:return:
"""
@ -124,7 +145,6 @@ def get_by_name(authority_name):
Retrieves an authority given it's name.
:param authority_name:
:rtype : Authority
:return:
"""
return database.get(Authority, authority_name, field='name')
@ -138,14 +158,9 @@ def get_authority_role(ca_name):
:param ca_name:
"""
if g.current_user.is_admin:
authority = get_by_name(ca_name)
# TODO we should pick admin ca roles for admin
return authority.roles[0]
return role_service.get_by_name("{0}_admin".format(ca_name))
else:
for role in g.current_user.roles:
if role.authority:
if role.authority.name == ca_name:
return role
return role_service.get_by_name("{0}_operator".format(ca_name))
def render(args):
@ -155,10 +170,6 @@ def render(args):
:return:
"""
query = database.session_query(Authority)
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
if filt:
@ -172,13 +183,8 @@ def render(args):
if not g.current_user.is_admin:
authority_ids = []
for role in g.current_user.roles:
if role.authority:
authority_ids.append(role.authority.id)
for authority in role.authorities:
authority_ids.append(authority.id)
query = query.filter(Authority.id.in_(authority_ids))
query = database.find_all(query, Authority, args)
if sort_by and sort_dir:
query = database.sort(query, Authority, sort_by, sort_dir)
return database.paginate(query, page, count)
return database.sort_and_page(query, Authority, args)

View File

@ -5,31 +5,19 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint, g
from flask.ext.restful import reqparse, fields, Api
from flask import Blueprint
from flask.ext.restful import reqparse, Api
from lemur.authorities import service
from lemur.roles import service as role_service
from lemur.certificates import service as certificate_service
from lemur.common.utils import paginated_parser
from lemur.common.schema import validate_schema
from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import AuthorityPermission
from lemur.common.utils import paginated_parser, marshal_items
from lemur.certificates import service as certificate_service
from lemur.authorities import service
from lemur.authorities.schemas import authority_input_schema, authority_output_schema, authorities_output_schema, authority_update_schema
FIELDS = {
'name': fields.String,
'description': fields.String,
'options': fields.Raw,
'pluginName': fields.String,
'body': fields.String,
'chain': fields.String,
'active': fields.Boolean,
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
'id': fields.Integer,
}
mod = Blueprint('authorities', __name__)
api = Api(mod)
@ -41,7 +29,7 @@ class AuthoritiesList(AuthenticatedResource):
self.reqparse = reqparse.RequestParser()
super(AuthoritiesList, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, authorities_output_schema)
def get(self):
"""
.. http:get:: /authorities
@ -65,28 +53,52 @@ class AuthoritiesList(AuthenticatedResource):
Content-Type: text/javascript
{
"items": [
{
"id": 1,
"name": "authority1",
"description": "this is authority1",
"pluginName": null,
"chain": "-----Begin ...",
"body": "-----Begin ...",
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39"
"options": null
}
]
"items": [{
"name": "TestAuthority",
"roles": [{
"id": 123,
"name": "secure@example.com"
}, {
"id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"options": null,
"active": true,
"authorityCertificate": {
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
"status": true,
"cn": "AcommonName",
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
"chain": "",
"notBefore": "2016-06-02T00:00:15+00:00",
"notAfter": "2023-06-02T23:59:15+00:00",
"owner": "secure@example.com",
"user": {
"username": "joe@example.com",
"active": true,
"email": "joe@example.com",
"id": 3
},
"active": true,
"bits": 2048,
"id": 2235,
"name": "TestAuthority"
},
"owner": "secure@example.com",
"id": 43,
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
}
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query page: int default is 1
:query filter: key value pair. format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
@ -97,8 +109,8 @@ class AuthoritiesList(AuthenticatedResource):
args = parser.parse_args()
return service.render(args)
@marshal_items(FIELDS)
def post(self):
@validate_schema(authority_input_schema, authority_output_schema)
def post(self, data=None):
"""
.. http:post:: /authorities
@ -112,31 +124,30 @@ class AuthoritiesList(AuthenticatedResource):
Host: example.com
Accept: application/json, text/javascript
{
"caDN": {
"country": "US",
"state": "CA",
"location": "A Location",
"organization": "ExampleInc",
"organizationalUnit": "Operations",
"commonName": "a common name"
},
"caType": "root",
"caSigningAlgo": "sha256WithRSA",
"caSensitivity": "medium",
{
"country": "US",
"state": "California",
"location": "Los Gatos",
"organization": "Netflix",
"organizationalUnit": "Operations",
"type": "root",
"signingAlgorithm": "sha256WithRSA",
"sensitivity": "medium",
"keyType": "RSA2048",
"pluginName": "cloudca",
"validityStart": "2015-06-11T07:00:00.000Z",
"validityEnd": "2015-06-13T07:00:00.000Z",
"caName": "DoctestCA",
"ownerEmail": "jimbob@example.com",
"caDescription": "Example CA",
"extensions": {
"subAltNames": {
"names": []
}
"plugin": {
"slug": "cloudca-issuer",
},
}
"name": "TimeTestAuthority5",
"owner": "secure@example.com",
"description": "test",
"commonName": "AcommonName",
"validityYears": "20",
"extensions": {
"subAltNames": {
"names": []
},
"custom": []
}
**Example response**:
@ -147,57 +158,67 @@ class AuthoritiesList(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "authority1",
"description": "this is authority1",
"pluginName": null,
"chain": "-----Begin ...",
"body": "-----Begin ...",
"name": "TestAuthority",
"roles": [{
"id": 123,
"name": "secure@example.com"
}, {
"id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"options": null,
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39"
"options": null
"authorityCertificate": {
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
"status": true,
"cn": "AcommonName",
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
"chain": "",
"notBefore": "2016-06-02T00:00:15+00:00",
"notAfter": "2023-06-02T23:59:15+00:00",
"owner": "secure@example.com",
"user": {
"username": "joe@example.com",
"active": true,
"email": "joe@example.com",
"id": 3
},
"active": true,
"bits": 2048,
"id": 2235,
"name": "TestAuthority"
},
"owner": "secure@example.com",
"id": 43,
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
}
:arg caName: authority's name
:arg caDescription: a sensible description about what the CA with be used for
:arg ownerEmail: the team or person who 'owns' this authority
:arg name: authority's name
:arg description: a sensible description about what the CA with be used for
:arg owner: the team or person who 'owns' this authority
:arg validityStart: when this authority should start issuing certificates
:arg validityEnd: when this authority should stop issuing certificates
:arg validityYears: starting from `now` how many years into the future the authority should be valid
:arg extensions: certificate extensions
:arg pluginName: name of the plugin to create the authority
:arg caType: the type of authority (root/subca)
:arg caParent: the parent authority if this is to be a subca
:arg caSigningAlgo: algorithm used to sign the authority
:arg plugin: name of the plugin to create the authority
:arg type: the type of authority (root/subca)
:arg parent: the parent authority if this is to be a subca
:arg signingAlgorithm: algorithm used to sign the authority
:arg keyType: key type
:arg caSensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
:arg sensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
in an HSM
:arg caKeyName: name of the key to store in the HSM (CloudCA)
:arg caSerialNumber: serial number of the authority
:arg caFirstSerial: specifies the starting serial number for certificates issued off of this authority
:arg keyName: name of the key to store in the HSM (CloudCA)
:arg serialNumber: serial number of the authority
:arg firstSerial: specifies the starting serial number for certificates issued off of this authority
:reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated
:statuscode 200: no error
"""
self.reqparse.add_argument('caName', type=str, location='json', required=True)
self.reqparse.add_argument('caDescription', type=str, location='json', required=False)
self.reqparse.add_argument('ownerEmail', type=str, location='json', required=True)
self.reqparse.add_argument('caDN', type=dict, location='json', required=False)
self.reqparse.add_argument('validityStart', type=str, location='json', required=False) # TODO validate
self.reqparse.add_argument('validityEnd', type=str, location='json', required=False) # TODO validate
self.reqparse.add_argument('extensions', type=dict, location='json', required=False)
self.reqparse.add_argument('pluginName', type=str, location='json', required=True)
self.reqparse.add_argument('caType', type=str, location='json', required=False)
self.reqparse.add_argument('caParent', type=str, location='json', required=False)
self.reqparse.add_argument('caSigningAlgo', type=str, location='json', required=False)
self.reqparse.add_argument('keyType', type=str, location='json', required=False)
self.reqparse.add_argument('caSensitivity', type=str, location='json', required=False)
self.reqparse.add_argument('caKeyName', type=str, location='json', required=False)
self.reqparse.add_argument('caSerialNumber', type=int, location='json', required=False)
self.reqparse.add_argument('caFirstSerial', type=int, location='json', required=False)
args = self.reqparse.parse_args()
return service.create(args)
return service.create(**data)
class Authorities(AuthenticatedResource):
@ -205,7 +226,7 @@ class Authorities(AuthenticatedResource):
self.reqparse = reqparse.RequestParser()
super(Authorities, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, authority_output_schema)
def get(self, authority_id):
"""
.. http:get:: /authorities/1
@ -229,26 +250,36 @@ class Authorities(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "authority1",
"description": "this is authority1",
"pluginName": null,
"chain": "-----Begin ...",
"body": "-----Begin ...",
"roles": [{
"id": 123,
"name": "secure@example.com"
}, {
"id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39"
"options": null
"owner": "secure@example.com",
"id": 43,
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
}
:arg description: a sensible description about what the CA with be used for
:arg owner: the team or person who 'owns' this authority
:arg active: set whether this authoritity is currently in use
:reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated
:statuscode 200: no error
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.get(authority_id)
@marshal_items(FIELDS)
def put(self, authority_id):
@validate_schema(authority_update_schema, authority_output_schema)
def put(self, authority_id, data=None):
"""
.. http:put:: /authorities/1
@ -263,9 +294,42 @@ class Authorities(AuthenticatedResource):
Accept: application/json, text/javascript
{
"roles": [],
"active": false
}
"name": "TestAuthority5",
"roles": [{
"id": 566,
"name": "TestAuthority5_admin"
}, {
"id": 567,
"name": "TestAuthority5_operator"
}, {
"id": 123,
"name": "secure@example.com"
}],
"active": true,
"authorityCertificate": {
"body": "-----BEGIN CERTIFICATE-----",
"status": null,
"cn": "AcommonName",
"description": "This is the ROOT certificate for the TestAuthority5 certificate authority.",
"chain": "",
"notBefore": "2016-06-03T00:00:51+00:00",
"notAfter": "2036-06-03T23:59:51+00:00",
"owner": "secure@example.com",
"user": {
"username": "joe@example.com",
"active": true,
"email": "joe@example.com",
"id": 3
},
"active": true,
"bits": 2048,
"id": 2280,
"name": "TestAuthority5"
},
"owner": "secure@example.com",
"id": 44,
"description": "This is the ROOT certificate for the TestAuthority5 certificate authority."
}
**Example response**:
@ -276,56 +340,74 @@ class Authorities(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "authority1",
"description": "this is authority1",
"pluginname": null,
"chain": "-----begin ...",
"body": "-----begin ...",
"active": false,
"notbefore": "2015-06-05t17:09:39",
"notafter": "2015-06-10t17:09:39"
"options": null
"name": "TestAuthority",
"roles": [{
"id": 123,
"name": "secure@example.com"
}, {
"id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"options": null,
"active": true,
"authorityCertificate": {
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
"status": true,
"cn": "AcommonName",
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
"chain": "",
"notBefore": "2016-06-02T00:00:15+00:00",
"notAfter": "2023-06-02T23:59:15+00:00",
"owner": "secure@example.com",
"user": {
"username": "joe@example.com",
"active": true,
"email": "joe@example.com",
"id": 3
},
"active": true,
"bits": 2048,
"id": 2235,
"name": "TestAuthority"
},
"owner": "secure@example.com",
"id": 43,
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
self.reqparse.add_argument('roles', type=list, location='json')
self.reqparse.add_argument('active', type=str, location='json')
args = self.reqparse.parse_args()
authority = service.get(authority_id)
role = role_service.get_by_name(authority.owner)
if not authority:
return dict(message='Not Found'), 404
# all the authority role members should be allowed
roles = [x.name for x in authority.roles]
# allow "owner" roles by team DL
roles.append(role)
permission = AuthorityPermission(authority_id, roles)
# we want to make sure that we cannot add roles that we are not members of
if not g.current_user.is_admin:
role_ids = set([r['id'] for r in args['roles']])
user_role_ids = set([r.id for r in g.current_user.roles])
if not role_ids.issubset(user_role_ids):
return dict(message="You are not allowed to associate a role which you are not a member of"), 400
if permission.can():
return service.update(authority_id, active=args['active'], roles=args['roles'])
return service.update(
authority_id,
owner=data['owner'],
description=data['description'],
active=data['active'],
roles=data['roles']
)
return dict(message="You are not authorized to update this authority"), 403
return dict(message="You are not authorized to update this authority."), 403
class CertificateAuthority(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificateAuthority, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, authority_output_schema)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/authority
@ -349,16 +431,42 @@ class CertificateAuthority(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "authority1",
"description": "this is authority1",
"pluginName": null,
"chain": "-----Begin ...",
"body": "-----Begin ...",
"name": "TestAuthority",
"roles": [{
"id": 123,
"name": "secure@example.com"
}, {
"id": 564,
"name": "TestAuthority_admin"
}, {
"id": 565,
"name": "TestAuthority_operator"
}],
"options": null,
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39"
"options": null
"authorityCertificate": {
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
"status": true,
"cn": "AcommonName",
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
"chain": "",
"notBefore": "2016-06-02T00:00:15+00:00",
"notAfter": "2023-06-02T23:59:15+00:00",
"owner": "secure@example.com",
"user": {
"username": "joe@example.com",
"active": true,
"email": "joe@example.com",
"id": 3
},
"active": true,
"bits": 2048,
"id": 2235,
"name": "TestAuthority"
},
"owner": "secure@example.com",
"id": 43,
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
}
:reqheader Authorization: OAuth token to authenticate
@ -367,10 +475,35 @@ class CertificateAuthority(AuthenticatedResource):
"""
cert = certificate_service.get(certificate_id)
if not cert:
return dict(message="Certificate not found"), 404
return dict(message="Certificate not found."), 404
return cert.authority
class AuthorityVisualizations(AuthenticatedResource):
def get(self, authority_id):
"""
{"name": "flare",
"children": [
{
"name": "analytics",
"children": [
{
"name": "cluster",
"children": [
{"name": "AgglomerativeCluster", "size": 3938},
{"name": "CommunityStructure", "size": 3812},
{"name": "HierarchicalCluster", "size": 6714},
{"name": "MergeEdge", "size": 743}
]
}
}
]}
"""
authority = service.get(authority_id)
return dict(name=authority.name, children=[{"name": c.name} for c in authority.certificates])
api.add_resource(AuthoritiesList, '/authorities', endpoint='authorities')
api.add_resource(Authorities, '/authorities/<int:authority_id>', endpoint='authority')
api.add_resource(AuthorityVisualizations, '/authorities/<int:authority_id>/visualize', endpoint='authority_visualizations')
api.add_resource(CertificateAuthority, '/certificates/<int:certificate_id>/authority', endpoint='certificateAuthority')

View File

@ -6,247 +6,102 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import datetime
from flask import current_app
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from sqlalchemy.orm import relationship
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy_utils import EncryptedType
from lemur.utils import get_key
from lemur.database import db
from lemur.models import certificate_associations, certificate_source_associations, \
certificate_destination_associations, certificate_notification_associations, \
certificate_replacement_associations, roles_certificates
from lemur.plugins.base import plugins
from lemur.utils import Vault
from lemur.common import defaults
from lemur.domains.models import Domain
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
from lemur.models import certificate_associations, certificate_source_associations, \
certificate_destination_associations, certificate_notification_associations
def get_or_increase_name(name):
count = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).count()
if count >= 1:
return name + '-' + str(count)
def create_name(issuer, not_before, not_after, subject, san):
"""
Create a name for our certificate. A naming standard
is based on a series of templates. The name includes
useful information such as Common Name, Validation dates,
and Issuer.
:rtype : str
:return:
"""
if san:
t = SAN_NAMING_TEMPLATE
else:
t = DEFAULT_NAMING_TEMPLATE
temp = t.format(
subject=subject,
issuer=issuer,
not_before=not_before.strftime('%Y%m%d'),
not_after=not_after.strftime('%Y%m%d')
)
# NOTE we may want to give more control over naming
# aws doesn't allow special chars except '-'
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
disallowed_chars = disallowed_chars.replace("-", "")
disallowed_chars = disallowed_chars.replace(".", "")
temp = temp.replace('*', "WILDCARD")
for c in disallowed_chars:
temp = temp.replace(c, "")
# white space is silly too
return temp.replace(" ", "-")
def cert_get_cn(cert):
"""
Attempts to get a sane common name from a given certificate.
:param cert:
:return: Common name or None
"""
return cert.subject.get_attributes_for_oid(
x509.OID_COMMON_NAME
)[0].value.strip()
def cert_get_domains(cert):
"""
Attempts to get an domains listed in a certificate.
If 'subjectAltName' extension is not available we simply
return the common name.
:param cert:
:return: List of domains
"""
domains = []
try:
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
entries = ext.value.get_values_for_type(x509.DNSName)
for entry in entries:
domains.append(entry)
except Exception as e:
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
return domains
def cert_get_serial(cert):
"""
Fetch the serial number from the certificate.
:param cert:
:return: serial number
"""
return cert.serial
def cert_is_san(cert):
"""
Determines if a given certificate is a SAN certificate.
SAN certificates are simply certificates that cover multiple domains.
:param cert:
:return: Bool
"""
if len(cert_get_domains(cert)) > 1:
return True
def cert_is_wildcard(cert):
"""
Determines if certificate is a wildcard certificate.
:param cert:
:return: Bool
"""
domains = cert_get_domains(cert)
if len(domains) == 1 and domains[0][0:1] == "*":
return True
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
return True
def cert_get_bitstrength(cert):
"""
Calculates a certificates public key bit length.
:param cert:
:return: Integer
"""
return cert.public_key().key_size
def cert_get_issuer(cert):
"""
Gets a sane issuer from a given certificate.
:param cert:
:return: Issuer
"""
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
try:
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
for c in delchars:
issuer = issuer.replace(c, "")
return issuer
except Exception as e:
current_app.logger.error("Unable to get issuer! {0}".format(e))
def cert_get_not_before(cert):
"""
Gets the naive datetime of the certificates 'not_before' field.
This field denotes the first date in time which the given certificate
is valid.
:param cert:
:return: Datetime
"""
return cert.not_valid_before
def cert_get_not_after(cert):
"""
Gets the naive datetime of the certificates 'not_after' field.
This field denotes the last date in time which the given certificate
is valid.
:param cert:
:return: Datetime
"""
return cert.not_valid_after
def get_name_from_arn(arn):
"""
Extract the certificate name from an arn.
:param arn: IAM SSL arn
:return: name of the certificate as uploaded to AWS
"""
return arn.split("/", 1)[1]
def get_account_number(arn):
"""
Extract the account number from an arn.
:param arn: IAM SSL arn
:return: account number associated with ARN
"""
return arn.split(":")[4]
return name
class Certificate(db.Model):
__tablename__ = 'certificates'
id = Column(Integer, primary_key=True)
owner = Column(String(128))
body = Column(Text())
private_key = Column(EncryptedType(String, get_key))
status = Column(String(128))
deleted = Column(Boolean, index=True)
name = Column(String(128))
owner = Column(String(128), nullable=False)
name = Column(String(128)) # , unique=True) TODO make all names unique
description = Column(String(1024))
active = Column(Boolean, default=True)
body = Column(Text(), nullable=False)
chain = Column(Text())
bits = Column(Integer())
private_key = Column(Vault)
issuer = Column(String(128))
serial = Column(String(128))
cn = Column(String(128))
description = Column(String(1024))
active = Column(Boolean, default=True)
san = Column(String(1024))
deleted = Column(Boolean, index=True)
not_before = Column(DateTime)
not_after = Column(DateTime)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
signing_algorithm = Column(String(128))
status = Column(String(128))
bits = Column(Integer())
san = Column(String(1024)) # TODO this should be migrated to boolean
user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id'))
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
roles = relationship("Role", secondary=roles_certificates, backref="certificate")
replaces = relationship("Certificate",
secondary=certificate_replacement_associations,
primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
backref='replaced')
def __init__(self, body, private_key=None, chain=None):
self.body = body
# We encrypt the private_key on creation
self.private_key = private_key
self.chain = chain
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
self.bits = cert_get_bitstrength(cert)
self.issuer = cert_get_issuer(cert)
self.serial = cert_get_serial(cert)
self.cn = cert_get_cn(cert)
self.san = cert_is_san(cert)
self.not_before = cert_get_not_before(cert)
self.not_after = cert_get_not_after(cert)
self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san)
def __init__(self, **kwargs):
cert = defaults.parse_certificate(kwargs['body'])
for domain in cert_get_domains(cert):
self.issuer = defaults.issuer(cert)
self.cn = defaults.common_name(cert)
self.san = defaults.san(cert)
self.not_before = defaults.not_before(cert)
self.not_after = defaults.not_after(cert)
# when destinations are appended they require a valid name.
if kwargs.get('name'):
self.name = kwargs['name']
else:
self.name = get_or_increase_name(defaults.certificate_name(self.cn, self.issuer, self.not_before, self.not_after, self.san))
self.owner = kwargs['owner']
self.body = kwargs['body']
self.private_key = kwargs.get('private_key')
self.chain = kwargs.get('chain')
self.destinations = kwargs.get('destinations', [])
self.notifications = kwargs.get('notifications', [])
self.description = kwargs.get('description')
self.roles = list(set(kwargs.get('roles', [])))
self.replaces = kwargs.get('replacements', [])
self.signing_algorithm = defaults.signing_algorithm(cert)
self.bits = defaults.bitstrength(cert)
self.serial = defaults.serial(cert)
for domain in defaults.domains(cert):
self.domains.append(Domain(name=domain))
@property
@ -276,11 +131,47 @@ class Certificate(db.Model):
"""
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
@event.listens_for(Certificate.destinations, 'append')
def update_destinations(target, value, initiator):
"""
Attempt to upload the new certificate to the new destination
:param target:
:param value:
:param initiator:
:return:
"""
destination_plugin = plugins.get(value.plugin_name)
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
try:
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
except Exception as e:
current_app.logger.exception(e)
@event.listens_for(Certificate.replaces, 'append')
def update_replacement(target, value, initiator):
"""
When a certificate is marked as 'replaced' it is then marked as in-active
:param target:
:param value:
:param initiator:
:return:
"""
value.active = False
@event.listens_for(Certificate, 'before_update')
def protect_active(mapper, connection, target):
"""
When a certificate has a replacement do not allow it to be marked as 'active'
:param connection:
:param mapper:
:param target:
:return:
"""
if target.active:
if target.replaced:
raise Exception("Cannot mark certificate as active, certificate has been marked as replaced.")

View File

@ -0,0 +1,155 @@
"""
.. module: lemur.certificates.schemas
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import current_app
from marshmallow import fields, validates_schema, post_load
from marshmallow.exceptions import ValidationError
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema
from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.destinations.schemas import DestinationNestedOutputSchema
from lemur.notifications.schemas import NotificationNestedOutputSchema
from lemur.roles.schemas import RoleNestedOutputSchema
from lemur.domains.schemas import DomainNestedOutputSchema
from lemur.users.schemas import UserNestedOutputSchema
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.common import validators
from lemur.notifications import service as notification_service
class CertificateSchema(LemurInputSchema):
owner = fields.Email(required=True)
description = fields.String()
@post_load
def default_notifications(self, data):
if not data['notifications']:
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
notification_name = 'DEFAULT_SECURITY'
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
return data
class CertificateInputSchema(CertificateSchema):
name = fields.String()
common_name = fields.String(required=True, validate=validators.sensitive_domain)
authority = fields.Nested(AssociatedAuthoritySchema, required=True)
validity_start = fields.DateTime()
validity_end = fields.DateTime()
validity_years = fields.Integer()
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
csr = fields.String(validate=validators.csr)
# certificate body fields
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
extensions = fields.Nested(ExtensionSchema)
@validates_schema
def validate_dates(self, data):
validators.dates(data)
class CertificateEditInputSchema(CertificateSchema):
active = fields.Boolean()
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
class CertificateNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
active = fields.Boolean()
bits = fields.Integer()
body = fields.String()
chain = fields.String()
description = fields.String()
name = fields.String()
cn = fields.String()
not_after = fields.DateTime()
not_before = fields.DateTime()
owner = fields.Email()
status = fields.Boolean()
creator = fields.Nested(UserNestedOutputSchema)
issuer = fields.Nested(AuthorityNestedOutputSchema)
class CertificateOutputSchema(LemurOutputSchema):
id = fields.Integer()
active = fields.Boolean()
bits = fields.Integer()
body = fields.String()
chain = fields.String()
deleted = fields.Boolean(default=False)
description = fields.String()
issuer = fields.String()
name = fields.String()
cn = fields.String()
not_after = fields.DateTime()
not_before = fields.DateTime()
owner = fields.Email()
san = fields.Boolean()
serial = fields.String()
signing_algorithm = fields.String()
status = fields.Boolean()
user = fields.Nested(UserNestedOutputSchema)
domains = fields.Nested(DomainNestedOutputSchema, many=True)
destinations = fields.Nested(DestinationNestedOutputSchema, many=True)
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
authority = fields.Nested(AuthorityNestedOutputSchema)
roles = fields.Nested(RoleNestedOutputSchema, many=True)
endpoints = fields.List(fields.Dict(), missing=[])
class CertificateUploadInputSchema(CertificateSchema):
name = fields.String()
active = fields.Boolean(missing=True)
private_key = fields.String(validate=validators.private_key)
body = fields.String(required=True, validate=validators.public_certificate)
chain = fields.String(validate=validators.public_certificate) # TODO this could be multiple certificates
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
@validates_schema
def keys(self, data):
if data.get('destinations'):
if not data.get('private_key'):
raise ValidationError('Destinations require private key.')
class CertificateExportInputSchema(LemurInputSchema):
plugin = fields.Nested(PluginInputSchema)
certificate_input_schema = CertificateInputSchema()
certificate_output_schema = CertificateOutputSchema()
certificates_output_schema = CertificateOutputSchema(many=True)
certificate_upload_input_schema = CertificateUploadInputSchema()
certificate_export_input_schema = CertificateExportInputSchema()
certificate_edit_input_schema = CertificateEditInputSchema()

View File

@ -11,14 +11,17 @@ from sqlalchemy import func, or_
from flask import g, current_app
from lemur import database
from lemur.extensions import metrics
from lemur.plugins.base import plugins
from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination
from lemur.notifications.models import Notification
from lemur.authorities.models import Authority
from lemur.domains.models import Domain
from lemur.roles.models import Role
from lemur.roles import service as role_service
from cryptography import x509
from cryptography.hazmat.backends import default_backend
@ -76,48 +79,74 @@ def find_duplicates(cert_body):
return Certificate.query.filter_by(body=cert_body).all()
def update(cert_id, owner, description, active, destinations, notifications):
def export(cert, export_plugin):
"""
Updates a certificate.
Exports a certificate to the requested format. This format
may be a binary format.
:param export_plugin:
:param cert:
:return:
"""
plugin = plugins.get(export_plugin['slug'])
return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions'])
def update(cert_id, owner, description, active, destinations, notifications, replaces, roles):
"""
Updates a certificate
:param cert_id:
:param owner:
:param description:
:param active:
:param destinations:
:param notifications:
:param replaces:
:return:
"""
cert = get(cert_id)
cert.owner = owner
cert.active = active
cert.description = description
database.update_list(cert, 'notifications', Notification, notifications)
database.update_list(cert, 'destinations', Destination, destinations)
cert.destinations = destinations
cert.notifications = notifications
cert.roles = roles
cert.replaces = replaces
cert.owner = owner
return database.update(cert)
def mint(issuer_options):
def create_certificate_roles(**kwargs):
# create an role for the owner and assign it
owner_role = role_service.get_by_name(kwargs['owner'])
if not owner_role:
owner_role = role_service.create(
kwargs['owner'],
description="Auto generated role based on owner: {0}".format(kwargs['owner'])
)
return [owner_role]
def mint(**kwargs):
"""
Minting is slightly different for each authority.
Support for multiple authorities is handled by individual plugins.
:param issuer_options:
"""
authority = issuer_options['authority']
authority = kwargs['authority']
issuer = plugins.get(authority.plugin_name)
csr, private_key = create_csr(issuer_options)
# allow the CSR to be specified by the user
if not kwargs.get('csr'):
csr, private_key = create_csr(**kwargs)
else:
csr = str(kwargs.get('csr'))
private_key = None
issuer_options['creator'] = g.user.email
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
cert = Certificate(cert_body, private_key, cert_chain)
cert.user = g.user
cert.authority = authority
database.update(cert)
return cert, private_key, cert_chain,
cert_body, cert_chain = issuer.create_certificate(csr, kwargs)
return cert_body, private_key, cert_chain,
def import_certificate(**kwargs):
@ -134,61 +163,32 @@ def import_certificate(**kwargs):
:param kwargs:
"""
from lemur.users import service as user_service
from lemur.notifications import service as notification_service
cert = Certificate(kwargs['public_certificate'], chain=kwargs['intermediate_certificate'])
# TODO future source plugins might have a better understanding of who the 'owner' is we should support this
cert.owner = kwargs.get('owner', current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0])
cert.creator = kwargs.get('creator', user_service.get_by_email('lemur@nobody'))
if not kwargs.get('owner'):
kwargs['owner'] = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0]
# NOTE existing certs may not follow our naming standard we will
# overwrite the generated name with the actual cert name
if kwargs.get('name'):
cert.name = kwargs.get('name')
if not kwargs.get('creator'):
kwargs['creator'] = user_service.get_by_email('lemur@nobody')
if kwargs.get('user'):
cert.user = kwargs.get('user')
notification_name = 'DEFAULT_SECURITY'
notifications = notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
cert.notifications = notifications
cert = database.create(cert)
return cert
return upload(**kwargs)
def upload(**kwargs):
"""
Allows for pre-made certificates to be imported into Lemur.
"""
from lemur.notifications import service as notification_service
cert = Certificate(
kwargs.get('public_cert'),
kwargs.get('private_key'),
kwargs.get('intermediate_cert'),
)
roles = create_certificate_roles(**kwargs)
cert.description = kwargs.get('description')
if kwargs.get('roles'):
kwargs['roles'] += roles
else:
kwargs['roles'] = roles
cert = Certificate(**kwargs)
cert.owner = kwargs['owner']
cert = database.create(cert)
g.user.certificates.append(cert)
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
# create default notifications for this certificate if none are provided
notifications = []
if not kwargs.get('notifications'):
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
notification_name = 'DEFAULT_SECURITY'
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
cert.notifications = notifications
database.update(cert)
return cert
@ -197,33 +197,26 @@ def create(**kwargs):
"""
Creates a new certificate.
"""
from lemur.notifications import service as notification_service
cert, private_key, cert_chain = mint(kwargs)
kwargs['creator'] = g.user.email
cert_body, private_key, cert_chain = mint(**kwargs)
kwargs['body'] = cert_body
kwargs['private_key'] = private_key
kwargs['chain'] = cert_chain
cert.owner = kwargs['owner']
roles = create_certificate_roles(**kwargs)
if kwargs.get('roles'):
kwargs['roles'] += roles
else:
kwargs['roles'] = roles
cert = Certificate(**kwargs)
database.create(cert)
cert.description = kwargs['description']
g.user.certificates.append(cert)
database.update(g.user)
cert.authority = kwargs['authority']
database.commit()
# do this after the certificate has already been created because if it fails to upload to the third party
# we do not want to lose the certificate information.
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
# create default notifications for this certificate if none are provided
notifications = []
if not kwargs.get('notifications'):
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
notification_name = 'DEFAULT_SECURITY'
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
cert.notifications = notifications
database.update(cert)
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
return cert
@ -247,6 +240,7 @@ def render(args):
if filt:
terms = filt.split(';')
if 'issuer' in terms:
# we can't rely on issuer being correct in the cert directly so we combine queries
sub_query = database.session_query(Authority.id)\
@ -261,10 +255,17 @@ def render(args):
)
return database.sort_and_page(query, Certificate, args)
if 'destination' in terms:
elif 'destination' in terms:
query = query.filter(Certificate.destinations.any(Destination.id == terms[1]))
elif 'active' in filt: # this is really weird but strcmp seems to not work here??
query = query.filter(Certificate.active == terms[1])
elif 'cn' in terms:
query = query.filter(
or_(
Certificate.cn.ilike('%{0}%'.format(terms[1])),
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
)
)
else:
query = database.filter(query, Certificate, terms)
@ -291,7 +292,7 @@ def render(args):
return database.sort_and_page(query, Certificate, args)
def create_csr(csr_config):
def create_csr(**csr_config):
"""
Given a list of domains create the appropriate csr
for those domains
@ -307,9 +308,9 @@ def create_csr(csr_config):
# TODO When we figure out a better way to validate these options they should be parsed as str
builder = x509.CertificateSigningRequestBuilder()
builder = builder.subject_name(x509.Name([
x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['commonName']),
x509.NameAttribute(x509.OID_COMMON_NAME, csr_config['common_name']),
x509.NameAttribute(x509.OID_ORGANIZATION_NAME, csr_config['organization']),
x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizationalUnit']),
x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, csr_config['organizational_unit']),
x509.NameAttribute(x509.OID_COUNTRY_NAME, csr_config['country']),
x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, csr_config['state']),
x509.NameAttribute(x509.OID_LOCALITY_NAME, csr_config['location']),
@ -319,17 +320,18 @@ def create_csr(csr_config):
x509.BasicConstraints(ca=False, path_length=None), critical=True,
)
for k, v in csr_config.get('extensions', {}).items():
if k == 'subAltNames':
# map types to their x509 objects
general_names = []
for name in v['names']:
if name['nameType'] == 'DNSName':
general_names.append(x509.DNSName(name['value']))
if csr_config.get('extensions'):
for k, v in csr_config.get('extensions', {}).items():
if k == 'sub_alt_names':
# map types to their x509 objects
general_names = []
for name in v['names']:
if name['name_type'] == 'DNSName':
general_names.append(x509.DNSName(name['value']))
builder = builder.add_extension(
x509.SubjectAlternativeName(general_names), critical=True
)
builder = builder.add_extension(
x509.SubjectAlternativeName(general_names), critical=True
)
# TODO support more CSR options, none of the authority plugins currently support these options
# builder.add_extension(
@ -379,7 +381,7 @@ def create_csr(csr_config):
)
# serialize our private key and CSR
pem = private_key.private_bytes(
private_key = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it
encryption_algorithm=serialization.NoEncryption()
@ -389,7 +391,7 @@ def create_csr(csr_config):
encoding=serialization.Encoding.PEM
)
return csr, pem
return csr, private_key
def stats(**kwargs):
@ -420,3 +422,23 @@ def stats(**kwargs):
values.append(count)
return {'labels': keys, 'values': values}
def get_account_number(arn):
"""
Extract the account number from an arn.
:param arn: IAM SSL arn
:return: account number associated with ARN
"""
return arn.split(":")[4]
def get_name_from_arn(arn):
"""
Extract the certificate name from an arn.
:param arn: IAM SSL arn
:return: name of the certificate as uploaded to AWS
"""
return arn.split("/", 1)[1]

View File

@ -5,14 +5,14 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import os
import re
import hashlib
import requests
import subprocess
from OpenSSL import crypto
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from flask import current_app
from lemur.utils import mktempfile
def ocsp_verify(cert_path, issuer_chain_path):
@ -53,27 +53,18 @@ def crl_verify(cert_path):
:return: True if certificate is valid, False otherwise
:raise Exception: If certificate does not have CRL
"""
s = "(http(s)?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}/\S*?$)"
regex = re.compile(s, re.MULTILINE)
with open(cert_path, 'rt') as c:
cert = x509.load_pem_x509_certificate(c.read(), default_backend())
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_path, 'rt').read())
for x in range(x509.get_extension_count()):
ext = x509.get_extension(x)
if ext.get_short_name() == 'crlDistributionPoints':
r = regex.search(ext.get_data())
points = r.groups()
break
else:
raise Exception("Certificate does not have a CRL distribution point")
for point in points:
if point:
response = requests.get(point)
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content)
revoked = crl.get_revoked()
for r in revoked:
if x509.get_serial_number() == r.get_serial():
return
distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value
for p in distribution_points:
point = p.full_name[0].value
response = requests.get(point)
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) # TODO this should be switched to cryptography when support exists
revoked = crl.get_revoked()
for r in revoked:
if cert.serial == r.get_serial():
return
return True
@ -99,22 +90,6 @@ def verify(cert_path, issuer_chain_path):
raise Exception("Failed to verify")
def make_tmp_file(string):
"""
Creates a temporary file for a given string
:param string:
:return: Full file path to created file
"""
m = hashlib.md5()
m.update(string)
hexdigest = m.hexdigest()
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), hexdigest)
with open(path, 'w') as f:
f.write(string)
return path
def verify_string(cert_string, issuer_string):
"""
Verify a certificate given only it's string value
@ -123,13 +98,11 @@ def verify_string(cert_string, issuer_string):
:param issuer_string:
:return: True if valid, False otherwise
"""
cert_path = make_tmp_file(cert_string)
issuer_path = make_tmp_file(issuer_string)
status = verify(cert_path, issuer_path)
remove_tmp_file(cert_path)
remove_tmp_file(issuer_path)
with mktempfile() as cert_tmp:
with open(cert_tmp, 'w') as f:
f.write(cert_string)
with mktempfile() as issuer_tmp:
with open(issuer_tmp, 'w') as f:
f.write(issuer_string)
status = verify(cert_tmp, issuer_tmp)
return status
def remove_tmp_file(file_path):
os.remove(file_path)

View File

@ -5,106 +5,37 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import base64
from builtins import str
from flask import Blueprint, make_response, jsonify
from flask.ext.restful import reqparse, Api, fields
from flask.ext.restful import reqparse, Api
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from lemur.certificates import service
from lemur.authorities.models import Authority
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, UpdateCertificatePermission
from lemur.auth.permissions import ViewKeyPermission, AuthorityPermission, CertificatePermission
from lemur.certificates import service
from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \
certificate_upload_input_schema, certificates_output_schema, certificate_export_input_schema, certificate_edit_input_schema
from lemur.roles import service as role_service
from lemur.common.utils import marshal_items, paginated_parser
mod = Blueprint('certificates', __name__)
api = Api(mod)
FIELDS = {
'name': fields.String,
'id': fields.Integer,
'bits': fields.Integer,
'deleted': fields.String,
'issuer': fields.String,
'serial': fields.String,
'owner': fields.String,
'chain': fields.String,
'san': fields.String,
'active': fields.Boolean,
'description': fields.String,
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
'cn': fields.String,
'status': fields.String,
'body': fields.String
}
def valid_authority(authority_options):
"""
Defends against invalid authorities
:param authority_options:
:return: :raise ValueError:
"""
name = authority_options['name']
authority = Authority.query.filter(Authority.name == name).one()
if not authority:
raise ValueError("Unable to find authority specified")
if not authority.active:
raise ValueError("Selected authority [{0}] is not currently active".format(name))
return authority
def pem_str(value, name):
"""
Used to validate that the given string is a PEM formatted string
:param value:
:param name:
:return: :raise ValueError:
"""
try:
x509.load_pem_x509_certificate(bytes(value), default_backend())
except Exception:
raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name))
return value
def private_key_str(value, name):
"""
User to validate that a given string is a RSA private key
:param value:
:param name:
:return: :raise ValueError:
"""
try:
serialization.load_pem_private_key(bytes(value), None, backend=default_backend())
except Exception:
raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name))
return value
class CertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesList, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, certificates_output_schema)
def get(self):
"""
.. http:get:: /certificates
@ -128,37 +59,65 @@ class CertificatesList(AuthenticatedResource):
Content-Type: text/javascript
{
"items": [
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": 'bob@example.com",
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
}
]
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query filter: key value pair format is k;v
:query count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
@ -172,8 +131,8 @@ class CertificatesList(AuthenticatedResource):
args = parser.parse_args()
return service.render(args)
@marshal_items(FIELDS)
def post(self):
@validate_schema(certificate_input_schema, certificate_output_schema)
def post(self, data=None):
"""
.. http:post:: /certificates
@ -187,47 +146,6 @@ class CertificatesList(AuthenticatedResource):
Host: example.com
Accept: application/json, text/javascript
{
"country": "US",
"state": "CA",
"location": "A Place",
"organization": "ExampleInc.",
"organizationalUnit": "Operations",
"owner": "bob@example.com",
"description": "test",
"selectedAuthority": "timetest2",
"authority": {
"body": "-----BEGIN...",
"name": "timetest2",
"chain": "",
"notBefore": "2015-06-05T15:20:59",
"active": true,
"id": 50,
"notAfter": "2015-06-17T15:21:08",
"description": "dsfdsf"
},
"extensions": {
"basicConstraints": {},
"keyUsage": {
"isCritical": true,
"useKeyEncipherment": true,
"useDigitalSignature": true
},
"extendedKeyUsage": {
"isCritical": true,
"useServerAuthentication": true
},
"subjectKeyIdentifier": {
"includeSKI": true
},
"subAltNames": {
"names": []
}
},
"commonName": "test",
"validityStart": "2015-06-05T07:00:00.000Z",
"validityEnd": "2015-06-16T07:00:00.000Z"
}
**Example response**:
@ -238,24 +156,54 @@ class CertificatesList(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": "jimbob@example.com",
"active": false,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}
:arg extensions: extensions to be used in the certificate
:arg description: description for new certificate
:arg owner: owner email
@ -266,53 +214,36 @@ class CertificatesList(AuthenticatedResource):
:arg state: state for the CSR
:arg location: location for the CSR
:arg organization: organization for CSR
:arg commonName: certiifcate common name
:arg commonName: certificate common name
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
self.reqparse.add_argument('extensions', type=dict, location='json')
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
self.reqparse.add_argument('authority', type=valid_authority, location='json')
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('country', type=str, location='json')
self.reqparse.add_argument('state', type=str, location='json')
self.reqparse.add_argument('location', type=str, location='json')
self.reqparse.add_argument('organization', type=str, location='json')
self.reqparse.add_argument('organizationalUnit', type=str, location='json')
self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('commonName', type=str, location='json')
args = self.reqparse.parse_args()
authority = args['authority']
role = role_service.get_by_name(authority.owner)
role = role_service.get_by_name(data['authority'].owner)
# all the authority role members should be allowed
roles = [x.name for x in authority.roles]
roles = [x.name for x in data['authority'].roles]
# allow "owner" roles by team DL
roles.append(role)
permission = AuthorityPermission(authority.id, roles)
authority_permission = AuthorityPermission(data['authority'].id, roles)
if permission.can():
return service.create(**args)
if authority_permission.can():
return service.create(**data)
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
return dict(message="You are not authorized to use {0}".format(data['authority'].name)), 403
class CertificatesUpload(AuthenticatedResource):
""" Defines the 'certificates' upload endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesUpload, self).__init__()
@marshal_items(FIELDS)
def post(self):
@validate_schema(certificate_upload_input_schema, certificate_output_schema)
def post(self, data=None):
"""
.. http:post:: /certificates/upload
@ -332,7 +263,9 @@ class CertificatesUpload(AuthenticatedResource):
"intermediateCert": "---Begin Public...",
"privateKey": "---Begin Private..."
"destinations": [],
"notifications": []
"notifications": [],
"replacements": [],
"name": "cert1"
}
**Example response**:
@ -344,22 +277,51 @@ class CertificatesUpload(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": "joe@example.com",
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}
:arg owner: owner email for certificate
@ -370,26 +332,19 @@ class CertificatesUpload(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate
:statuscode 403: unauthenticated
:statuscode 200: no error
"""
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('owner', type=str, required=True, location='json')
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', location='json')
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
args = self.reqparse.parse_args()
if args.get('destinations'):
if args.get('private_key'):
return service.upload(**args)
"""
if data.get('destinations'):
if data.get('private_key'):
return service.upload(**data)
else:
raise Exception("Private key must be provided in order to upload certificate to AWS")
return service.upload(**args)
return service.upload(**data)
class CertificatesStats(AuthenticatedResource):
""" Defines the 'certificates' stats endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesStats, self).__init__()
@ -446,7 +401,7 @@ class CertificatePrivateKey(AuthenticatedResource):
role = role_service.get_by_name(cert.owner)
permission = ViewKeyPermission(certificate_id, hasattr(role, 'id'))
permission = ViewKeyPermission(certificate_id, getattr(role, 'name', None))
if permission.can():
response = make_response(jsonify(key=cert.private_key), 200)
@ -462,7 +417,7 @@ class Certificates(AuthenticatedResource):
self.reqparse = reqparse.RequestParser()
super(Certificates, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, certificate_output_schema)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1
@ -486,32 +441,62 @@ class Certificates(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": "bob@example.com",
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.get(certificate_id)
@marshal_items(FIELDS)
def put(self, certificate_id):
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
def put(self, certificate_id, data=None):
"""
.. http:put:: /certificates/1
@ -529,7 +514,8 @@ class Certificates(AuthenticatedResource):
"owner": "jimbob@example.com",
"active": false
"notifications": [],
"destinations": []
"destinations": [],
"replacements": []
}
**Example response**:
@ -541,47 +527,72 @@ class Certificates(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": "jimbob@example.com",
"active": false,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown",
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
self.reqparse.add_argument('active', type=bool, location='json')
self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
args = self.reqparse.parse_args()
"""
cert = service.get(certificate_id)
role = role_service.get_by_name(cert.owner)
permission = UpdateCertificatePermission(certificate_id, hasattr(role, 'id'))
permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
if permission.can():
return service.update(
certificate_id,
args['owner'],
args['description'],
args['active'],
args['destinations'],
args['notifications']
data['owner'],
data['description'],
data['active'],
data['destinations'],
data['notifications'],
data['replacements'],
data['roles']
)
return dict(message='You are not authorized to update this certificate'), 403
@ -589,11 +600,12 @@ class Certificates(AuthenticatedResource):
class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(NotificationCertificatesList, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, certificates_output_schema)
def get(self, notification_id):
"""
.. http:get:: /notifications/1/certificates
@ -617,37 +629,65 @@ class NotificationCertificatesList(AuthenticatedResource):
Content-Type: text/javascript
{
"items": [
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": 'bob@example.com",
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
}
]
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
@ -662,9 +702,192 @@ class NotificationCertificatesList(AuthenticatedResource):
args['notification_id'] = notification_id
return service.render(args)
class CertificatesReplacementsList(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificatesReplacementsList, self).__init__()
@validate_schema(None, certificates_output_schema)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/replacements
One certificate
**Example request**:
.. sourcecode:: http
GET /certificates/1/replacements HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"status": null,
"cn": "*.test.example.net",
"chain": "",
"authority": {
"active": true,
"owner": "secure@example.com",
"id": 1,
"description": "verisign test authority",
"name": "verisign"
},
"owner": "joe@example.com",
"serial": "82311058732025924142789179368889309156",
"id": 2288,
"issuer": "SymantecCorporation",
"notBefore": "2016-06-03T00:00:00+00:00",
"notAfter": "2018-01-12T23:59:59+00:00",
"destinations": [],
"bits": 2048,
"body": "-----BEGIN CERTIFICATE-----...",
"description": null,
"deleted": null,
"notifications": [{
"id": 1
}]
"signingAlgorithm": "sha256",
"user": {
"username": "jane",
"active": true,
"email": "jane@example.com",
"id": 2
},
"active": true,
"domains": [{
"sensitive": false,
"id": 1090,
"name": "*.test.example.net"
}],
"replaces": [],
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
"roles": [{
"id": 464,
"description": "This is a google group based role created by Lemur",
"name": "joe@example.com"
}],
"san": null
}],
"total": 1
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.get(certificate_id).replaces
class CertificateExport(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(CertificateExport, self).__init__()
@validate_schema(certificate_export_input_schema, None)
def post(self, certificate_id, data=None):
"""
.. http:post:: /certificates/1/export
Export a certificate
**Example request**:
.. sourcecode:: http
PUT /certificates/1/export HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"export": {
"plugin": {
"pluginOptions": [{
"available": ["Java Key Store (JKS)"],
"required": true,
"type": "select",
"name": "type",
"helpMessage": "Choose the format you wish to export",
"value": "Java Key Store (JKS)"
}, {
"required": false,
"type": "str",
"name": "passphrase",
"validation": "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,}$",
"helpMessage": "If no passphrase is given one will be generated for you, we highly recommend this. Minimum length is 8."
}, {
"required": false,
"type": "str",
"name": "alias",
"helpMessage": "Enter the alias you wish to use for the keystore."
}],
"version": "unknown",
"description": "Attempts to generate a JKS keystore or truststore",
"title": "Java",
"author": "Kevin Glisson",
"type": "export",
"slug": "java-export"
}
}
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"data": "base64encodedstring",
"passphrase": "UAWOHW#&@_%!tnwmxh832025",
"extension": "jks"
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
cert = service.get(certificate_id)
permission = CertificatePermission(cert.id, [x.name for x in cert.roles])
options = data['plugin']['plugin_options']
plugin = data['plugin']['plugin_object']
if plugin.requires_key:
if permission.can():
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
else:
return dict(message='You are not authorized to export this certificate'), 403
else:
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
# we take a hit in message size when b64 encoding
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data))
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates')
api.add_resource(CertificateExport, '/certificates/<int:certificate_id>/export', endpoint='exportCertificate')
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
endpoint='notificationCertificates')
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',
endpoint='replacements')

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

@ -0,0 +1,175 @@
import sys
from flask import current_app
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
def parse_certificate(body):
if sys.version_info >= (3, 0):
return x509.load_pem_x509_certificate(body, default_backend())
else:
return x509.load_pem_x509_certificate(bytes(body), default_backend())
def certificate_name(common_name, issuer, not_before, not_after, san):
"""
Create a name for our certificate. A naming standard
is based on a series of templates. The name includes
useful information such as Common Name, Validation dates,
and Issuer.
:param san:
:param common_name:
:param not_after:
:param issuer:
:param not_before:
:rtype : str
:return:
"""
if san:
t = SAN_NAMING_TEMPLATE
else:
t = DEFAULT_NAMING_TEMPLATE
temp = t.format(
subject=common_name,
issuer=issuer,
not_before=not_before.strftime('%Y%m%d'),
not_after=not_after.strftime('%Y%m%d')
)
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
disallowed_chars = disallowed_chars.replace("-", "")
disallowed_chars = disallowed_chars.replace(".", "")
temp = temp.replace('*', "WILDCARD")
for c in disallowed_chars:
temp = temp.replace(c, "")
# white space is silly too
return temp.replace(" ", "-")
def signing_algorithm(cert):
return cert.signature_hash_algorithm.name
def common_name(cert):
"""
Attempts to get a sane common name from a given certificate.
:param cert:
:return: Common name or None
"""
return cert.subject.get_attributes_for_oid(
x509.OID_COMMON_NAME
)[0].value.strip()
def domains(cert):
"""
Attempts to get an domains listed in a certificate.
If 'subjectAltName' extension is not available we simply
return the common name.
:param cert:
:return: List of domains
"""
domains = []
try:
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
entries = ext.value.get_values_for_type(x509.DNSName)
for entry in entries:
domains.append(entry)
except Exception as e:
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
return domains
def serial(cert):
"""
Fetch the serial number from the certificate.
:param cert:
:return: serial number
"""
return cert.serial
def san(cert):
"""
Determines if a given certificate is a SAN certificate.
SAN certificates are simply certificates that cover multiple domains.
:param cert:
:return: Bool
"""
if len(domains(cert)) > 1:
return True
def is_wildcard(cert):
"""
Determines if certificate is a wildcard certificate.
:param cert:
:return: Bool
"""
d = domains(cert)
if len(d) == 1 and d[0][0:1] == "*":
return True
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
return True
def bitstrength(cert):
"""
Calculates a certificates public key bit length.
:param cert:
:return: Integer
"""
return cert.public_key().key_size
def issuer(cert):
"""
Gets a sane issuer from a given certificate.
:param cert:
:return: Issuer
"""
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
try:
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
for c in delchars:
issuer = issuer.replace(c, "")
return issuer
except Exception as e:
current_app.logger.error("Unable to get issuer! {0}".format(e))
def not_before(cert):
"""
Gets the naive datetime of the certificates 'not_before' field.
This field denotes the first date in time which the given certificate
is valid.
:param cert:
:return: Datetime
"""
return cert.not_valid_before
def not_after(cert):
"""
Gets the naive datetime of the certificates 'not_after' field.
This field denotes the last date in time which the given certificate
is valid.
:return: Datetime
"""
return cert.not_valid_after

View File

@ -22,7 +22,8 @@ class InstanceManager(object):
def add(self, class_path):
self.cache = None
self.class_list.append(class_path)
if class_path not in self.class_list:
self.class_list.append(class_path)
def remove(self, class_path):
self.cache = None

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

@ -0,0 +1,153 @@
"""
.. module: lemur.common.schema
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from functools import wraps
from flask import request, current_app
from sqlalchemy.orm.collections import InstrumentedList
from marshmallow import Schema, post_dump, pre_load, pre_dump
from inflection import camelize, underscore
class LemurSchema(Schema):
"""
Base schema from which all grouper schema's inherit
"""
__envelope__ = True
def under(self, data, many=None):
items = []
if many:
for i in data:
items.append(
{underscore(key): value for key, value in i.items()}
)
return items
return {
underscore(key): value
for key, value in data.items()
}
def camel(self, data, many=None):
items = []
if many:
for i in data:
items.append(
{camelize(key, uppercase_first_letter=False): value for key, value in i.items()}
)
return items
return {
camelize(key, uppercase_first_letter=False): value
for key, value in data.items()
}
def wrap_with_envelope(self, data, many):
if many:
if 'total' in self.context.keys():
return dict(total=self.context['total'], items=data)
return data
class LemurInputSchema(LemurSchema):
@pre_load(pass_many=True)
def preprocess(self, data, many):
return self.under(data, many=many)
class LemurOutputSchema(LemurSchema):
@pre_load(pass_many=True)
def preprocess(self, data, many):
if many:
data = self.unwrap_envelope(data, many)
return self.under(data, many=many)
@pre_dump(pass_many=True)
def unwrap_envelope(self, data, many):
if many:
if data:
if isinstance(data, InstrumentedList) or isinstance(data, list):
self.context['total'] = len(data)
return data
else:
self.context['total'] = data['total']
else:
self.context['total'] = 0
data = {'items': []}
return data['items']
return data
@post_dump(pass_many=True)
def post_process(self, data, many):
if data:
data = self.camel(data, many=many)
if self.__envelope__:
return self.wrap_with_envelope(data, many=many)
else:
return data
def format_errors(messages):
errors = {}
for k, v in messages.items():
key = camelize(k, uppercase_first_letter=False)
if isinstance(v, dict):
errors[key] = format_errors(v)
elif isinstance(v, list):
errors[key] = v[0]
return errors
def wrap_errors(messages):
errors = dict(message='Validation Error.')
if messages.get('_schema'):
errors['reasons'] = {'Schema': {'rule': messages['_schema']}}
else:
errors['reasons'] = format_errors(messages)
return errors
def validate_schema(input_schema, output_schema):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if input_schema:
if request.get_json():
request_data = request.get_json()
else:
request_data = request.args
data, errors = input_schema.load(request_data)
if errors:
return wrap_errors(errors), 400
kwargs['data'] = data
try:
resp = f(*args, **kwargs)
except Exception as e:
current_app.logger.exception(e)
return dict(message=e.message), 500
if isinstance(resp, tuple):
return resp[0], resp[1]
if not resp:
return dict(message="No data found"), 404
if output_schema:
data = output_schema.dump(resp)
return data.data, 200
return resp, 200
return decorated_function
return decorator

View File

@ -63,9 +63,9 @@ class marshal_items(object):
if hasattr(e, 'data'):
return {'message': e.data['message']}, 400
else:
return {'message': 'unknown'}, 400
return {'message': {'exception': 'unknown'}}, 400
else:
return {'message': str(e)}, 400
return {'message': {'exception': str(e)}}, 400
return wrapper

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

@ -0,0 +1,120 @@
import arrow
from marshmallow.exceptions import ValidationError
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from lemur.domains import service as domain_service
from lemur.auth.permissions import SensitiveDomainPermission
def public_certificate(body):
"""
Determines if specified string is valid public certificate.
:param body:
:return:
"""
try:
x509.load_pem_x509_certificate(bytes(body), default_backend())
except Exception:
raise ValidationError('Public certificate presented is not valid.')
def private_key(key):
"""
User to validate that a given string is a RSA private key
:param key:
:return: :raise ValueError:
"""
try:
serialization.load_pem_private_key(bytes(key), None, backend=default_backend())
except Exception:
raise ValidationError('Private key presented is not valid.')
def sensitive_domain(domain):
"""
Determines if domain has been marked as sensitive.
:param domain:
:return:
"""
domains = domain_service.get_by_name(domain)
for domain in domains:
# we only care about non-admins
if not SensitiveDomainPermission().can():
if domain.sensitive:
raise ValidationError(
'Domain {0} has been marked as sensitive, contact and administrator \
to issue the certificate.'.format(domain))
def oid_type(oid_type):
"""
Determines if the specified oid type is valid.
:param oid_type:
:return:
"""
valid_types = ['b64asn1', 'string', 'ia5string']
if oid_type.lower() not in [o_type.lower() for o_type in valid_types]:
raise ValidationError('Invalid Oid Type: {0} choose from {1}'.format(oid_type, ",".join(valid_types)))
def sub_alt_type(alt_type):
"""
Determines if the specified subject alternate type is valid.
:param alt_type:
:return:
"""
valid_types = ['DNSName', 'IPAddress', 'uniFormResourceIdentifier', 'directoryName', 'rfc822Name', 'registrationID',
'otherName', 'x400Address', 'EDIPartyName']
if alt_type.lower() not in [a_type.lower() for a_type in valid_types]:
raise ValidationError('Invalid SubAltName Type: {0} choose from {1}'.format(type, ",".join(valid_types)))
def csr(data):
"""
Determines if the CSR is valid.
:param data:
:return:
"""
try:
x509.load_pem_x509_csr(bytes(data), default_backend())
except Exception:
raise ValidationError('CSR presented is not valid.')
def dates(data):
if not data.get('validity_start') and data.get('validity_end'):
raise ValidationError('If validity start is specified so must validity end.')
if not data.get('validity_end') and data.get('validity_start'):
raise ValidationError('If validity end is specified so must validity start.')
if data.get('validity_end') and data.get('validity_years'):
raise ValidationError('Cannot specify both validity end and validity years.')
if data.get('validity_start') and data.get('validity_end'):
if not data['validity_start'] < data['validity_end']:
raise ValidationError('Validity start must be before validity end.')
if data.get('authority'):
if data.get('validity_start').replace(hour=0, minute=0, second=0, tzinfo=None) < data['authority'].authority_certificate.not_before.replace(hour=0, minute=0, second=0):
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
if data.get('validity_end').replace(hour=0, minute=0, second=0, tzinfo=None) > data['authority'].authority_certificate.not_after.replace(hour=0, minute=0, second=0):
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
if data.get('validity_years'):
now = arrow.utcnow()
end = now.replace(years=+data['validity_years'])
if data.get('authority'):
if now.naive < data['authority'].authority_certificate.not_before:
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
if end.naive > data['authority'].authority_certificate.not_after:
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))

View File

@ -9,10 +9,11 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import current_app
from sqlalchemy import exc
from sqlalchemy.sql import and_, or_
from sqlalchemy.orm import make_transient
from sqlalchemy.orm.exc import NoResultFound
from lemur.extensions import db
from lemur.exceptions import AttrNotFound, DuplicateError
@ -126,8 +127,7 @@ def get(model, value, field="id"):
query = session_query(model)
try:
return query.filter(getattr(model, field) == value).one()
except Exception as e:
current_app.logger.exception(e)
except NoResultFound as e:
return
@ -239,9 +239,6 @@ def update_list(model, model_attr, item_model, items):
"""
ids = []
for i in items:
ids.append(i['id'])
for i in getattr(model, model_attr):
if i.id not in ids:
getattr(model, model_attr).remove(i)
@ -256,6 +253,18 @@ def update_list(model, model_attr, item_model, items):
return model
def clone(model):
"""
Clones the given model and removes it's primary key
:param model:
:return:
"""
db.session.expunge(model)
make_transient(model)
model.id = None
return model
def sort_and_page(query, model, args):
"""
Helper that allows us to combine sorting and paging
@ -275,4 +284,9 @@ def sort_and_page(query, model, args):
if sort_by and sort_dir:
query = sort(query, model, sort_by, sort_dir)
return paginate(query, page, count)
total = query.count()
# offset calculated at zero
page -= 1
items = query.offset(count * page).limit(count).all()
return dict(items=items, total=total)

63
lemur/defaults/views.py Normal file
View File

@ -0,0 +1,63 @@
"""
.. module: lemur.status.views
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from flask import current_app, Blueprint
from flask.ext.restful import Api
from lemur.auth.service import AuthenticatedResource
mod = Blueprint('default', __name__)
api = Api(mod)
class LemurDefaults(AuthenticatedResource):
""" Defines the 'defaults' endpoint """
def __init__(self):
super(LemurDefaults)
def get(self):
"""
.. http:get:: /defaults
Returns defaults needed to generate CSRs
**Example request**:
.. sourcecode:: http
GET /defaults HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"country": "US",
"state": "CA",
"location": "Los Gatos",
"organization": "Netflix",
"organizationalUnit": "Operations"
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return dict(
country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
state=current_app.config.get('LEMUR_DEFAULT_STATE'),
location=current_app.config.get('LEMUR_DEFAULT_LOCATION'),
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT')
)
api.add_resource(LemurDefaults, '/defaults', endpoint='default')

View File

@ -0,0 +1,42 @@
"""
.. module: lemur.destinations.schemas
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from marshmallow import fields, post_dump
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.schemas import PluginInputSchema, PluginOutputSchema
class DestinationInputSchema(LemurInputSchema):
id = fields.Integer()
label = fields.String(required=True)
description = fields.String(required=True)
active = fields.Boolean()
plugin = fields.Nested(PluginInputSchema, required=True)
class DestinationOutputSchema(LemurOutputSchema):
id = fields.Integer()
label = fields.String()
description = fields.String()
active = fields.Boolean()
plugin = fields.Nested(PluginOutputSchema)
options = fields.List(fields.Dict())
@post_dump
def fill_object(self, data):
data['plugin']['pluginOptions'] = data['options']
return data
class DestinationNestedOutputSchema(DestinationOutputSchema):
__envelope__ = False
destination_input_schema = DestinationInputSchema()
destinations_output_schema = DestinationOutputSchema(many=True)
destination_output_schema = DestinationOutputSchema()

View File

@ -8,6 +8,7 @@
from sqlalchemy import func
from lemur import database
from lemur.models import certificate_destination_associations
from lemur.destinations.models import Destination
from lemur.certificates.models import Certificate
@ -85,10 +86,6 @@ def get_all():
def render(args):
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
@ -102,12 +99,7 @@ def render(args):
terms = filt.split(';')
query = database.filter(query, Destination, terms)
query = database.find_all(query, Destination, args)
if sort_by and sort_dir:
query = database.sort(query, Destination, sort_by, sort_dir)
return database.paginate(query, page, count)
return database.sort_and_page(query, Destination, args)
def stats(**kwargs):
@ -117,10 +109,9 @@ def stats(**kwargs):
:param kwargs:
:return:
"""
attr = getattr(Destination, kwargs.get('metric'))
query = database.db.session.query(attr, func.count(attr))
items = query.group_by(attr).all()
items = database.db.session.query(Destination.label, func.count(certificate_destination_associations.c.certificate_id))\
.join(certificate_destination_associations)\
.group_by(Destination.label).all()
keys = []
values = []

View File

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

View File

@ -7,7 +7,7 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import Column, Integer, String
from sqlalchemy import Column, Integer, String, Boolean
from lemur.database import db
@ -16,11 +16,4 @@ class Domain(db.Model):
__tablename__ = 'domains'
id = Column(Integer, primary_key=True)
name = Column(String(256))
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
def serialize(self):
blob = self.as_dict()
blob['certificates'] = [x.id for x in self.certificate]
return blob
sensitive = Column(Boolean, default=False)

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

@ -0,0 +1,35 @@
"""
.. module: lemur.domains.schemas
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from marshmallow import fields
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.schemas import AssociatedCertificateSchema
# from lemur.certificates.schemas import CertificateNestedOutputSchema
class DomainInputSchema(LemurInputSchema):
id = fields.Integer()
name = fields.String(required=True)
sensitive = fields.Boolean()
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
class DomainOutputSchema(LemurOutputSchema):
id = fields.Integer()
name = fields.String()
sensitive = fields.Boolean()
# certificates = fields.Nested(CertificateNestedOutputSchema, many=True, missing=[])
class DomainNestedOutputSchema(DomainOutputSchema):
__envelope__ = False
domain_input_schema = DomainInputSchema()
domain_output_schema = DomainOutputSchema()
domains_output_schema = DomainOutputSchema(many=True)

View File

@ -32,6 +32,43 @@ def get_all():
return database.find_all(query, Domain, {}).all()
def get_by_name(name):
"""
Fetches domain by it's name
:param name:
:return:
"""
return database.get_all(Domain, name, field="name").all()
def create(name, sensitive):
"""
Create a new domain
:param name:
:param sensitive:
:return:
"""
domain = Domain(name=name, sensitive=sensitive)
return database.create(domain)
def update(domain_id, name, sensitive):
"""
Update an existing domain
:param domain_id:
:param name:
:param sensitive:
:return:
"""
domain = get(domain_id)
domain.name = name
domain.sensitive = sensitive
database.update(domain)
def render(args):
"""
Helper to parse REST Api requests
@ -40,11 +77,6 @@ def render(args):
:return:
"""
query = database.session_query(Domain).join(Certificate, Domain.certificate)
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
@ -55,9 +87,4 @@ def render(args):
if certificate_id:
query = query.filter(Certificate.id == certificate_id)
query = database.find_all(query, Domain, args)
if sort_by and sort_dir:
query = database.sort(query, Domain, sort_by, sort_dir)
return database.paginate(query, page, count)
return database.sort_and_page(query, Domain, args)

View File

@ -8,17 +8,16 @@
"""
from flask import Blueprint
from flask.ext.restful import reqparse, Api, fields
from flask.ext.restful import reqparse, Api
from lemur.domains import service
from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import SensitiveDomainPermission
from lemur.common.utils import paginated_parser, marshal_items
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
FIELDS = {
'id': fields.Integer,
'name': fields.String
}
from lemur.domains.schemas import domain_input_schema, domain_output_schema, domains_output_schema
mod = Blueprint('domains', __name__)
api = Api(mod)
@ -29,7 +28,7 @@ class DomainsList(AuthenticatedResource):
def __init__(self):
super(DomainsList, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, domains_output_schema)
def get(self):
"""
.. http:get:: /domains
@ -57,10 +56,12 @@ class DomainsList(AuthenticatedResource):
{
"id": 1,
"name": "www.example.com",
"sensitive": false
},
{
"id": 2,
"name": "www.example2.com",
"sensitive": false
}
]
"total": 2
@ -68,9 +69,9 @@ class DomainsList(AuthenticatedResource):
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
@ -79,13 +80,58 @@ class DomainsList(AuthenticatedResource):
args = parser.parse_args()
return service.render(args)
@validate_schema(domain_input_schema, domain_output_schema)
def post(self, data=None):
"""
.. http:post:: /domains
The current domain list
**Example request**:
.. sourcecode:: http
GET /domains HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"name": "www.example.com",
"sensitive": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"name": "www.example.com",
"sensitive": false
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
return service.create(data['name'], data['sensitive'])
class Domains(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Domains, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, domain_output_schema)
def get(self, domain_id):
"""
.. http:get:: /domains/1
@ -111,6 +157,7 @@ class Domains(AuthenticatedResource):
{
"id": 1,
"name": "www.example.com",
"sensitive": false
}
:reqheader Authorization: OAuth token to authenticate
@ -119,13 +166,56 @@ class Domains(AuthenticatedResource):
"""
return service.get(domain_id)
@validate_schema(domain_input_schema, domain_output_schema)
def put(self, domain_id, data=None):
"""
.. http:get:: /domains/1
update one domain
**Example request**:
.. sourcecode:: http
GET /domains HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"name": "www.example.com",
"sensitive": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"name": "www.example.com",
"sensitive": false
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
if SensitiveDomainPermission().can():
return service.update(domain_id, data['name'], data['sensitive'])
return dict(message='You are not authorized to modify this domain'), 403
class CertificateDomains(AuthenticatedResource):
""" Defines the 'domains' endpoint """
def __init__(self):
super(CertificateDomains, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, domains_output_schema)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/domains
@ -153,10 +243,12 @@ class CertificateDomains(AuthenticatedResource):
{
"id": 1,
"name": "www.example.com",
"sensitive": false
},
{
"id": 2,
"name": "www.example2.com",
"sensitive": false
}
]
"total": 2
@ -164,9 +256,9 @@ class CertificateDomains(AuthenticatedResource):
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated

View File

@ -3,17 +3,20 @@
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from flask.ext.sqlalchemy import SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
from flask.ext.migrate import Migrate
from flask_migrate import Migrate
migrate = Migrate()
from flask.ext.bcrypt import Bcrypt
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt()
from flask.ext.principal import Principal
from flask_principal import Principal
principal = Principal()
from flask_mail import Mail
smtp_mail = Mail()
from lemur.metrics import Metrics
metrics = Metrics()

View File

@ -19,7 +19,7 @@ from logging.handlers import RotatingFileHandler
from flask import Flask
from lemur.common.health import mod as health
from lemur.extensions import db, migrate, principal, smtp_mail
from lemur.extensions import db, migrate, principal, smtp_mail, metrics
DEFAULT_BLUEPRINTS = (
@ -112,6 +112,7 @@ def configure_extensions(app):
migrate.init_app(app, db)
principal.init_app(app)
smtp_mail.init_app(app)
metrics.init_app(app)
def configure_blueprints(app, blueprints):

View File

@ -1,7 +1,12 @@
from __future__ import unicode_literals # at top of module
import os
import sys
import base64
import time
import arrow
import requests
import json
from gunicorn.config import make_settings
from cryptography.fernet import Fernet
@ -20,8 +25,12 @@ from lemur.certificates import service as cert_service
from lemur.sources import service as source_service
from lemur.notifications import service as notification_service
from lemur.certificates.service import get_name_from_arn
from lemur.certificates.verify import verify_string
from lemur.sources.service import sync
from lemur.plugins.lemur_aws import elb
from lemur.sources.service import sync as source_sync
from lemur import create_app
@ -68,7 +77,7 @@ SECRET_KEY = '{flask_secret_key}'
# You should consider storing these separately from your config
LEMUR_TOKEN_SECRET = '{secret_token}'
LEMUR_ENCRYPTION_KEY = '{encryption_key}'
LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
# this is a list of domains as regexes that only admins can issue
LEMUR_RESTRICTED_DOMAINS = []
@ -77,7 +86,17 @@ LEMUR_RESTRICTED_DOMAINS = []
LEMUR_EMAIL = ''
LEMUR_SECURITY_TEAM_EMAIL = []
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
# Certificate Defaults
LEMUR_DEFAULT_COUNTRY = ''
LEMUR_DEFAULT_STATE = ''
LEMUR_DEFAULT_LOCATION = ''
LEMUR_DEFAULT_ORGANIZATION = ''
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
# Authentication Providers
ACTIVE_PROVIDERS = []
# Logging
@ -100,13 +119,6 @@ SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
# These will be dependent on which 3rd party that Lemur is
# configured to use.
# CLOUDCA_URL = ''
# CLOUDCA_PEM_PATH = ''
# CLOUDCA_BUNDLE = ''
# number of years to issue if not specified
# CLOUDCA_DEFAULT_VALIDITY = 2
# VERISIGN_URL = ''
# VERISIGN_PEM_PATH = ''
# VERISIGN_FIRST_NAME = ''
@ -136,12 +148,15 @@ def check_revoked():
as `unknown`.
"""
for cert in cert_service.get_all_certs():
if cert.chain:
status = verify_string(cert.body, cert.chain)
else:
status = verify_string(cert.body, "")
try:
if cert.chain:
status = verify_string(cert.body, cert.chain)
else:
status = verify_string(cert.body, "")
cert.status = 'valid' if status else "invalid"
cert.status = 'valid' if status else 'invalid'
except Exception as e:
cert.status = 'unknown'
database.update(cert)
@ -163,7 +178,9 @@ def generate_settings():
settings file.
"""
output = CONFIG_TEMPLATE.format(
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)),
# we use Fernet.generate_key to make sure that the key length is
# compatible with Fernet
encryption_key=Fernet.generate_key(),
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
)
@ -171,19 +188,18 @@ def generate_settings():
return output
@manager.option('-s', '--sources', dest='labels', default='', required=False)
@manager.option('-l', '--list', dest='view', default=False, required=False)
def sync_sources(labels, view):
@manager.option('-s', '--sources', dest='labels')
def sync(labels):
"""
Attempts to run several methods Certificate discovery. This is
run on a periodic basis and updates the Lemur datastore with the
information it discovers.
"""
if view:
if not labels:
sys.stdout.write("Active\tLabel\tDescription\n")
for source in source_service.get_all():
sys.stdout.write(
"[{active}]\t{label}\t{description}!\n".format(
"{active}\t{label}\t{description}!\n".format(
label=source.label,
description=source.description,
active=source.active
@ -198,13 +214,14 @@ def sync_sources(labels, view):
try:
sync_lock.acquire(timeout=10) # wait up to 10 seconds
if labels:
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
labels = labels.split(",")
else:
sys.stdout.write("[+] Starting to sync ALL sources!\n")
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
labels = labels.split(",")
if labels[0] == 'all':
source_sync()
else:
source_sync(labels=labels)
sync(labels=labels)
sys.stdout.write(
"[+] Finished syncing sources. Run Time: {time}\n".format(
time=(time.time() - start_time)
@ -248,18 +265,23 @@ class InitializeApp(Command):
Additionally a Lemur user will be created as a default user
and be used when certificates are discovered by Lemur.
"""
def run(self):
option_list = (
Option('-p', '--password', dest='password'),
)
def run(self, password):
create()
user = user_service.get_by_username("lemur")
if not user:
sys.stdout.write("We need to set Lemur's password to continue!\n")
password1 = prompt_pass("Password")
password2 = prompt_pass("Confirm Password")
if not password:
sys.stdout.write("We need to set Lemur's password to continue!\n")
password = prompt_pass("Password")
password1 = prompt_pass("Confirm Password")
if password1 != password2:
sys.stderr.write("[!] Passwords do not match!\n")
sys.exit(1)
if password != password1:
sys.stderr.write("[!] Passwords do not match!\n")
sys.exit(1)
role = role_service.get_by_name('admin')
@ -270,16 +292,16 @@ class InitializeApp(Command):
role = role_service.create('admin', description='this is the lemur administrator role')
sys.stdout.write("[+] Created 'admin' role\n")
user_service.create("lemur", password1, 'lemur@nobody', True, None, [role])
user_service.create("lemur", password, 'lemur@nobody', True, None, [role])
sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n")
else:
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
sys.stdout.write("[+] Creating expiration email notifications!\n")
sys.stdout.write("[!] Using {recipients} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n")
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
sys.stdout.write(
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
num=len(intervals),
@ -295,13 +317,13 @@ class InitializeApp(Command):
class CreateUser(Command):
"""
This command allows for the creation of a new user within Lemur
This command allows for the creation of a new user within Lemur.
"""
option_list = (
Option('-u', '--username', dest='username', required=True),
Option('-e', '--email', dest='email', required=True),
Option('-a', '--active', dest='active', default=True),
Option('-r', '--roles', dest='roles', default=[])
Option('-r', '--roles', dest='roles', action='append', default=[])
)
def run(self, username, email, active, roles):
@ -311,18 +333,46 @@ class CreateUser(Command):
if role_obj:
role_objs.append(role_obj)
else:
sys.stderr.write("[!] Cannot find role {0}".format(r))
sys.stderr.write("[!] Cannot find role {0}\n".format(r))
sys.exit(1)
password1 = prompt_pass("Password")
password2 = prompt_pass("Confirm Password")
if password1 != password2:
sys.stderr.write("[!] Passwords do not match")
sys.stderr.write("[!] Passwords do not match!\n")
sys.exit(1)
user_service.create(username, password1, email, active, None, role_objs)
sys.stdout.write("[+] Created new user: {0}".format(username))
sys.stdout.write("[+] Created new user: {0}\n".format(username))
class ResetPassword(Command):
"""
This command allows you to reset a user's password.
"""
option_list = (
Option('-u', '--username', dest='username', required=True),
)
def run(self, username):
user = user_service.get_by_username(username)
if not user:
sys.stderr.write("[!] No user found for username: {0}\n".format(username))
sys.exit(1)
sys.stderr.write("[+] Resetting password for {0}\n".format(username))
password1 = prompt_pass("Password")
password2 = prompt_pass("Confirm Password")
if password1 != password2:
sys.stderr.write("[!] Passwords do not match\n")
sys.exit(1)
user.password = password1
user.hash_password()
database.commit()
class CreateRole(Command):
@ -369,7 +419,7 @@ class LemurServer(Command):
settings = make_settings()
options = (
Option(*klass.cli, action=klass.action)
for setting, klass in settings.iteritems() if klass.cli
for setting, klass in settings.items() if klass.cli
)
return options
@ -480,6 +530,337 @@ def unlock(path=None):
sys.stdout.write("[+] Keys have been unencrypted!\n")
def unicode_(data):
import sys
if sys.version_info.major < 3:
return data.decode('UTF-8')
return data
class RotateELBs(Command):
"""
Rotates existing certificates to a new one on an ELB
"""
option_list = (
Option('-e', '--elb-list', dest='elb_list', required=True),
Option('-p', '--chain-path', dest='chain_path'),
Option('-c', '--cert-name', dest='cert_name'),
Option('-a', '--cert-prefix', dest='cert_prefix'),
Option('-d', '--description', dest='description')
)
def run(self, elb_list, chain_path, cert_name, cert_prefix, description):
for e in open(elb_list, 'r').readlines():
elb_name, account_id, region, from_port, to_port, protocol = e.strip().split(',')
if cert_name:
arn = "arn:aws:iam::{0}:server-certificate/{1}".format(account_id, cert_name)
else:
# if no cert name is provided we need to discover it
listeners = elb.get_listeners(account_id, region, elb_name)
# get the listener we care about
for listener in listeners:
if listener[0] == int(from_port) and listener[1] == int(to_port):
arn = listener[4]
name = get_name_from_arn(arn)
certificate = cert_service.get_by_name(name)
break
else:
sys.stdout.write("[-] Could not find ELB {0}".format(elb_name))
continue
if not certificate:
sys.stdout.write("[-] Could not find certificate {0} in Lemur".format(name))
continue
dests = []
for d in certificate.destinations:
dests.append({'id': d.id})
nots = []
for n in certificate.notifications:
nots.append({'id': n.id})
new_certificate = database.clone(certificate)
if cert_prefix:
new_certificate.name = "{0}-{1}".format(cert_prefix, new_certificate.name)
new_certificate.chain = open(chain_path, 'r').read()
new_certificate.description = "{0} - {1}".format(new_certificate.description, description)
new_certificate = database.create(new_certificate)
database.update_list(new_certificate, 'destinations', Destination, dests)
database.update_list(new_certificate, 'notifications', Notification, nots)
database.update(new_certificate)
arn = new_certificate.get_arn(account_id)
elb.update_listeners(account_id, region, elb_name, [(from_port, to_port, protocol, arn)], [from_port])
sys.stdout.write("[+] Updated {0} to use {1}\n".format(elb_name, new_certificate.name))
class ProvisionELB(Command):
"""
Creates and provisions a certificate on an ELB based on command line arguments
"""
option_list = (
Option('-d', '--dns', dest='dns', action='append', required=True, type=unicode_),
Option('-e', '--elb', dest='elb_name', required=True, type=unicode_),
Option('-o', '--owner', dest='owner', type=unicode_),
Option('-a', '--authority', dest='authority', required=True, type=unicode_),
Option('-s', '--description', dest='description', default=u'Command line provisioned keypair', type=unicode_),
Option('-t', '--destination', dest='destinations', action='append', type=unicode_, required=True),
Option('-n', '--notification', dest='notifications', action='append', type=unicode_, default=[]),
Option('-r', '--region', dest='region', default=u'us-east-1', type=unicode_),
Option('-p', '--dport', '--port', dest='dport', default=7002),
Option('--src-port', '--source-port', '--sport', dest='sport', default=443),
Option('--dry-run', dest='dryrun', action='store_true')
)
def configure_user(self, owner):
from flask import g
import lemur.users.service
# grab the user
g.user = lemur.users.service.get_by_username(owner)
# get the first user by default
if not g.user:
g.user = lemur.users.service.get_all()[0]
return g.user.username
def build_cert_options(self, destinations, notifications, description, owner, dns, authority):
from sqlalchemy.orm.exc import NoResultFound
from lemur.certificates.views import valid_authority
import sys
# convert argument lists to arrays, or empty sets
destinations = self.get_destinations(destinations)
if not destinations:
sys.stderr.write("Valid destinations provided\n")
sys.exit(1)
# get the primary CN
common_name = dns[0]
# If there are more than one fqdn, add them as alternate names
extensions = {}
if len(dns) > 1:
extensions['subAltNames'] = {'names': map(lambda x: {'nameType': 'DNSName', 'value': x}, dns)}
try:
authority = valid_authority({"name": authority})
except NoResultFound:
sys.stderr.write("Invalid authority specified: '{}'\naborting\n".format(authority))
sys.exit(1)
options = {
# Convert from the Destination model to the JSON input expected further in the code
'destinations': map(lambda x: {'id': x.id, 'label': x.label}, destinations),
'description': description,
'notifications': notifications,
'commonName': common_name,
'extensions': extensions,
'authority': authority,
'owner': owner,
# defaults:
'organization': current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
'organizationalUnit': current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
'country': current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
'state': current_app.config.get('LEMUR_DEFAULT_STATE'),
'location': current_app.config.get('LEMUR_DEFAULT_LOCATION')
}
return options
def get_destinations(self, destination_names):
from lemur.destinations import service
destinations = []
for destination_name in destination_names:
destination = service.get_by_label(destination_name)
if not destination:
sys.stderr.write("Invalid destination specified: '{}'\nAborting...\n".format(destination_name))
sys.exit(1)
destinations.append(service.get_by_label(destination_name))
return destinations
def check_duplicate_listener(self, elb_name, region, account, sport, dport):
from lemur.plugins.lemur_aws import elb
listeners = elb.get_listeners(account, region, elb_name)
for listener in listeners:
if listener[0] == sport and listener[1] == dport:
return True
return False
def get_destination_account(self, destinations):
for destination in self.get_destinations(destinations):
if destination.plugin_name == 'aws-destination':
account_number = destination.plugin.get_option('accountNumber', destination.options)
return account_number
sys.stderr.write("No destination AWS account provided, failing\n")
sys.exit(1)
def run(self, dns, elb_name, owner, authority, description, notifications, destinations, region, dport, sport,
dryrun):
from lemur.certificates import service
from lemur.plugins.lemur_aws import elb
from boto.exception import BotoServerError
# configure the owner if we can find it, or go for default, and put it in the global
owner = self.configure_user(owner)
# make a config blob from the command line arguments
cert_options = self.build_cert_options(
destinations=destinations,
notifications=notifications,
description=description,
owner=owner,
dns=dns,
authority=authority)
aws_account = self.get_destination_account(destinations)
if dryrun:
import json
cert_options['authority'] = cert_options['authority'].name
sys.stdout.write('Will create certificate using options: {}\n'
.format(json.dumps(cert_options, sort_keys=True, indent=2)))
sys.stdout.write('Will create listener {}->{} HTTPS using the new certificate to elb {}\n'
.format(sport, dport, elb_name))
sys.exit(0)
if self.check_duplicate_listener(elb_name, region, aws_account, sport, dport):
sys.stderr.write("ELB {} already has a listener {}->{}\nAborting...\n".format(elb_name, sport, dport))
sys.exit(1)
# create the certificate
try:
sys.stdout.write('Creating certificate for {}\n'.format(cert_options['commonName']))
cert = service.create(**cert_options)
except Exception as e:
if e.message == 'Duplicate certificate: a certificate with the same common name exists already':
sys.stderr.write("Certificate already exists named: {}\n".format(dns[0]))
sys.exit(1)
raise e
cert_arn = cert.get_arn(aws_account)
sys.stderr.write('cert arn: {}\n'.format(cert_arn))
sys.stderr.write('Configuring elb {} from port {} to port {} in region {} with cert {}\n'
.format(elb_name, sport, dport, region, cert_arn))
delay = 1
done = False
retries = 5
while not done and retries > 0:
try:
elb.create_new_listeners(aws_account, region, elb_name, [(sport, dport, 'HTTPS', cert_arn)])
except BotoServerError as bse:
# if the server returns ad error, the certificate
if bse.error_code == 'CertificateNotFound':
sys.stderr.write('Certificate not available yet in the AWS account, waiting {}, {} retries left\n'
.format(delay, retries))
time.sleep(delay)
delay *= 2
retries -= 1
elif bse.error_code == 'DuplicateListener':
sys.stderr.write('ELB {} already has a listener {}->{}'.format(elb_name, sport, dport))
sys.exit(1)
else:
raise bse
else:
done = True
@manager.command
def publish_verisign_units():
"""
Simple function that queries verisign for API units and posts the mertics to
Atlas API for other teams to consume.
:return:
"""
from lemur.plugins import plugins
v = plugins.get('verisign-issuer')
units = v.get_available_units()
metrics = {}
for item in units:
if item['@type'] in metrics.keys():
metrics[item['@type']] += int(item['@remaining'])
else:
metrics.update({item['@type']: int(item['@remaining'])})
for name, value in metrics.items():
metric = [
{
"timestamp": 1321351651,
"type": "GAUGE",
"name": "Symantec {0} Unit Count".format(name),
"tags": {},
"value": value
}
]
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
class Rolling(Command):
"""
Rotates existing certificates to a new one on an ELB
"""
option_list = (
Option('-w', '--window', dest='window', default=24),
)
def run(self, window):
"""
Simple function that queries verisign for API units and posts the mertics to
Atlas API for other teams to consume.
:return:
"""
end = arrow.utcnow()
start = end.replace(hours=-window)
items = Certificate.query.filter(Certificate.not_before <= end.format('YYYY-MM-DD')) \
.filter(Certificate.not_before >= start.format('YYYY-MM-DD')).all()
metrics = {}
for i in items:
name = "{0},{1}".format(i.owner, i.issuer)
if metrics.get(name):
metrics[name] += 1
else:
metrics[name] = 1
for name, value in metrics.iteritems():
owner, issuer = name.split(",")
metric = [
{
"timestamp": 1321351651,
"type": "GAUGE",
"name": "Issued Certificates",
"tags": {"owner": owner, "issuer": issuer, "window": window},
"value": value
}
]
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
def main():
manager.add_command("start", LemurServer())
manager.add_command("runserver", Server(host='127.0.0.1'))
@ -488,9 +869,12 @@ def main():
manager.add_command("db", MigrateCommand)
manager.add_command("init", InitializeApp())
manager.add_command("create_user", CreateUser())
manager.add_command("reset_password", ResetPassword())
manager.add_command("create_role", CreateRole())
manager.add_command("provision_elb", ProvisionELB())
manager.add_command("rotate_elbs", RotateELBs())
manager.add_command("rolling", Rolling())
manager.run()
if __name__ == "__main__":
main()

32
lemur/metrics.py Normal file
View File

@ -0,0 +1,32 @@
"""
.. module: lemur.metrics
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from flask import current_app
from lemur.plugins.base import plugins
class Metrics(object):
"""
:param app: The Flask application object. Defaults to None.
"""
_providers = []
def __init__(self, app=None):
if app is not None:
self.init_app(app)
def init_app(self, app):
"""Initializes the application with the extension.
:param app: The Flask application object.
"""
self._providers = app.config.get('METRIC_PROVIDERS', [])
def send(self, metric_name, metric_type, metric_value, *args, **kwargs):
for provider in self._providers:
current_app.logger.debug(
"Sending metric '{metric}' to the {provider} provider.".format(metric=metric_name, provider=provider))
p = plugins.get(provider)
p.submit(metric_name, metric_type, metric_value, *args, **kwargs)

View File

@ -1,42 +0,0 @@
"""Adding in models for certificate sources
Revision ID: 1ff763f5b80b
Revises: 4dc5ddd111b8
Create Date: 2015-08-01 15:24:20.412725
"""
# revision identifiers, used by Alembic.
revision = '1ff763f5b80b'
down_revision = '4dc5ddd111b8'
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('sources',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('label', sa.String(length=32), nullable=True),
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('plugin_name', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('certificate_source_associations',
sa.Column('source_id', sa.Integer(), nullable=True),
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['source_id'], ['destinations.id'], ondelete='cascade')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_source_associations')
op.drop_table('sources')
### end Alembic commands ###

View File

@ -0,0 +1,131 @@
"""
Refactor authority columns and associates an authorities root certificate with a certificate stored in the
certificate tables.
Migrates existing authority owners to associated roles.
Migrates existing certificate owners to associated role.
Revision ID: 3307381f3b88
Revises: 412b22cb656a
Create Date: 2016-05-20 17:33:04.360687
"""
# revision identifiers, used by Alembic.
revision = '3307381f3b88'
down_revision = '412b22cb656a'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
from sqlalchemy.dialects import postgresql
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.alter_column('authorities', 'owner',
existing_type=sa.VARCHAR(length=128),
nullable=True)
op.drop_column('authorities', 'not_after')
op.drop_column('authorities', 'bits')
op.drop_column('authorities', 'cn')
op.drop_column('authorities', 'not_before')
op.add_column('certificates', sa.Column('root_authority_id', sa.Integer(), nullable=True))
op.alter_column('certificates', 'body',
existing_type=sa.TEXT(),
nullable=False)
op.alter_column('certificates', 'owner',
existing_type=sa.VARCHAR(length=128),
nullable=True)
op.drop_constraint(u'certificates_authority_id_fkey', 'certificates', type_='foreignkey')
op.create_foreign_key(None, 'certificates', 'authorities', ['authority_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'certificates', 'authorities', ['root_authority_id'], ['id'], ondelete='CASCADE')
### end Alembic commands ###
# link existing certificate to their authority certificates
conn = op.get_bind()
for id, body, owner in conn.execute(text('select id, body, owner from authorities')):
if not owner:
owner = "lemur@nobody"
# look up certificate by body, if duplications are found, pick one
stmt = text('select id from certificates where body=:body')
stmt = stmt.bindparams(body=body)
root_certificate = conn.execute(stmt).fetchone()
if root_certificate:
stmt = text('update certificates set root_authority_id=:root_authority_id where id=:id')
stmt = stmt.bindparams(root_authority_id=id, id=root_certificate[0])
op.execute(stmt)
# link owner roles to their authorities
stmt = text('select id from roles where name=:name')
stmt = stmt.bindparams(name=owner)
owner_role = conn.execute(stmt).fetchone()
if not owner_role:
stmt = text('insert into roles (name, description) values (:name, :description)')
stmt = stmt.bindparams(name=owner, description='Lemur generated role or existing owner.')
op.execute(stmt)
stmt = text('select id from roles where name=:name')
stmt = stmt.bindparams(name=owner)
owner_role = conn.execute(stmt).fetchone()
stmt = text('select * from roles_authorities where role_id=:role_id and authority_id=:authority_id')
stmt = stmt.bindparams(role_id=owner_role[0], authority_id=id)
exists = conn.execute(stmt).fetchone()
if not exists:
stmt = text('insert into roles_authorities (role_id, authority_id) values (:role_id, :authority_id)')
stmt = stmt.bindparams(role_id=owner_role[0], authority_id=id)
op.execute(stmt)
# link owner roles to their certificates
for id, owner in conn.execute(text('select id, owner from certificates')):
if not owner:
owner = "lemur@nobody"
stmt = text('select id from roles where name=:name')
stmt = stmt.bindparams(name=owner)
owner_role = conn.execute(stmt).fetchone()
if not owner_role:
stmt = text('insert into roles (name, description) values (:name, :description)')
stmt = stmt.bindparams(name=owner, description='Lemur generated role or existing owner.')
op.execute(stmt)
# link owner roles to their authorities
stmt = text('select id from roles where name=:name')
stmt = stmt.bindparams(name=owner)
owner_role = conn.execute(stmt).fetchone()
stmt = text('select * from roles_certificates where role_id=:role_id and certificate_id=:certificate_id')
stmt = stmt.bindparams(role_id=owner_role[0], certificate_id=id)
exists = conn.execute(stmt).fetchone()
if not exists:
stmt = text('insert into roles_certificates (role_id, certificate_id) values (:role_id, :certificate_id)')
stmt = stmt.bindparams(role_id=owner_role[0], certificate_id=id)
op.execute(stmt)
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'certificates', type_='foreignkey')
op.drop_constraint(None, 'certificates', type_='foreignkey')
op.create_foreign_key(u'certificates_authority_id_fkey', 'certificates', 'authorities', ['authority_id'], ['id'])
op.alter_column('certificates', 'owner',
existing_type=sa.VARCHAR(length=128),
nullable=True)
op.alter_column('certificates', 'body',
existing_type=sa.TEXT(),
nullable=True)
op.drop_column('certificates', 'root_authority_id')
op.add_column('authorities', sa.Column('not_before', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
op.add_column('authorities', sa.Column('cn', sa.VARCHAR(length=128), autoincrement=False, nullable=True))
op.add_column('authorities', sa.Column('bits', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('authorities', sa.Column('not_after', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
op.alter_column('authorities', 'owner',
existing_type=sa.VARCHAR(length=128),
nullable=True)
### end Alembic commands ###

View File

@ -0,0 +1,31 @@
"""Adding the ability to specify certificate replacements
Revision ID: 33de094da890
Revises: ed422fc58ba
Create Date: 2015-11-30 15:40:19.827272
"""
# revision identifiers, used by Alembic.
revision = '33de094da890'
down_revision = None
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('certificate_replacement_associations',
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_replacement_associations')
### end Alembic commands ###

View File

@ -1,49 +0,0 @@
"""Refactors Accounts to Destinations
Revision ID: 3b718f59b8ce
Revises: None
Create Date: 2015-07-09 17:44:55.626221
"""
# revision identifiers, used by Alembic.
revision = '3b718f59b8ce'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_account_associations')
op.drop_table('accounts')
op.add_column('destinations', sa.Column('plugin_name', sa.String(length=32), nullable=True))
op.drop_index('ix_elbs_account_id', table_name='elbs')
op.drop_constraint(u'elbs_account_id_fkey', 'elbs', type_='foreignkey')
op.drop_column('elbs', 'account_id')
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('elbs', sa.Column('account_id', sa.BIGINT(), autoincrement=False, nullable=True))
op.create_foreign_key(u'elbs_account_id_fkey', 'elbs', 'accounts', ['account_id'], ['id'])
op.create_index('ix_elbs_account_id', 'elbs', ['account_id'], unique=False)
op.drop_column('destinations', 'plugin_name')
op.create_table('accounts',
sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('accounts_id_seq'::regclass)"), nullable=False),
sa.Column('account_number', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
sa.Column('label', sa.VARCHAR(length=32), autoincrement=False, nullable=True),
sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint('id', name=u'accounts_pkey'),
sa.UniqueConstraint('account_number', name=u'accounts_account_number_key'),
postgresql_ignore_search_path=False
)
op.create_table('certificate_account_associations',
sa.Column('account_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('certificate_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['account_id'], [u'accounts.id'], name=u'certificate_account_associations_account_id_fkey', ondelete=u'CASCADE'),
sa.ForeignKeyConstraint(['certificate_id'], [u'certificates.id'], name=u'certificate_account_associations_certificate_id_fkey', ondelete=u'CASCADE')
)
### end Alembic commands ###

View File

@ -0,0 +1,63 @@
"""
Revision ID: 412b22cb656a
Revises: 4c50b903d1ae
Create Date: 2016-05-17 17:37:41.210232
"""
# revision identifiers, used by Alembic.
revision = '412b22cb656a'
down_revision = '4c50b903d1ae'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('roles_authorities',
sa.Column('authority_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
)
op.create_index('roles_authorities_ix', 'roles_authorities', ['authority_id', 'role_id'], unique=True)
op.create_table('roles_certificates',
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
)
op.create_index('roles_certificates_ix', 'roles_certificates', ['certificate_id', 'role_id'], unique=True)
op.create_index('certificate_associations_ix', 'certificate_associations', ['domain_id', 'certificate_id'], unique=True)
op.create_index('certificate_destination_associations_ix', 'certificate_destination_associations', ['destination_id', 'certificate_id'], unique=True)
op.create_index('certificate_notification_associations_ix', 'certificate_notification_associations', ['notification_id', 'certificate_id'], unique=True)
op.create_index('certificate_replacement_associations_ix', 'certificate_replacement_associations', ['certificate_id', 'certificate_id'], unique=True)
op.create_index('certificate_source_associations_ix', 'certificate_source_associations', ['source_id', 'certificate_id'], unique=True)
op.create_index('roles_users_ix', 'roles_users', ['user_id', 'role_id'], unique=True)
### end Alembic commands ###
# migrate existing authority_id relationship to many_to_many
conn = op.get_bind()
for id, authority_id in conn.execute(text('select id, authority_id from roles where authority_id is not null')):
stmt = text('insert into roles_authorities (role_id, authority_id) values (:role_id, :authority_id)')
stmt = stmt.bindparams(role_id=id, authority_id=authority_id)
op.execute(stmt)
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index('roles_users_ix', table_name='roles_users')
op.drop_index('certificate_source_associations_ix', table_name='certificate_source_associations')
op.drop_index('certificate_replacement_associations_ix', table_name='certificate_replacement_associations')
op.drop_index('certificate_notification_associations_ix', table_name='certificate_notification_associations')
op.drop_index('certificate_destination_associations_ix', table_name='certificate_destination_associations')
op.drop_index('certificate_associations_ix', table_name='certificate_associations')
op.drop_index('roles_certificates_ix', table_name='roles_certificates')
op.drop_table('roles_certificates')
op.drop_index('roles_authorities_ix', table_name='roles_authorities')
op.drop_table('roles_authorities')
### end Alembic commands ###

View File

@ -0,0 +1,26 @@
"""Adding ability to mark domains as 'sensitive'
Revision ID: 4c50b903d1ae
Revises: 33de094da890
Create Date: 2015-12-30 10:19:30.057791
"""
# revision identifiers, used by Alembic.
revision = '4c50b903d1ae'
down_revision = '33de094da890'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('domains', sa.Column('sensitive', sa.Boolean(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('domains', 'sensitive')
### end Alembic commands ###

View File

@ -1,41 +0,0 @@
"""Adding notifications
Revision ID: 4c8915e461b3
Revises: 3b718f59b8ce
Create Date: 2015-07-24 14:34:57.316273
"""
# revision identifiers, used by Alembic.
revision = '4c8915e461b3'
down_revision = '3b718f59b8ce'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import sqlalchemy_utils
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('notifications',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('label', sa.String(length=128), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('plugin_name', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.drop_column(u'certificates', 'challenge')
op.drop_column(u'certificates', 'csr_config')
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column(u'certificates', sa.Column('csr_config', sa.TEXT(), autoincrement=False, nullable=True))
op.add_column(u'certificates', sa.Column('challenge', postgresql.BYTEA(), autoincrement=False, nullable=True))
op.drop_table('notifications')
### end Alembic commands ###

View File

@ -1,31 +0,0 @@
"""Creating a one-to-many relationship for notifications
Revision ID: 4dc5ddd111b8
Revises: 4c8915e461b3
Create Date: 2015-07-24 15:02:04.398262
"""
# revision identifiers, used by Alembic.
revision = '4dc5ddd111b8'
down_revision = '4c8915e461b3'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('certificate_notification_associations',
sa.Column('notification_id', sa.Integer(), nullable=True),
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_notification_associations')
### end Alembic commands ###

View File

@ -8,7 +8,8 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy import Column, Integer, ForeignKey, Index
from lemur.database import db
certificate_associations = db.Table('certificate_associations',
@ -16,6 +17,8 @@ certificate_associations = db.Table('certificate_associations',
Column('certificate_id', Integer, ForeignKey('certificates.id'))
)
Index('certificate_associations_ix', certificate_associations.c.domain_id, certificate_associations.c.certificate_id)
certificate_destination_associations = db.Table('certificate_destination_associations',
Column('destination_id', Integer,
ForeignKey('destinations.id', ondelete='cascade')),
@ -23,6 +26,8 @@ certificate_destination_associations = db.Table('certificate_destination_associa
ForeignKey('certificates.id', ondelete='cascade'))
)
Index('certificate_destination_associations_ix', certificate_destination_associations.c.destination_id, certificate_destination_associations.c.certificate_id)
certificate_source_associations = db.Table('certificate_source_associations',
Column('source_id', Integer,
ForeignKey('sources.id', ondelete='cascade')),
@ -30,13 +35,44 @@ certificate_source_associations = db.Table('certificate_source_associations',
ForeignKey('certificates.id', ondelete='cascade'))
)
Index('certificate_source_associations_ix', certificate_source_associations.c.source_id, certificate_source_associations.c.certificate_id)
certificate_notification_associations = db.Table('certificate_notification_associations',
Column('notification_id', Integer,
ForeignKey('notifications.id', ondelete='cascade')),
Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade'))
)
Index('certificate_notification_associations_ix', certificate_notification_associations.c.notification_id, certificate_notification_associations.c.certificate_id)
certificate_replacement_associations = db.Table('certificate_replacement_associations',
Column('replaced_certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade')),
Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade'))
)
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.certificate_id, certificate_replacement_associations.c.certificate_id)
roles_authorities = db.Table('roles_authorities',
Column('authority_id', Integer, ForeignKey('authorities.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
roles_certificates = db.Table('roles_certificates',
Column('certificate_id', Integer, ForeignKey('certificates.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id)
roles_users = db.Table('roles_users',
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id)

View File

@ -0,0 +1,49 @@
"""
.. module: lemur.notifications.schemas
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from marshmallow import fields, post_dump
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
from lemur.schemas import PluginInputSchema, PluginOutputSchema, AssociatedCertificateSchema
class NotificationInputSchema(LemurInputSchema):
id = fields.Integer()
label = fields.String(required=True)
description = fields.String()
active = fields.Boolean()
plugin = fields.Nested(PluginInputSchema, required=True)
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
class NotificationOutputSchema(LemurOutputSchema):
id = fields.Integer()
label = fields.String()
description = fields.String()
active = fields.Boolean()
options = fields.List(fields.Dict())
plugin = fields.Nested(PluginOutputSchema)
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
@post_dump
def fill_object(self, data):
data['plugin']['pluginOptions'] = data['options']
return data
class NotificationNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
label = fields.String()
description = fields.String()
active = fields.Boolean()
options = fields.List(fields.Dict())
plugin = fields.Nested(PluginOutputSchema)
notification_input_schema = NotificationInputSchema()
notification_output_schema = NotificationOutputSchema()
notifications_output_schema = NotificationOutputSchema(many=True)

View File

@ -9,7 +9,6 @@
"""
import ssl
import socket
import arrow
@ -37,10 +36,16 @@ def _get_message_data(cert):
:param cert:
:return:
"""
cert_dict = cert.as_dict()
cert_dict['creator'] = cert.user.email
cert_dict['domains'] = [x .name for x in cert.domains]
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert) if cert.name != x]))
cert_dict = {}
if cert.user:
cert_dict['creator'] = cert.user.email
cert_dict['not_after'] = cert.not_after
cert_dict['owner'] = cert.owner
cert_dict['name'] = cert.name
cert_dict['body'] = cert.body
return cert_dict
@ -56,13 +61,18 @@ def _deduplicate(messages):
for m, r, o in roll_ups:
if r == targets:
m.append(data)
current_app.logger.info(
"Sending expiration alert about {0} to {1}".format(
data['name'], ",".join(targets)))
for cert in m:
if cert['body'] == data['body']:
break
else:
m.append(data)
current_app.logger.info(
"Sending expiration alert about {0} to {1}".format(
data['name'], ",".join(targets)))
break
else:
roll_ups.append(([data], targets, options))
return roll_ups
@ -106,8 +116,9 @@ def _get_domain_certificate(name):
try:
pub_key = ssl.get_server_certificate((name, 443))
return cert_service.find_duplicates(pub_key.strip())
except socket.gaierror as e:
except Exception as e:
current_app.logger.info(str(e))
return []
def _find_superseded(cert):
@ -178,6 +189,9 @@ def create_default_expiration_notifications(name, recipients):
:param name:
:return:
"""
if not recipients:
return []
options = [
{
'name': 'unit',
@ -198,7 +212,7 @@ def create_default_expiration_notifications(name, recipients):
},
]
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [30, 15, 2])
notifications = []
for i in intervals:
@ -259,7 +273,7 @@ def update(notification_id, label, options, description, active, certificates):
notification.options = options
notification.description = description
notification.active = active
notification = database.update_list(notification, 'certificates', Certificate, certificates)
notification.certificates = certificates
return database.update(notification)
@ -305,10 +319,6 @@ def get_all():
def render(args):
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
@ -327,9 +337,4 @@ def render(args):
else:
query = database.filter(query, Notification, terms)
query = database.find_all(query, Notification, args)
if sort_by and sort_dir:
query = database.sort(query, Notification, sort_by, sort_dir)
return database.paginate(query, page, count)
return database.sort_and_page(query, Notification, args)

View File

@ -7,34 +7,27 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint
from flask.ext.restful import Api, reqparse, fields
from flask.ext.restful import Api, reqparse
from lemur.notifications import service
from lemur.notifications.schemas import notification_input_schema, notification_output_schema, notifications_output_schema
from lemur.auth.service import AuthenticatedResource
from lemur.common.utils import paginated_parser, marshal_items
from lemur.common.utils import paginated_parser
from lemur.common.schema import validate_schema
mod = Blueprint('notifications', __name__)
api = Api(mod)
FIELDS = {
'description': fields.String,
'notificationOptions': fields.Raw(attribute='options'),
'pluginName': fields.String(attribute='plugin_name'),
'label': fields.String,
'active': fields.Boolean,
'id': fields.Integer,
}
class NotificationsList(AuthenticatedResource):
""" Defines the 'notifications' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(NotificationsList, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, notifications_output_schema)
def get(self):
"""
.. http:get:: /notifications
@ -61,7 +54,7 @@ class NotificationsList(AuthenticatedResource):
"items": [
{
"description": "An example",
"notificationOptions": [
"options": [
{
"name": "interval",
"required": true,
@ -103,9 +96,9 @@ class NotificationsList(AuthenticatedResource):
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
@ -114,8 +107,8 @@ class NotificationsList(AuthenticatedResource):
args = parser.parse_args()
return service.render(args)
@marshal_items(FIELDS)
def post(self):
@validate_schema(notification_input_schema, notification_output_schema)
def post(self, data=None):
"""
.. http:post:: /notifications
@ -131,7 +124,7 @@ class NotificationsList(AuthenticatedResource):
{
"description": "a test",
"notificationOptions": [
"options": [
{
"name": "interval",
"required": true,
@ -178,7 +171,7 @@ class NotificationsList(AuthenticatedResource):
{
"description": "a test",
"notificationOptions": [
"options": [
{
"name": "interval",
"required": true,
@ -221,18 +214,12 @@ class NotificationsList(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
args = self.reqparse.parse_args()
return service.create(
args['label'],
args['plugin']['slug'],
args['plugin']['pluginOptions'],
args['description'],
args['certificates']
data['label'],
data['plugin']['slug'],
data['plugin']['plugin_options'],
data['description'],
data['certificates']
)
@ -241,7 +228,7 @@ class Notifications(AuthenticatedResource):
self.reqparse = reqparse.RequestParser()
super(Notifications, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, notification_output_schema)
def get(self, notification_id):
"""
.. http:get:: /notifications/1
@ -266,7 +253,7 @@ class Notifications(AuthenticatedResource):
{
"description": "a test",
"notificationOptions": [
"options": [
{
"name": "interval",
"required": true,
@ -308,8 +295,8 @@ class Notifications(AuthenticatedResource):
"""
return service.get(notification_id)
@marshal_items(FIELDS)
def put(self, notification_id):
@validate_schema(notification_input_schema, notification_output_schema)
def put(self, notification_id, data=None):
"""
.. http:put:: /notifications/1
@ -345,20 +332,13 @@ class Notifications(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('active', type=bool, location='json')
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args()
return service.update(
notification_id,
args['label'],
args['plugin']['pluginOptions'],
args['description'],
args['active'],
args['certificates']
data['label'],
data['plugin']['plugin_options'],
data['description'],
data['active'],
data['certificates']
)
def delete(self, notification_id):
@ -371,7 +351,7 @@ class CertificateNotifications(AuthenticatedResource):
def __init__(self):
super(CertificateNotifications, self).__init__()
@marshal_items(FIELDS)
@validate_schema(None, notifications_output_schema)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/notifications
@ -398,7 +378,7 @@ class CertificateNotifications(AuthenticatedResource):
"items": [
{
"description": "An example",
"notificationOptions": [
"options": [
{
"name": "interval",
"required": true,
@ -440,17 +420,13 @@ class CertificateNotifications(AuthenticatedResource):
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:query page: int default is 1
:query filter: key value pair format is k;v
:query count: count number default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
parser = paginated_parser.copy()
parser.add_argument('active', type=bool, location='args')
args = parser.parse_args()
args['certificate_id'] = certificate_id
return service.render(args)
return service.render({'certificate_id': certificate_id})
api.add_resource(NotificationsList, '/notifications', endpoint='notifications')

View File

@ -108,10 +108,11 @@ class IPlugin(local):
"""
return self.resource_links
def get_option(self, name, options):
@staticmethod
def get_option(name, options):
for o in options:
if o.get(name):
return o['value']
if o.get('name') == name:
return o.get('value', o.get('default'))
class Plugin(IPlugin):

View File

@ -2,3 +2,4 @@ from .destination import DestinationPlugin # noqa
from .issuer import IssuerPlugin # noqa
from .source import SourcePlugin # noqa
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa
from .export import ExportPlugin # noqa

View File

@ -0,0 +1,21 @@
"""
.. module: lemur.bases.export
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from lemur.plugins.base import Plugin
class ExportPlugin(Plugin):
"""
This is the base class from which all supported
exporters will inherit from.
"""
type = 'export'
requires_key = True
def export(self):
raise NotImplemented

View File

@ -0,0 +1,16 @@
"""
.. module: lemur.bases.metric
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from lemur.plugins.base import Plugin
class MetricPlugin(Plugin):
type = 'metric'
def submit(self, *args, **kwargs):
raise NotImplemented

View File

@ -45,7 +45,7 @@ class ExpirationNotificationPlugin(NotificationPlugin):
]
@property
def options(self):
def plugin_options(self):
return list(self.default_options) + self.additional_options
def send(self):

View File

@ -0,0 +1,107 @@
"""
.. module: lemur.plugins.lemur_atlas.plugin
:platform: Unix
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import json
import requests
from requests.exceptions import ConnectionError
from datetime import datetime
from flask import current_app
from lemur.plugins import lemur_atlas as atlas
from lemur.plugins.bases.metric import MetricPlugin
def millis_since_epoch():
"""
current time since epoch in milliseconds
"""
epoch = datetime.utcfromtimestamp(0)
delta = datetime.now() - epoch
return int(delta.total_seconds() * 1000.0)
class AtlasMetricPlugin(MetricPlugin):
title = 'Atlas'
slug = 'atlas-metric'
description = 'Adds support for sending key metrics to Atlas'
version = atlas.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = [
{
'name': 'sidecar_host',
'type': 'str',
'required': False,
'help_message': 'If no host is provided localhost is assumed',
'default': 'localhost'
},
{
'name': 'sidecar_port',
'type': 'int',
'required': False,
'default': 8078
}
]
metric_data = {}
sidecar_host = None
sidecar_port = None
def submit(self, metric_name, metric_type, metric_value, metric_tags=None, options=None):
if not options:
options = self.options
# TODO marshmallow schema?
valid_types = ['COUNTER', 'GAUGE', 'TIMER']
if metric_type.upper() not in valid_types:
raise Exception(
"Invalid Metric Type for Atlas: '{metric}' choose from: {options}".format(
metric=metric_type, options=','.join(valid_types)
)
)
if metric_tags:
if not isinstance(metric_tags, dict):
raise Exception(
"Invalid Metric Tags for Atlas: Tags must be in dict format"
)
if metric_value == "NaN" or isinstance(metric_value, int) or isinstance(metric_value, float):
self.metric_data['value'] = metric_value
else:
raise Exception(
"Invalid Metric Value for Atlas: Metric must be a number"
)
self.metric_data['type'] = metric_type.upper()
self.metric_data['name'] = str(metric_name)
self.metric_data['tags'] = metric_tags
self.metric_data['timestamp'] = millis_since_epoch()
self.sidecar_host = self.get_option('sidecar_host', options)
self.sidecar_port = self.get_option('sidecar_port', options)
try:
res = requests.post(
'http://{host}:{port}/metrics'.format(
host=self.sidecar_host,
port=self.sidecar_port),
data=json.dumps([self.metric_data])
)
if res.status_code != 200:
current_app.logger.warning("Failed to publish altas metric. {0}".format(res.content))
except ConnectionError:
current_app.logger.warning(
"AtlasMetrics: could not connect to sidecar at {host}:{port}".format(
host=self.sidecar_host, port=self.sidecar_port
)
)

View File

@ -1,5 +1,5 @@
"""
.. module: elb
.. module: lemur.plugins.lemur_aws.elb
:synopsis: Module contains some often used and helpful classes that
are used to deal with ELBs
@ -28,7 +28,6 @@ def is_valid(listener_tuple):
:param listener_tuple:
"""
current_app.logger.debug(listener_tuple)
lb_port, i_port, lb_protocol, arn = listener_tuple
current_app.logger.debug(lb_protocol)
@ -138,3 +137,19 @@ def delete_listeners(account_number, region, name, ports):
:return:
"""
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
def get_listeners(account_number, region, name):
"""
Gets the listeners configured on an elb and returns a array of tuples
:param account_number:
:param region:
:param name:
:return: list of tuples
"""
conn = assume_service(account_number, 'elb', region)
elbs = conn.get_all_load_balancers(load_balancer_names=[name])
if elbs:
return elbs[0].listeners

View File

@ -6,6 +6,7 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from boto.exception import BotoServerError
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
from lemur.plugins.lemur_aws import iam, elb
from lemur.plugins import lemur_aws as aws
@ -42,11 +43,18 @@ class AWSDestinationPlugin(DestinationPlugin):
# }
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
if private_key:
try:
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
except BotoServerError as e:
if e.error_code != 'EntityAlreadyExists':
raise Exception(e)
e = find_value('elb', options)
if e:
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
e = find_value('elb', options)
if e:
elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
else:
raise Exception("Unable to upload to AWS, private key is required")
class AWSSourcePlugin(SourcePlugin):
@ -75,8 +83,8 @@ class AWSSourcePlugin(SourcePlugin):
cert_body, cert_chain = iam.get_cert_from_arn(arn)
cert_name = iam.get_name_from_arn(arn)
cert = dict(
public_certificate=cert_body,
intermediate_certificate=cert_chain,
body=cert_body,
chain=cert_chain,
name=cert_name
)
certs.append(cert)

View File

@ -2,7 +2,7 @@ from moto import mock_iam, mock_sts
from lemur.certificates.models import Certificate
from lemur.tests.certs import EXTERNAL_VALID_STR, PRIVATE_KEY_STR
from lemur.tests.vectors import EXTERNAL_VALID_STR, PRIVATE_KEY_STR
def test_get_name_from_arn():

View File

@ -1,352 +0,0 @@
"""
.. module: lemur.common.services.issuers.plugins.cloudca
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import ssl
import base64
from json import dumps
import arrow
import requests
from requests.adapters import HTTPAdapter
from flask import current_app
from lemur.exceptions import LemurException
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
from lemur.plugins import lemur_cloudca as cloudca
from lemur.authorities import service as authority_service
API_ENDPOINT = '/v1/ca/netflix' # TODO this should be configurable
class CloudCAException(LemurException):
def __init__(self, message):
self.message = message
current_app.logger.error(self)
def __str__(self):
return repr("CloudCA request failed: {0}".format(self.message))
class CloudCAHostNameCheckingAdapter(HTTPAdapter):
def cert_verify(self, conn, url, verify, cert):
super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert)
conn.assert_hostname = False
def remove_none(options):
"""
Simple function that traverse the options and removed any None items
CloudCA really dislikes null values.
:param options:
:return:
"""
new_dict = {}
for k, v in options.items():
if v:
new_dict[k] = v
# this is super hacky and gross, cloudca doesn't like null values
if new_dict.get('extensions'):
if len(new_dict['extensions']['subAltNames']['names']) == 0:
del new_dict['extensions']['subAltNames']
return new_dict
def get_default_issuance(options):
"""
Gets the default time range for certificates
:param options:
:return:
"""
if not options.get('validityStart') and not options.get('validityEnd'):
start = arrow.utcnow()
options['validityStart'] = start.floor('second').isoformat()
options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY'))\
.ceil('second').isoformat()
return options
def convert_to_pem(der):
"""
Converts DER to PEM Lemur uses PEM internally
:param der:
:return:
"""
decoded = base64.b64decode(der)
return ssl.DER_cert_to_PEM_cert(decoded)
def convert_date_to_utc_time(date):
"""
Converts a python `datetime` object to the current date + current time in UTC.
:param date:
:return:
"""
d = arrow.get(date)
return arrow.utcnow().replace(day=d.naive.day).replace(month=d.naive.month).replace(year=d.naive.year)\
.replace(microsecond=0)
def process_response(response):
"""
Helper function that processes responses from CloudCA.
:param response:
:return: :raise CloudCAException:
"""
if response.status_code == 200:
res = response.json()
if res['returnValue'] != 'success':
current_app.logger.debug(res)
if res.get('data'):
raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']]))
else:
raise CloudCAException(res['returnMessage'])
else:
raise CloudCAException("There was an error with your request: {0}".format(response.status_code))
return response.json()
def get_auth_data(ca_name):
"""
Creates the authentication record needed to authenticate a user request to CloudCA.
:param ca_name:
:return: :raise CloudCAException:
"""
role = authority_service.get_authority_role(ca_name)
if role:
return {
"authInfo": {
"credType": "password",
"credentials": {
"username": role.username,
"password": role.password # we only decrypt when we need to
}
}
}
raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
class CloudCA(object):
def __init__(self, *args, **kwargs):
self.session = requests.Session()
self.session.mount('https://', CloudCAHostNameCheckingAdapter())
self.url = current_app.config.get('CLOUDCA_URL')
if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'):
self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH')
self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE')
else:
current_app.logger.warning(
"No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA"
)
super(CloudCA, self).__init__(*args, **kwargs)
def post(self, endpoint, data):
"""
HTTP POST to CloudCA
:param endpoint:
:param data:
:return:
"""
data = dumps(dict(data.items() + get_auth_data(data['caName']).items()))
# we set a low timeout, if cloudca is down it shouldn't bring down
# lemur
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
return process_response(response)
def get(self, endpoint):
"""
HTTP GET to CloudCA
:param endpoint:
:return:
"""
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
return process_response(response)
def random(self, length=10):
"""
Uses CloudCA as a decent source of randomness.
:param length:
:return:
"""
endpoint = '/v1/random/{0}'.format(length)
response = self.session.get(self.url + endpoint, verify=self.ca_bundle)
return response
def get_authorities(self):
"""
Retrieves authorities that were made outside of Lemur.
:return:
"""
endpoint = '{0}/listCAs'.format(API_ENDPOINT)
authorities = []
for ca in self.get(endpoint)['data']['caList']:
try:
authorities.append(ca['caName'])
except AttributeError:
current_app.logger.error("No authority has been defined for {}".format(ca['caName']))
return authorities
class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
title = 'CloudCA'
slug = 'cloudca-issuer'
description = 'Enables the creation of certificates from the cloudca API.'
version = cloudca.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
def create_authority(self, options):
"""
Creates a new certificate authority
:param options:
:return:
"""
# this is weird and I don't like it
endpoint = '{0}/createCA'.format(API_ENDPOINT)
options['caDN']['email'] = options['ownerEmail']
if options['caType'] == 'subca':
options = dict(options.items() + self.auth_data(options['caParent']).items())
options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat()
options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat()
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10,
verify=self.ca_bundle)
json = process_response(response)
roles = []
for cred in json['data']['authInfo']:
role = {
'username': cred['credentials']['username'],
'password': cred['credentials']['password'],
'name': "_".join([options['caName'], cred['credentials']['username']])
}
roles.append(role)
if options['caType'] == 'subca':
cert = convert_to_pem(json['data']['certificate'])
else:
cert = convert_to_pem(json['data']['rootCertificate'])
intermediates = []
for i in json['data']['intermediateCertificates']:
intermediates.append(convert_to_pem(i))
return cert, "".join(intermediates), roles,
def create_certificate(self, csr, options):
"""
Creates a new certificate from cloudca
If no start and end date are specified the default issue range
will be used.
:param csr:
:param options:
"""
endpoint = '{0}/enroll'.format(API_ENDPOINT)
# lets default to two years if it's not specified
# we do some last minute data massaging
options = get_default_issuance(options)
cloudca_options = {
'extensions': options['extensions'],
'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(),
'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(),
'creator': options['creator'],
'ownerEmail': options['owner'],
'caName': options['authority'].name,
'csr': csr,
'comment': options['description']
}
response = self.post(endpoint, remove_none(cloudca_options))
# we return a concatenated list of intermediate because that is what aws
# expects
cert = convert_to_pem(response['data']['certificate'])
intermediates = [convert_to_pem(response['data']['rootCertificate'])]
for i in response['data']['intermediateCertificates']:
intermediates.append(convert_to_pem(i))
return cert, "".join(intermediates),
class CloudCASourcePlugin(SourcePlugin, CloudCA):
title = 'CloudCA'
slug = 'cloudca-source'
description = 'Discovers all SSL certificates in CloudCA'
version = cloudca.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
options = {
'pollRate': {'type': 'int', 'default': '60'}
}
def get_certificates(self, **kwargs):
certs = []
for authority in self.get_authorities():
certs += self.get_cert(ca_name=authority)
return
def get_cert(self, ca_name=None, cert_handle=None):
"""
Returns a given cert from CloudCA.
:param ca_name:
:param cert_handle:
:return:
"""
endpoint = '{0}/getCert'.format(API_ENDPOINT)
response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10,
verify=self.ca_bundle)
raw = process_response(response)
certs = []
for c in raw['data']['certList']:
cert = convert_to_pem(c['certValue'])
intermediates = []
for i in c['intermediateCertificates']:
intermediates.append(convert_to_pem(i))
certs.append({
'public_certificate': cert,
'intermediate_cert': "\n".join(intermediates),
'owner': c['ownerEmail']
})
return certs

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