Compare commits
193 Commits
Author | SHA1 | Date | |
---|---|---|---|
d11f254476 | |||
d54a11ad11 | |||
a9361fe428 | |||
5345170a4f | |||
d0ccd85afe | |||
520404c215 | |||
9ac1756011 | |||
851d74da3d | |||
3f2691c5d4 | |||
eaf34b1c8b | |||
e9219adfb5 | |||
9eddaf66cb | |||
0a29a3fa2a | |||
9bb0787410 | |||
dd14fd202d | |||
114deba06e | |||
0334f1094d | |||
7af68c3cc0 | |||
953d3a08e7 | |||
f141ae78f3 | |||
94d619cfa6 | |||
89470a0ce0 | |||
e6b291d034 | |||
b0eef03c73 | |||
25a6c722b6 | |||
67a5993926 | |||
aa979e31fd | |||
b74df2b3e4 | |||
4afedaf537 | |||
2b79474060 | |||
a6360ebfe5 | |||
d99681904e | |||
1ac1a44e83 | |||
f990f92977 | |||
490d5b6e6c | |||
4b7fc8551c | |||
cd9c112218 | |||
a8f44944b1 | |||
d31c9b19ce | |||
fb178866f4 | |||
f921b67fff | |||
c367e4f73f | |||
dcb18a57c4 | |||
1b861baf0a | |||
10d833e598 | |||
708d85abeb | |||
ee028382df | |||
c05a49f8c9 | |||
35cfb50955 | |||
f179e74a4a | |||
9065aa3750 | |||
96e42c793e | |||
72a390c563 | |||
a19c918c68 | |||
c45c23ae6f | |||
5cbf5365c5 | |||
3ad7a37f95 | |||
6cac2838e3 | |||
fbbf7f90f6 | |||
1ea75a5d2d | |||
3ce87c8a6b | |||
39645a1a84 | |||
a60e372c5a | |||
76cece7b90 | |||
ca2944d566 | |||
53d0636574 | |||
7e6278684c | |||
2d7a6ccf3c | |||
18b99c0de4 | |||
96674571a5 | |||
29a330b1f4 | |||
a644f45625 | |||
3db669b24d | |||
f38868a97f | |||
4f3dc5422c | |||
1ba7181067 | |||
74bf54cb8f | |||
d4732d3ab0 | |||
cb9631b122 | |||
4077893d08 | |||
4ee1c21144 | |||
c8eca56690 | |||
300e2d0b7d | |||
a8040777b3 | |||
e34de921b6 | |||
a04f707f63 | |||
9aec899bfd | |||
afb66df1a4 | |||
54b888bb08 | |||
eefff8497a | |||
ecbab64c35 | |||
c8447dea3d | |||
5021e8ba91 | |||
f846d78778 | |||
fe9703dd94 | |||
b44a7c73d8 | |||
9ae27f1415 | |||
19b928d663 | |||
5193342b3a | |||
109fb4bb45 | |||
d6ccd812c2 | |||
81a6228028 | |||
eeb216b75e | |||
6714595fee | |||
025924c4f7 | |||
7c10c8dac7 | |||
daea8f6ae4 | |||
41d1fe9191 | |||
7d50e4d65f | |||
9a653403ae | |||
77f13c9edb | |||
d95b1a0a41 | |||
d9cc4980e8 | |||
5e987fa8b6 | |||
42001be9ec | |||
dc198fec8c | |||
acd47d5ec9 | |||
72e3fb5bfe | |||
b2539b843b | |||
be5dff8472 | |||
76037e8b3a | |||
11f4bd503b | |||
6688b279e7 | |||
1ca38015bc | |||
656269ff17 | |||
bd727b825d | |||
e04c1e7dc9 | |||
615df76dd5 | |||
112c6252d6 | |||
b13370bf0d | |||
88aa5d3fdb | |||
b187d8f836 | |||
1763a1a717 | |||
62b61ed980 | |||
c11034b9bc | |||
58e8fe0bd0 | |||
a0c8765588 | |||
9022059dc6 | |||
7f790be1e4 | |||
93791c999d | |||
5e9f1437ad | |||
f9655213b3 | |||
008d608ec4 | |||
78c8d12ad8 | |||
df0ad4d875 | |||
776e0fcd11 | |||
6ec3bad49a | |||
52f44c3ea6 | |||
941d36ebfe | |||
db8243b4b4 | |||
f919b7360e | |||
8e1b7c0036 | |||
9b0e0fa9c2 | |||
565d7afa92 | |||
c914ba946f | |||
6f9280f64a | |||
8fe460e401 | |||
b9fe359d23 | |||
2c6d494c32 | |||
dbd1279226 | |||
b463fcf61b | |||
82b4f5125d | |||
3f89d6d009 | |||
676f843c92 | |||
c2387dc120 | |||
9a8e1534c0 | |||
dbc4964e94 | |||
00b263f345 | |||
62d03b0d41 | |||
b5a4b293a9 | |||
bfcfdb83a7 | |||
4ccbfa8164 | |||
675d10c8a6 | |||
2cde7336dc | |||
169490dbec | |||
3ceb297276 | |||
12633bfed6 | |||
5958bac2a2 | |||
37f2d5b8b0 | |||
47891d2953 | |||
af68571f4e | |||
d0ec925ca3 | |||
939194158a | |||
576265e09c | |||
dfaf45344c | |||
6c378957e9 | |||
e8f9bc80a0 | |||
a30b8b21e4 | |||
12204852aa | |||
edba980b56 | |||
ba666ddbfa | |||
35f9f59c57 | |||
ac1f493338 |
2
.coveragerc
Normal file
2
.coveragerc
Normal file
@ -0,0 +1,2 @@
|
||||
[report]
|
||||
include = lemur/*.py
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/.cache
|
||||
.coverage
|
||||
.tox
|
||||
.DS_Store
|
||||
|
@ -8,7 +8,7 @@
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"latedef": false,
|
||||
"newcap": false,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
@ -22,6 +22,8 @@
|
||||
"angular": false,
|
||||
"moment": false,
|
||||
"toaster": false,
|
||||
"d3": false,
|
||||
"self": false,
|
||||
"_": false
|
||||
}
|
||||
}
|
||||
|
5
.pre-commit-config.yaml
Normal file
5
.pre-commit-config.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
sha: 18d7035de5388cc7775be57f529c154bf541aab9
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: flake8
|
24
.travis.yml
24
.travis.yml
@ -1,6 +1,9 @@
|
||||
sudo: false
|
||||
|
||||
language: python
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
node_js:
|
||||
- "4.2"
|
||||
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
@ -9,10 +12,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:
|
||||
@ -22,15 +23,26 @@ cache:
|
||||
env:
|
||||
global:
|
||||
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
|
||||
# do not load /etc/boto.cfg with Python 3 incompatible plugin
|
||||
# https://github.com/travis-ci/travis-ci/issues/5246#issuecomment-166460882
|
||||
- BOTO_CONFIG=/doesnotexist
|
||||
|
||||
before_script:
|
||||
- psql -c "create database lemur;" -U postgres
|
||||
- psql -c "create user lemur with password 'lemur;'" -U postgres
|
||||
- npm config set registry https://registry.npmjs.org
|
||||
- npm install -g bower
|
||||
- pip install --upgrade setuptools
|
||||
|
||||
install:
|
||||
- pip install coveralls
|
||||
|
||||
script:
|
||||
- make test
|
||||
|
||||
after_success:
|
||||
- coveralls
|
||||
|
||||
notifications:
|
||||
email:
|
||||
kglisson@netflix.com
|
||||
|
100
CHANGELOG.rst
100
CHANGELOG.rst
@ -1,21 +1,103 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.5 - `master`
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. note:: This version is not yet released and is under active development
|
||||
|
||||
|
||||
0.4 - ``
|
||||
~~~~~~~~
|
||||
|
||||
There have been quite a few issues closed in this release. Some notables:
|
||||
|
||||
* Closed `#284 <https://github.com/Netflix/lemur/issues/284>`_ - Created new models for `Endpoints` created associated
|
||||
AWS ELB endpoint tracking code. This was the major stated goal of this milestone and should serve as the basis for
|
||||
future enhancements of Lemur's certificate 'deployment' capabilities.
|
||||
|
||||
* Closed `#334 <https://github.com/Netflix/lemur/issues/334>`_ - Lemur not has the ability
|
||||
to restrict certificate expiration dates to weekdays.
|
||||
|
||||
Several fixes/tweaks to Lemurs python3 support (thanks chadhendrie!)
|
||||
|
||||
This will most likely be the last release to support python2.7 moving Lemur to target python3 exclusively. Please comment
|
||||
on issue #340 if this negatively affects your usage of Lemur.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
See the full list of issues closed in `0.4 <https://github.com/Netflix/lemur/milestone/3>`_.
|
||||
|
||||
.. note:: This release will need a slight migration change. Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
|
||||
|
||||
|
||||
0.3.0 - `2016-06-06`
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is quite a large upgrade, it is highly advised you backup your database before attempting to upgrade as this release
|
||||
requires the migration of database structure as well as data.
|
||||
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
|
||||
|
||||
|
||||
Source Plugin Owners
|
||||
--------------------
|
||||
|
||||
The dictionary returned from a source plugin has changed keys from `public_certificate` to `body` and `intermediate_certificate` to chain.
|
||||
|
||||
|
||||
Issuer Plugin Owners
|
||||
--------------------
|
||||
|
||||
This release may break your plugins, the keys in `issuer_options` have been changed from `camelCase` to `under_score`.
|
||||
This change was made to break a undue reliance on downstream options maintains a more pythonic naming convention. Renaming
|
||||
these keys should be fairly trivial, additionally pull requests have been submitted to affected plugins to help ease the transition.
|
||||
|
||||
.. note:: This change only affects issuer plugins and does not affect any other types of plugins.
|
||||
|
||||
|
||||
* Closed `#63 <https://github.com/Netflix/lemur/issues/63>`_ - Validates all endpoints with Marshmallow schemas, this allows for
|
||||
stricter input validation and better error messages when validation fails.
|
||||
* Closed `#146 <https://github.com/Netflix/lemur/issues/146>`_ - Moved authority type to first pane of authority creation wizard.
|
||||
* Closed `#147 <https://github.com/Netflix/lemur/issues/147>`_ - Added and refactored the relationship between authorities and their
|
||||
root certificates. Displays the certificates (and chains) next the the authority in question.
|
||||
* Closed `#199 <https://github.com/Netflix/lemur/issues/199>`_ - Ensures that the dates submitted to Lemur during authority and
|
||||
certificate creation are actually dates.
|
||||
* Closed `#230 <https://github.com/Netflix/lemur/issues/230>`_ - Migrated authority dropdown to a ui-select based dropdown, this
|
||||
should be easier to determine what authorities are available and when an authority has actually been selected.
|
||||
* Closed `#254 <https://github.com/Netflix/lemur/issues/254>`_ - Forces certificate names to be generally unique. If a certificate name
|
||||
(generated or otherwise) is found to be a duplicate we increment by appending a counter.
|
||||
* Closed `#254 <https://github.com/Netflix/lemur/issues/275>`_ - Switched to using Fernet generated passphrases for exported items.
|
||||
These are more sounds that pseudo random passphrases generated before and have the nice property of being in base64.
|
||||
* Closed `#278 <https://github.com/Netflix/lemur/issues/278>`_ - Added ability to specify a custom name to certificate creation, previously
|
||||
this was only available in the certificate import wizard.
|
||||
* Closed `#281 <https://github.com/Netflix/lemur/issues/281>`_ - Fixed an issue where notifications could not be removed from a certificate
|
||||
via the UI.
|
||||
* Closed `#289 <https://github.com/Netflix/lemur/issues/289>`_ - Fixed and issue where intermediates were not being properly exported.
|
||||
* Closed `#315 <https://github.com/Netflix/lemur/issues/315>`_ - Made how roles are associated with certificates and authorities much more
|
||||
explict, including adding the ability to add roles directly to certificates and authorities on creation.
|
||||
|
||||
|
||||
|
||||
0.2.2 - 2016-02-05
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Closed [#234](https://github.com/Netflix/lemur/issues/234) - Allows export plugins to define whether they need
|
||||
* Closed `#234 <https://github.com/Netflix/lemur/issues/234>`_ - Allows export plugins to define whether they need
|
||||
private key material (default is True)
|
||||
* Closed [#231](https://github.com/Netflix/lemur/issues/231) - Authorities were not respecting 'owning' roles and their
|
||||
* Closed `#231 <https://github.com/Netflix/lemur/issues/231>`_ - Authorities were not respecting 'owning' roles and their
|
||||
users
|
||||
* Closed [#228](https://github.com/Netflix/lemur/issues/228) - Fixed documentation with correct filter values
|
||||
* Closed [#226](https://github.com/Netflix/lemur/issues/226) - Fixes issue were `import_certificate` was requiring
|
||||
* Closed `#228 <https://github.com/Netflix/lemur/issues/228>`_ - Fixed documentation with correct filter values
|
||||
* Closed `#226 <https://github.com/Netflix/lemur/issues/226>`_ - Fixes issue were `import_certificate` was requiring
|
||||
replacement certificates to be specified
|
||||
* Closed [#224](https://github.com/Netflix/lemur/issues/224) - Fixed an issue where NPM might not be globally available (thanks AlexClineBB!)
|
||||
* Closed [#221](https://github.com/Netflix/lemur/issues/234) - Fixes several reported issues where older migration scripts were
|
||||
* Closed `#224 <https://github.com/Netflix/lemur/issues/224>`_ - Fixed an issue where NPM might not be globally available (thanks AlexClineBB!)
|
||||
* Closed `#221 <https://github.com/Netflix/lemur/issues/234>`_ - Fixes several reported issues where older migration scripts were
|
||||
missing tables, this change removes pre 0.2 migration scripts
|
||||
* Closed [#218](https://github.com/Netflix/lemur/issues/234) - Fixed an issue where export passphrases would not validate
|
||||
* Closed `#218 <https://github.com/Netflix/lemur/issues/234>`_ - Fixed an issue where export passphrases would not validate
|
||||
|
||||
|
||||
0.2.1 - 2015-12-14
|
||||
@ -30,7 +112,7 @@ Changelog
|
||||
|
||||
|
||||
0.2.0 - 2015-12-02
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Closed #120 - Error messages not displaying long enough
|
||||
* Closed #121 - Certificate create form should not be valid until a Certificate Authority object is available
|
||||
@ -46,7 +128,7 @@ Changelog
|
||||
|
||||
|
||||
0.1.5 - 2015-10-26
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* **SECURITY ISSUE**: Switched from use a AES static key to Fernet encryption.
|
||||
Affects all versions prior to 0.1.5. If upgrading this will require a data migration.
|
||||
|
13
Makefile
13
Makefile
@ -1,9 +1,16 @@
|
||||
NPM_ROOT = ./node_modules
|
||||
STATIC_DIR = src/lemur/static/app
|
||||
|
||||
USER := $(shell whoami)
|
||||
|
||||
develop: update-submodules setup-git
|
||||
@echo "--> Installing dependencies"
|
||||
ifeq ($(USER), root)
|
||||
@echo "WARNING: It looks like you are installing Lemur as root. This is not generally advised."
|
||||
npm install --unsafe-perm
|
||||
else
|
||||
npm install
|
||||
endif
|
||||
pip install "setuptools>=0.9.8"
|
||||
# order matters here, base package must install first
|
||||
pip install -e .
|
||||
@ -41,7 +48,7 @@ test: develop lint test-python
|
||||
|
||||
testloop: develop
|
||||
pip install pytest-xdist
|
||||
py.test tests -f
|
||||
coverage run --source lemur -m py.test
|
||||
|
||||
test-cli:
|
||||
@echo "--> Testing CLI"
|
||||
@ -60,7 +67,7 @@ test-js:
|
||||
|
||||
test-python:
|
||||
@echo "--> Running Python tests"
|
||||
py.test lemur/tests || exit 1
|
||||
coverage run --source lemur -m py.test
|
||||
@echo ""
|
||||
|
||||
lint: lint-python lint-js
|
||||
@ -82,4 +89,4 @@ coverage: develop
|
||||
publish:
|
||||
python setup.py sdist bdist_wheel upload
|
||||
|
||||
.PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish
|
||||
.PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish
|
||||
|
13
README.rst
13
README.rst
@ -5,24 +5,15 @@ Lemur
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/lemur.svg
|
||||
:target: https://pypi.python.org/pypi/lemur/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://readthedocs.org/projects/lemur/badge/?version=latest
|
||||
:target: https://lemur.readthedocs.org
|
||||
:alt: Latest Docs
|
||||
|
||||
.. image:: https://img.shields.io/badge/NetflixOSS-active-brightgreen.svg
|
||||
|
||||
.. 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
|
||||
|
||||
.. image:: https://badge.waffle.io/Netflix/lemur.png?label=ready&title=Ready
|
||||
:target: https://waffle.io/Netflix/lemur
|
||||
:alt: 'Stories in Ready'
|
||||
|
||||
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
|
||||
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.
|
||||
|
67
bower.json
67
bower.json
@ -6,43 +6,46 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"angular": "1.3",
|
||||
"json3": "~3.3",
|
||||
"es5-shim": "~4.0",
|
||||
"jquery": "~2.1",
|
||||
"angular-resource": "1.2.15",
|
||||
"angular-cookies": "1.2.15",
|
||||
"angular-sanitize": "1.2.15",
|
||||
"angular-route": "1.2.15",
|
||||
"angular-strap": "~2.0.2",
|
||||
"restangular": "~1.4.0",
|
||||
"ng-table": "~0.5.4",
|
||||
"ngAnimate": "*",
|
||||
"moment": "~2.6.0",
|
||||
"angular-animate": "~1.4.0",
|
||||
"angular-loading-bar": "~0.6.0",
|
||||
"fontawesome": "~4.2.0",
|
||||
"jquery": "~2.2.0",
|
||||
"angular-wizard": "~0.4.0",
|
||||
"bootswatch": "3.3.1+2",
|
||||
"angular": "1.4.9",
|
||||
"json3": "~3.3",
|
||||
"es5-shim": "~4.5.0",
|
||||
"bootstrap": "~3.3.6",
|
||||
"angular-bootstrap": "~1.1.1",
|
||||
"angular-animate": "~1.4.9",
|
||||
"restangular": "~1.5.1",
|
||||
"ng-table": "~0.8.3",
|
||||
"moment": "~2.11.1",
|
||||
"angular-loading-bar": "~0.8.0",
|
||||
"angular-moment": "~0.10.3",
|
||||
"moment-range": "~2.1.0",
|
||||
"angular-spinkit": "~0.3.3",
|
||||
"angular-bootstrap": "~0.12.0",
|
||||
"angular-ui-switch": "~0.1.0",
|
||||
"angular-chart.js": "~0.7.1",
|
||||
"satellizer": "~0.9.4",
|
||||
"angularjs-toaster": "~0.4.14",
|
||||
"ngletteravatar": "~3.0.1",
|
||||
"angular-clipboard": "~1.3.0",
|
||||
"angularjs-toaster": "~1.0.0",
|
||||
"angular-chart.js": "~0.8.8",
|
||||
"ngletteravatar": "~4.0.0",
|
||||
"bootswatch": "~3.3.6",
|
||||
"fontawesome": "~4.5.0",
|
||||
"satellizer": "~0.13.4",
|
||||
"angular-ui-router": "~0.2.15",
|
||||
"angular-clipboard": "~1.1.1",
|
||||
"angular-file-saver": "~1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "~1.3",
|
||||
"angular-scenario": "~1.3",
|
||||
"ngletteravatar": "~3.0.1"
|
||||
"font-awesome": "~4.5.0",
|
||||
"lodash": "~4.0.1",
|
||||
"underscore": "~1.8.3",
|
||||
"angular-smart-table": "~2.1.6",
|
||||
"angular-strap": ">= 2.2.2",
|
||||
"angular-underscore": "^0.5.0",
|
||||
"angular-translate": "^2.9.0",
|
||||
"angular-ui-switch": "~0.1.0",
|
||||
"angular-sanitize": "^1.5.0",
|
||||
"angular-file-saver": "~1.0.1",
|
||||
"angular-ui-select": "~0.17.1",
|
||||
"d3": "^3.5.17"
|
||||
},
|
||||
"resolutions": {
|
||||
"bootstrap": "~3.3.1",
|
||||
"angular": "1.3"
|
||||
"moment": ">=2.8.0 <2.11.0",
|
||||
"lodash": ">=1.3.0 <2.5.0",
|
||||
"angular": "1.4.9"
|
||||
},
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
|
@ -51,7 +51,7 @@ Basic Configuration
|
||||
CORS = False
|
||||
|
||||
|
||||
.. data:: SQLACHEMY_DATABASE_URI
|
||||
.. data:: SQLALCHEMY_DATABASE_URI
|
||||
:noindex:
|
||||
|
||||
If you have ever used sqlalchemy before this is the standard connection string used. Lemur uses a postgres database and the connection string would look something like:
|
||||
@ -61,6 +61,11 @@ Basic Configuration
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://<user>:<password>@<hostname>:5432/lemur'
|
||||
|
||||
|
||||
.. data:: LEMUR_ALLOW_WEEKEND_EXPIRATION
|
||||
:noindex:
|
||||
|
||||
Specifies whether to allow certificates created by Lemur to expire on weekends. Default is True.
|
||||
|
||||
.. data:: LEMUR_RESTRICTED_DOMAINS
|
||||
:noindex:
|
||||
|
||||
@ -143,7 +148,7 @@ and are used when Lemur creates the CSR for your certificates.
|
||||
LEMUR_DEFAULT_ORGANIZATION = "Netflix"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION_UNIT
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATIONAL_UNIT
|
||||
:noindex:
|
||||
|
||||
::
|
||||
@ -151,6 +156,14 @@ and are used when Lemur creates the CSR for your certificates.
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ISSUER_PLUGIN
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ISSUER_PLUGIN = "verisign-issuer"
|
||||
|
||||
|
||||
Notification Options
|
||||
--------------------
|
||||
|
||||
@ -174,7 +187,7 @@ Lemur supports sending certification expiration notifications through SES and SM
|
||||
Specifies which service will be delivering notification emails. Valid values are `SMTP` or `SES`
|
||||
|
||||
.. note::
|
||||
If using SMP as your provider you will need to define additional configuration options as specified by Flask-Mail.
|
||||
If using SMTP as your provider you will need to define additional configuration options as specified by Flask-Mail.
|
||||
See: `Flask-Mail <https://pythonhosted.org/Flask-Mail>`_
|
||||
|
||||
If you are using SES the email specified by the `LEMUR_MAIL` configuration will need to be verified by AWS before
|
||||
@ -268,12 +281,26 @@ For more information about how to use social logins, see: `Satellizer <https://g
|
||||
|
||||
PING_CLIENT_ID = "client-id"
|
||||
|
||||
.. data:: PING_REDIRECT_URI
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_REDIRECT_URI = "https://<yourlemurserver>/api/1/auth/ping"
|
||||
|
||||
.. data:: PING_AUTH_ENDPOINT
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
PING_AUTH_ENDPOINT = "https://<yourpingserver>/oauth2/authorize"
|
||||
|
||||
.. data:: GOOGLE_CLIENT_ID
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
GOOGLE_CLIENT_ID = "client-id"
|
||||
GOOGLE_CLIENT_ID = "client-id"
|
||||
|
||||
.. data:: GOOGLE_SECRET
|
||||
:noindex:
|
||||
@ -334,6 +361,69 @@ for those plugins.
|
||||
This is the root to be used for your CA chain
|
||||
|
||||
|
||||
Digicert Issuer Plugin
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following configuration properties are required to use the Digicert issuer plugin.
|
||||
|
||||
|
||||
.. data:: DIGICERT_URL
|
||||
:noindex:
|
||||
|
||||
This is the url for the Digicert API
|
||||
|
||||
|
||||
.. data:: DIGICERT_API_KEY
|
||||
:noindex:
|
||||
|
||||
This is the Digicert API key
|
||||
|
||||
|
||||
.. data:: DIGICERT_ORG_ID
|
||||
:noindex:
|
||||
|
||||
This is the Digicert organization ID tied to your API key
|
||||
|
||||
|
||||
.. data:: DIGICERT_INTERMEDIATE
|
||||
:noindex:
|
||||
|
||||
This is the intermediate to be used for your CA chain
|
||||
|
||||
|
||||
.. data:: DIGICERT_ROOT
|
||||
:noindex:
|
||||
|
||||
This is the root to be used for your CA chain
|
||||
|
||||
|
||||
.. data:: DIGICERT_DEFAULT_VALIDITY
|
||||
:noindex:
|
||||
|
||||
This is the default validity (in years), if no end date is specified. (Default: 1)
|
||||
|
||||
|
||||
|
||||
CFSSL Issuer Plugin
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The following configuration properties are required to use the the CFSSL issuer plugin.
|
||||
|
||||
.. data:: CFSSL_URL
|
||||
:noindex:
|
||||
|
||||
This is the URL for the CFSSL API
|
||||
|
||||
.. data:: CFSSL_ROOT
|
||||
:noindex:
|
||||
|
||||
This is the root to be used for your CA chain
|
||||
|
||||
.. data:: CFSSL_INTERMEDIATE
|
||||
:noindex:
|
||||
|
||||
This is the intermediate to be used for your CA chain
|
||||
|
||||
|
||||
AWS Source/Destination Plugin
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -588,24 +678,33 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
|
||||
|
||||
Traverses every certificate that Lemur is aware of and attempts to understand its validity.
|
||||
It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates
|
||||
validity its status is marked 'unknown'
|
||||
validity its status is marked 'unknown'.
|
||||
|
||||
|
||||
.. data:: sync
|
||||
|
||||
Sync attempts to discover certificates in the environment that were not created by Lemur. If you wish to only sync
|
||||
a few sources you can pass a comma delimited list of sources to sync
|
||||
a few sources you can pass a comma delimited list of sources to sync.
|
||||
|
||||
::
|
||||
|
||||
lemur sync source1,source2
|
||||
lemur sync -s source1,source2
|
||||
|
||||
|
||||
Additionally you can also list the available sources that Lemur can sync
|
||||
Additionally you can also list the available sources that Lemur can sync.
|
||||
|
||||
::
|
||||
|
||||
lemur sync -list
|
||||
lemur sync
|
||||
|
||||
|
||||
.. data:: notify
|
||||
|
||||
Will traverse all current notifications and see if any of them need to be triggered.
|
||||
|
||||
::
|
||||
|
||||
lemur notify
|
||||
|
||||
|
||||
Sub-commands
|
||||
@ -673,15 +772,161 @@ 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.
|
||||
|
||||
Lemur includes several plugins by default. Including extensive support for AWS, VeriSign/Symantec and CloudCA services.
|
||||
Verisign/Symantec
|
||||
-----------------
|
||||
|
||||
3rd Party Extensions
|
||||
--------------------
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Issuer
|
||||
:Description:
|
||||
Basic support for the VICE 2.0 API
|
||||
|
||||
|
||||
Cryptography
|
||||
------------
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>,
|
||||
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||
:Type:
|
||||
Issuer
|
||||
:Description:
|
||||
Toy certificate authority that creates self-signed certificate authorities.
|
||||
Allows for the creation of arbitrary authorities and end-entity certificates.
|
||||
This is *not* recommended for production use.
|
||||
|
||||
|
||||
Acme
|
||||
----
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>,
|
||||
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||
:Type:
|
||||
Issuer
|
||||
:Description:
|
||||
Adds support for the ACME protocol (including LetsEncrypt) with domain validation being handled Route53.
|
||||
|
||||
|
||||
Atlas
|
||||
-----
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Metric
|
||||
:Description:
|
||||
Adds basic support for the `Atlas <https://github.com/Netflix/atlas/wiki>`_ telemetry system.
|
||||
|
||||
|
||||
Email
|
||||
-----
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Notification
|
||||
:Description:
|
||||
Adds support for basic email notifications via SES.
|
||||
|
||||
|
||||
Slack
|
||||
-----
|
||||
|
||||
:Authors:
|
||||
Harm Weites <harm@weites.com>
|
||||
:Type:
|
||||
Notification
|
||||
:Description:
|
||||
Adds support for slack notifications.
|
||||
|
||||
|
||||
AWS
|
||||
----
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Source
|
||||
:Description:
|
||||
Uses AWS IAM as a source of certificates to manage. Supports a multi-account deployment.
|
||||
|
||||
|
||||
AWS
|
||||
----
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Destination
|
||||
:Description:
|
||||
Uses AWS IAM as a destination for Lemur generated certificates. Support a multi-account deployment.
|
||||
|
||||
|
||||
Kubernetes
|
||||
----------
|
||||
|
||||
:Authors:
|
||||
Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||
:Type:
|
||||
Destination
|
||||
:Description:
|
||||
Allows Lemur to upload generated certificates to the Kubernetes certificate store.
|
||||
|
||||
|
||||
Java
|
||||
----
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Export
|
||||
:Description:
|
||||
Generates java compatible .jks keystores and truststores from Lemur managed certificates.
|
||||
|
||||
|
||||
Openssl
|
||||
-------
|
||||
|
||||
:Authors:
|
||||
Kevin Glisson <kglisson@netflix.com>
|
||||
:Type:
|
||||
Export
|
||||
:Description:
|
||||
Leverages Openssl to support additional export formats (pkcs12)
|
||||
|
||||
|
||||
CFSSL
|
||||
-----
|
||||
|
||||
:Authors:
|
||||
Charles Hendrie <chad.hendrie@thomsonreuters.com>
|
||||
:Type:
|
||||
Issuer
|
||||
:Description:
|
||||
Basic support for generating certificates from the private certificate authority CFSSL
|
||||
|
||||
|
||||
3rd Party Plugins
|
||||
=================
|
||||
|
||||
The following plugins are available and maintained by members of the Lemur community:
|
||||
|
||||
Digicert
|
||||
--------
|
||||
|
||||
:Authors:
|
||||
Chris Dorros
|
||||
:Type:
|
||||
Issuer
|
||||
:Description:
|
||||
Adds support for basic Digicert
|
||||
:Links:
|
||||
https://github.com/opendns/lemur-digicert
|
||||
|
||||
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.
|
||||
@ -715,4 +960,3 @@ These permissions are applied to the user upon login and refreshed on every requ
|
||||
.. seealso::
|
||||
|
||||
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
|
||||
|
||||
|
@ -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
|
||||
----------------------
|
||||
|
||||
@ -194,7 +205,7 @@ 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.
|
||||
API. The following is documents and provides examples on how to make requests to the Lemur API.
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
20
docs/developer/internals/lemur.plugins.lemur_cfssl.rst
Normal file
20
docs/developer/internals/lemur.plugins.lemur_cfssl.rst
Normal file
@ -0,0 +1,20 @@
|
||||
lemur_cfssl Package
|
||||
===================
|
||||
|
||||
:mod:`lemur_cfssl` Package
|
||||
--------------------------
|
||||
|
||||
.. automodule:: lemur.plugins.lemur_cfssl
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`plugin` Module
|
||||
--------------------
|
||||
|
||||
.. automodule:: lemur.plugins.lemur_cfssl.plugin
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -27,5 +27,6 @@ Subpackages
|
||||
lemur.plugins.base
|
||||
lemur.plugins.bases
|
||||
lemur.plugins.lemur_aws
|
||||
lemur.plugins.lemur_cfssl
|
||||
lemur.plugins.lemur_email
|
||||
lemur.plugins.lemur_verisign
|
||||
|
@ -137,6 +137,12 @@ Destination
|
||||
Destination plugins allow you to propagate certificates managed by Lemur to additional third parties. This provides flexibility when
|
||||
different orchestration systems have their own way of manage certificates or there is an existing system you wish to integrate with Lemur.
|
||||
|
||||
By default destination plugins have a private key requirement. If your plugin does not require a certificates private key mark `requires_key = False`
|
||||
in the plugins base class like so::
|
||||
|
||||
class MyDestinationPlugin(DestinationPlugin):
|
||||
requires_key = False
|
||||
|
||||
The DestinationPlugin requires only one function to be implemented::
|
||||
|
||||
def upload(self, cert, private_key, cert_chain, options, **kwargs):
|
||||
@ -211,8 +217,8 @@ certificate Lemur does not know about and adding the certificate to it's invento
|
||||
|
||||
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
|
||||
|
||||
.. warning::
|
||||
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
|
||||
.. warning::
|
||||
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
|
||||
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron
|
||||
job if you need a higher resolution sync job.
|
||||
|
||||
@ -223,8 +229,8 @@ The `SourcePlugin` object requires implementation of one function::
|
||||
# request.get("some source of certificates")
|
||||
|
||||
|
||||
.. Note::
|
||||
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
|
||||
.. note::
|
||||
Often times to facilitate code re-use it makes sense put source and destination plugins into one package.
|
||||
|
||||
|
||||
Export
|
||||
@ -244,9 +250,8 @@ The `ExportPlugin` object requires the implementation of one function::
|
||||
# return "extension", passphrase, raw
|
||||
|
||||
|
||||
.. Note::
|
||||
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to
|
||||
these calls.
|
||||
.. note::
|
||||
Support of various formats sometimes relies on external tools system calls. Always be mindful of sanitizing any input to these calls.
|
||||
|
||||
|
||||
Testing
|
||||
@ -278,11 +283,7 @@ The ``conftest.py`` file is our main entry-point for py.test. We need to configu
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
pytest_plugins = [
|
||||
'lemur.utils.pytest'
|
||||
]
|
||||
from lemur.tests.conftest import * # noqa
|
||||
|
||||
|
||||
Test Cases
|
||||
@ -292,14 +293,18 @@ You can now inherit from Lemur's core test classes. These are Django-based and e
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# test_myextension.py
|
||||
from __future__ import absolute_import
|
||||
import pytest
|
||||
from lemur.tests.vectors import INTERNAL_CERTIFICATE_A_STR, INTERNAL_PRIVATE_KEY_A_STR
|
||||
|
||||
from lemur.testutils import TestCase
|
||||
def test_export_keystore(app):
|
||||
from lemur.plugins.base import plugins
|
||||
p = plugins.get('java-keystore-jks')
|
||||
options = [{'name': 'passphrase', 'value': 'test1234'}]
|
||||
with pytest.raises(Exception):
|
||||
p.export(INTERNAL_CERTIFICATE_A_STR, "", "", options)
|
||||
|
||||
class MyExtensionTest(TestCase):
|
||||
def test_simple(self):
|
||||
assert 1 != 2
|
||||
raw = p.export(INTERNAL_CERTIFICATE_A_STR, "", INTERNAL_PRIVATE_KEY_A_STR, options)
|
||||
assert raw != b""
|
||||
|
||||
|
||||
Running Tests
|
||||
@ -311,13 +316,14 @@ Running tests follows the py.test standard. As long as your test files and metho
|
||||
|
||||
$ py.test -v
|
||||
============================== test session starts ==============================
|
||||
platform darwin -- Python 2.7.9 -- py-1.4.26 -- pytest-2.6.4/python2.7
|
||||
plugins: django
|
||||
collected 1 items
|
||||
platform darwin -- Python 2.7.10, pytest-2.8.5, py-1.4.30, pluggy-0.3.1
|
||||
cachedir: .cache
|
||||
plugins: flask-0.10.0
|
||||
collected 346 items
|
||||
|
||||
tests/test_myextension.py::MyExtensionTest::test_simple PASSED
|
||||
lemur/plugins/lemur_acme/tests/test_acme.py::test_get_certificates PASSED
|
||||
|
||||
=========================== 1 passed in 0.35 seconds ============================
|
||||
|
||||
|
||||
.. SeeAlso:: Lemur bundles several plugins that use the same interfaces mentioned above. View the source: # TODO
|
||||
.. SeeAlso:: Lemur bundles several plugins that use the same interfaces mentioned above.
|
||||
|
@ -110,7 +110,7 @@ You can make some adjustments to get a better user experience::
|
||||
error_log /var/log/nginx/log/lemur.error.log;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
@ -176,7 +176,7 @@ sensitive nature of Lemur and what it controls makes this essential. This is a s
|
||||
resolver <IP DNS resolver>;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
@ -295,3 +295,25 @@ Then you can manage the process by running::
|
||||
It will start a shell from which you can start/stop/restart the service.
|
||||
|
||||
You can read all errors that might occur from /tmp/lemur.log.
|
||||
|
||||
|
||||
Periodic Tasks
|
||||
==============
|
||||
|
||||
Lemur contains a few tasks that are run and scheduled basis, currently the recommend way to run these tasks is to create
|
||||
a cron job that runs the commands.
|
||||
|
||||
There are currently three commands that could/should be run on a periodic basis:
|
||||
|
||||
- `notify`
|
||||
- `check_revoked`
|
||||
- `sync`
|
||||
|
||||
How often you run these commands is largely up to the user. `notify` and `check_revoked` are typically run at least once a day.
|
||||
`sync` is typically run every 15 minutes.
|
||||
|
||||
Example cron entries::
|
||||
|
||||
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur notify
|
||||
*/15 * * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur sync -s all
|
||||
0 22 * * * lemuruser export LEMUR_CONF=/Users/me/.lemur/lemur.conf.py; /www/lemur/bin/lemur check_revoked
|
@ -27,7 +27,7 @@ If installing Lemur on a bare Ubuntu OS you will need to grab the following pack
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql
|
||||
$ sudo apt-get install nodejs-legacy python-pip python-dev libpq-dev build-essential libssl-dev libffi-dev nginx git supervisor npm postgresql
|
||||
|
||||
.. note:: PostgreSQL is only required if your database is going to be on the same host as the webserver. npm is needed if you're installing Lemur from the source (e.g., from git).
|
||||
|
||||
@ -118,7 +118,7 @@ First, set a password for the postgres user. For this guide, we will use ``lemu
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo -u postgres psql postgres
|
||||
$ sudo -u postgres -i
|
||||
# \password postgres
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
@ -133,17 +133,8 @@ Next, we will create our new database:
|
||||
|
||||
.. _InitializingLemur:
|
||||
|
||||
Set a password for lemur user inside Postgres:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo -u postgres psql postgres
|
||||
\password lemur
|
||||
Enter new password: lemur
|
||||
Enter it again: lemur
|
||||
|
||||
Again, enter CTRL-D to exit the Postgres shell.
|
||||
|
||||
.. note::
|
||||
For this guide we assume you will use the `postgres` user to connect to your database, when deploying to a VM or container this is often all you will need. If you have a shared database it is recommend you give Lemur it's own user.
|
||||
|
||||
Initializing Lemur
|
||||
------------------
|
||||
@ -161,6 +152,7 @@ Additional notifications can be created through the UI or API. See :ref:`Creati
|
||||
$ cd /www/lemur/lemur
|
||||
$ lemur init
|
||||
|
||||
|
||||
.. note:: It is recommended that once the ``lemur`` user is created that you create individual users for every day access. There is currently no way for a user to self enroll for Lemur access, they must have an administrator create an account for them or be enrolled automatically through SSO. This can be done through the CLI or UI. See :ref:`Creating Users <CreatingUsers>` and :ref:`Command Line Interface <CommandLineInterface>` for details.
|
||||
|
||||
|
||||
@ -178,7 +170,7 @@ You'll use the builtin ``HttpProxyModule`` within Nginx to handle proxying. Edi
|
||||
::
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
@ -251,13 +243,14 @@ See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervis
|
||||
Syncing
|
||||
-------
|
||||
|
||||
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. As always, things can change outside of Lemur, but we do our best to reconcile those changes, for example, using Cron:
|
||||
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. Things change outside of Lemur we do our best to reconcile those changes. The recommended method is to use CRON:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ crontab -e
|
||||
* 3 * * * lemur sync --all
|
||||
* 3 * * * lemur check_revoked
|
||||
*/15 * * * * lemur sync -s all
|
||||
0 22 * * * lemur check_revoked
|
||||
0 22 * * * lemur notify
|
||||
|
||||
|
||||
Additional Utilities
|
||||
|
@ -1,29 +1,54 @@
|
||||
Jinja2>=2.3
|
||||
Pygments>=1.2
|
||||
Sphinx>=1.3
|
||||
docutils>=0.7
|
||||
markupsafe
|
||||
sphinxcontrib-httpdomain
|
||||
Flask==0.10.1
|
||||
Flask-RESTful==0.3.3
|
||||
Flask-SQLAlchemy==2.1
|
||||
Flask-Script==2.0.5
|
||||
Flask-Migrate==1.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
|
||||
alabaster==0.7.8
|
||||
alembic==0.8.6
|
||||
aniso8601==1.1.0
|
||||
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
|
||||
Babel==2.3.4
|
||||
bcrypt==2.0.0
|
||||
beautifulsoup4==4.4.1
|
||||
blinker==1.4
|
||||
boto==2.38.0
|
||||
cffi==1.7.0
|
||||
cryptography==1.3.2
|
||||
docutils==0.12
|
||||
enum34==1.1.6
|
||||
Flask==0.10.1
|
||||
Flask-Bcrypt==0.7.1
|
||||
Flask-Mail==0.9.1
|
||||
Flask-Migrate==1.7.0
|
||||
Flask-Principal==0.4.0
|
||||
Flask-RESTful==0.3.3
|
||||
Flask-Script==2.0.5
|
||||
Flask-SQLAlchemy==2.1
|
||||
future==0.15.2
|
||||
gunicorn==19.4.1
|
||||
idna==2.1
|
||||
imagesize==0.7.1
|
||||
inflection==0.3.1
|
||||
ipaddress==1.0.16
|
||||
itsdangerous==0.24
|
||||
Jinja2==2.8
|
||||
lockfile==0.12.2
|
||||
Mako==1.0.4
|
||||
MarkupSafe==0.23
|
||||
marshmallow==2.4.0
|
||||
marshmallow-sqlalchemy==0.8.0
|
||||
psycopg2==2.6.1
|
||||
pyasn1==0.1.9
|
||||
pycparser==2.14
|
||||
pycrypto==2.6.1
|
||||
Pygments==2.1.3
|
||||
PyJWT==1.4.0
|
||||
pyOpenSSL==0.15.1
|
||||
python-dateutil==2.5.3
|
||||
python-editor==1.0.1
|
||||
pytz==2016.4
|
||||
requests==2.9.1
|
||||
six==1.10.0
|
||||
snowballstemmer==1.2.1
|
||||
Sphinx==1.4.4
|
||||
sphinx-rtd-theme==0.1.9
|
||||
sphinxcontrib-httpdomain==1.5.0
|
||||
SQLAlchemy==1.0.13
|
||||
SQLAlchemy-Utils==0.31.4
|
||||
Werkzeug==0.11.10
|
||||
xmltodict==0.9.2
|
||||
|
@ -79,8 +79,9 @@ gulp.task('dev:styles', function () {
|
||||
'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/ng-table/dist/ng-table.css',
|
||||
'bower_components/angularjs-toaster/toaster.css',
|
||||
'bower_components/angular-ui-select/dist/select.css',
|
||||
'lemur/static/app/styles/lemur.css'
|
||||
];
|
||||
|
||||
|
@ -17,13 +17,12 @@ if 'VIRTUAL_ENV' in os.environ:
|
||||
|
||||
|
||||
def py_lint(files_modified):
|
||||
from flake8.main import DEFAULT_CONFIG
|
||||
from flake8.engine import get_style_guide
|
||||
|
||||
# remove non-py files and files which no longer exist
|
||||
files_modified = filter(lambda x: x.endswith('.py'), files_modified)
|
||||
|
||||
flake8_style = get_style_guide(parse_argv=True, config_file=DEFAULT_CONFIG)
|
||||
flake8_style = get_style_guide(parse_argv=True)
|
||||
report = flake8_style.check_files(files_modified)
|
||||
|
||||
return report.total_errors != 0
|
||||
|
@ -9,7 +9,7 @@ __title__ = "lemur"
|
||||
__summary__ = ("Certificate management and orchestration service")
|
||||
__uri__ = "https://github.com/Netflix/lemur"
|
||||
|
||||
__version__ = "0.2.2"
|
||||
__version__ = "0.4.0"
|
||||
|
||||
__author__ = "The Lemur developers"
|
||||
__email__ = "security@netflix.com"
|
||||
|
@ -11,6 +11,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from lemur import factory
|
||||
from lemur.extensions import metrics
|
||||
|
||||
from lemur.users.views import mod as users_bp
|
||||
from lemur.roles.views import mod as roles_bp
|
||||
@ -23,6 +24,7 @@ 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.endpoints.views import mod as endpoints_bp
|
||||
|
||||
from lemur.__about__ import (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
@ -46,7 +48,8 @@ LEMUR_BLUEPRINTS = (
|
||||
defaults_bp,
|
||||
plugins_bp,
|
||||
notifications_bp,
|
||||
sources_bp
|
||||
sources_bp,
|
||||
endpoints_bp
|
||||
)
|
||||
|
||||
|
||||
@ -62,7 +65,8 @@ def configure_hook(app):
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
from flask.ext.principal import PermissionDenied
|
||||
from flask import jsonify
|
||||
from werkzeug.exceptions import default_exceptions
|
||||
from lemur.decorators import crossdomain
|
||||
if app.config.get('CORS'):
|
||||
@app.after_request
|
||||
@ -70,8 +74,13 @@ def configure_hook(app):
|
||||
def after(response):
|
||||
return response
|
||||
|
||||
@app.errorhandler(PermissionDenied)
|
||||
def handle_invalid_usage(error):
|
||||
response = {'message': 'You are not allow to access this resource'}
|
||||
response.status_code = 403
|
||||
return response
|
||||
def make_json_handler(code):
|
||||
def json_handler(error):
|
||||
metrics.send('{}_status_code'.format(code), 'counter', 1)
|
||||
response = jsonify(message=str(error))
|
||||
response.status_code = code
|
||||
return response
|
||||
return json_handler
|
||||
|
||||
for code, value in default_exceptions.items():
|
||||
app.error_handler_spec[None][code] = make_json_handler(code)
|
||||
|
@ -18,32 +18,32 @@ admin_permission = Permission(RoleNeed('admin'))
|
||||
CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||
|
||||
CertificateOwner = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'role')
|
||||
|
||||
|
||||
class SensitiveDomainPermission(Permission):
|
||||
def __init__(self):
|
||||
super(SensitiveDomainPermission, self).__init__(RoleNeed('admin'))
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(ViewKeyPermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
class CertificatePermission(Permission):
|
||||
def __init__(self, certificate_id, owner, roles):
|
||||
needs = [RoleNeed('admin'), CertificateCreatorNeed(certificate_id), RoleNeed(owner)]
|
||||
for r in roles:
|
||||
needs.append(CertificateOwnerNeed(str(r)))
|
||||
|
||||
super(CertificatePermission, self).__init__(*needs)
|
||||
|
||||
|
||||
class UpdateCertificatePermission(Permission):
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
RoleMember = namedtuple('role', ['method', 'value'])
|
||||
RoleMemberNeed = partial(RoleMember, 'member')
|
||||
|
||||
|
||||
RoleUser = namedtuple('role', ['method', 'value'])
|
||||
ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
|
||||
|
||||
|
||||
class ViewRoleCredentialsPermission(Permission):
|
||||
class RoleMemberPermission(Permission):
|
||||
def __init__(self, role_id):
|
||||
need = ViewRoleCredentialsNeed(role_id)
|
||||
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin'))
|
||||
needs = [RoleNeed('admin'), RoleMemberNeed(role_id)]
|
||||
super(RoleMemberPermission, self).__init__(*needs)
|
||||
|
||||
|
||||
AuthorityCreator = namedtuple('authority', ['method', 'value'])
|
||||
|
@ -8,11 +8,9 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from builtins import bytes
|
||||
import sys
|
||||
import jwt
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
|
||||
from functools import wraps
|
||||
@ -31,20 +29,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
|
||||
|
||||
from lemur.users import service as user_service
|
||||
from lemur.auth.permissions import CertificateCreatorNeed, \
|
||||
AuthorityCreatorNeed, ViewRoleCredentialsNeed
|
||||
|
||||
|
||||
def base64url_decode(data):
|
||||
rem = len(data) % 4
|
||||
|
||||
if rem > 0:
|
||||
data += '=' * (4 - rem)
|
||||
|
||||
return base64.urlsafe_b64decode(bytes(data.encode('latin-1')))
|
||||
|
||||
|
||||
def base64url_encode(data):
|
||||
return base64.urlsafe_b64encode(data).replace('=', '')
|
||||
AuthorityCreatorNeed, RoleMemberNeed
|
||||
|
||||
|
||||
def get_rsa_public_key(n, e):
|
||||
@ -55,8 +40,13 @@ def get_rsa_public_key(n, e):
|
||||
:param e:
|
||||
:return: a RSA Public Key in PEM format
|
||||
"""
|
||||
n = int(binascii.hexlify(base64url_decode(n)), 16)
|
||||
e = int(binascii.hexlify(base64url_decode(e)), 16)
|
||||
if sys.version_info >= (3, 0):
|
||||
n = int(binascii.hexlify(jwt.utils.base64url_decode(bytes(n, 'utf-8'))), 16)
|
||||
e = int(binascii.hexlify(jwt.utils.base64url_decode(bytes(e, 'utf-8'))), 16)
|
||||
else:
|
||||
n = int(binascii.hexlify(jwt.utils.base64url_decode(str(n))), 16)
|
||||
e = int(binascii.hexlify(jwt.utils.base64url_decode(str(e))), 16)
|
||||
|
||||
pub = RSAPublicNumbers(e, n).public_key(default_backend())
|
||||
return pub.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
@ -75,8 +65,8 @@ def create_token(user):
|
||||
expiration_delta = timedelta(days=int(current_app.config.get('LEMUR_TOKEN_EXPIRATION', 1)))
|
||||
payload = {
|
||||
'sub': user.id,
|
||||
'iat': datetime.now(),
|
||||
'exp': datetime.now() + expiration_delta
|
||||
'iat': datetime.utcnow(),
|
||||
'exp': datetime.utcnow() + expiration_delta
|
||||
}
|
||||
token = jwt.encode(payload, current_app.config['LEMUR_TOKEN_SECRET'])
|
||||
return token.decode('unicode_escape')
|
||||
@ -138,13 +128,13 @@ def fetch_token_header(token):
|
||||
raise jwt.DecodeError('Not enough segments')
|
||||
|
||||
try:
|
||||
return json.loads(base64url_decode(header_segment))
|
||||
if sys.version_info >= (3, 0):
|
||||
return json.loads(jwt.utils.base64url_decode(header_segment).decode('utf-8'))
|
||||
else:
|
||||
return json.loads(jwt.utils.base64url_decode(header_segment))
|
||||
except TypeError as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
except binascii.Error as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
|
||||
|
||||
@identity_loaded.connect
|
||||
@ -165,8 +155,8 @@ def on_identity_loaded(sender, identity):
|
||||
# identity with the roles that the user provides
|
||||
if hasattr(user, 'roles'):
|
||||
for role in user.roles:
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.id))
|
||||
identity.provides.add(RoleNeed(role.name))
|
||||
identity.provides.add(RoleMemberNeed(role.id))
|
||||
|
||||
# apply ownership for authorities
|
||||
if hasattr(user, 'authorities'):
|
||||
|
@ -5,15 +5,17 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import sys
|
||||
import jwt
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from flask import g, Blueprint, current_app
|
||||
from flask import Blueprint, current_app
|
||||
|
||||
from flask.ext.restful import reqparse, Resource, Api
|
||||
from flask.ext.principal import Identity, identity_changed
|
||||
|
||||
from lemur.extensions import metrics
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
from lemur.users import service as user_service
|
||||
@ -96,13 +98,13 @@ 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):
|
||||
"""
|
||||
@ -139,8 +141,14 @@ class Ping(Resource):
|
||||
user_api_url = current_app.config.get('PING_USER_API_URL')
|
||||
|
||||
# the secret and cliendId will be given to you when you signup for the provider
|
||||
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
|
||||
headers = {'Authorization': 'Basic {0}'.format(basic)}
|
||||
token = '{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET"))
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
basic = base64.b64encode(bytes(token, 'utf-8'))
|
||||
headers = {'authorization': 'basic {0}'.format(basic.decode('utf-8'))}
|
||||
else:
|
||||
basic = base64.b64encode(token, 'utf-8')
|
||||
headers = {'authorization': 'basic {0}'.format(basic)}
|
||||
|
||||
# exchange authorization code for access token.
|
||||
|
||||
@ -164,7 +172,10 @@ class Ping(Resource):
|
||||
|
||||
# validate your token based on the key it was signed with
|
||||
try:
|
||||
jwt.decode(id_token, secret, algorithms=[algo], audience=args['clientId'])
|
||||
if sys.version_info >= (3, 0):
|
||||
jwt.decode(id_token, secret.decode('utf-8'), algorithms=[algo], audience=args['clientId'])
|
||||
else:
|
||||
jwt.decode(id_token, secret, algorithms=[algo], audience=args['clientId'])
|
||||
except jwt.DecodeError:
|
||||
return dict(message='Token is invalid'), 403
|
||||
except jwt.ExpiredSignatureError:
|
||||
@ -179,6 +190,7 @@ class Ping(Resource):
|
||||
profile = r.json()
|
||||
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
|
||||
# update their google 'roles'
|
||||
roles = []
|
||||
@ -189,10 +201,13 @@ class Ping(Resource):
|
||||
role = role_service.create(group, description='This is a google group based role created by Lemur')
|
||||
roles.append(role)
|
||||
|
||||
# if we get an sso user create them an account
|
||||
# we still pick a random password in case sso is down
|
||||
if not user:
|
||||
role = role_service.get_by_name(profile['email'])
|
||||
if not role:
|
||||
role = role_service.create(profile['email'], description='This is a user specific role')
|
||||
roles.append(role)
|
||||
|
||||
# if we get an sso user create them an account
|
||||
if not user:
|
||||
# every user is an operator (tied to a default role)
|
||||
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
|
||||
v = role_service.get_by_name(current_app.config.get('LEMUR_DEFAULT_ROLE'))
|
||||
@ -266,6 +281,7 @@ class Google(Resource):
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
|
||||
if user:
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
@ -273,7 +289,7 @@ class Providers(Resource):
|
||||
def get(self):
|
||||
active_providers = []
|
||||
|
||||
for provider in current_app.config.get("ACTIVE_PROVIDERS"):
|
||||
for provider in current_app.config.get("ACTIVE_PROVIDERS", []):
|
||||
provider = provider.lower()
|
||||
|
||||
if provider == "google":
|
||||
|
@ -6,53 +6,43 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime, PassiveDefault, Boolean
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.certificates.models import get_cn, get_not_after, get_not_before
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.models import roles_authorities
|
||||
|
||||
|
||||
class Authority(db.Model):
|
||||
__tablename__ = 'authorities'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
owner = Column(String(128), nullable=False)
|
||||
name = Column(String(128), unique=True)
|
||||
body = Column(Text())
|
||||
chain = Column(Text())
|
||||
bits = Column(Integer())
|
||||
cn = Column(String(128))
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
active = Column(Boolean, default=True)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
plugin_name = Column(String(64))
|
||||
description = Column(Text)
|
||||
options = Column(JSON)
|
||||
roles = relationship('Role', backref=db.backref('authority'), lazy='dynamic')
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
roles = relationship('Role', secondary=roles_authorities, passive_deletes=True, backref=db.backref('authority'), lazy='dynamic')
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
certificates = relationship("Certificate", backref='authority')
|
||||
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
||||
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
||||
|
||||
def __init__(self, name, owner, plugin_name, body, roles=None, chain=None, description=None):
|
||||
self.name = name
|
||||
self.body = body
|
||||
self.chain = chain
|
||||
self.owner = owner
|
||||
self.plugin_name = plugin_name
|
||||
cert = x509.load_pem_x509_certificate(str(body), default_backend())
|
||||
self.cn = get_cn(cert)
|
||||
self.not_before = get_not_before(cert)
|
||||
self.not_after = get_not_after(cert)
|
||||
self.roles = roles
|
||||
self.description = description
|
||||
def __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']
|
||||
|
||||
def as_dict(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
@property
|
||||
def plugin(self):
|
||||
return plugins.get(self.plugin_name)
|
||||
|
||||
def serialize(self):
|
||||
blob = self.as_dict()
|
||||
return blob
|
||||
def __repr__(self):
|
||||
return "Authority(name={name})".format(name=self.name)
|
||||
|
119
lemur/authorities/schemas.py
Normal file
119
lemur/authorities/schemas.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""
|
||||
.. 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, pre_load
|
||||
from marshmallow import validate
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema, ExtensionSchema, AssociatedAuthoritySchema, AssociatedRoleSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.common import validators, missing
|
||||
|
||||
from lemur.common.fields import ArrowDateTime
|
||||
|
||||
|
||||
class AuthorityInputSchema(LemurInputSchema):
|
||||
name = fields.String(required=True)
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
common_name = fields.String(required=True, validate=validators.sensitive_domain)
|
||||
|
||||
validity_start = ArrowDateTime()
|
||||
validity_end = ArrowDateTime()
|
||||
validity_years = fields.Integer()
|
||||
|
||||
# certificate body fields
|
||||
organizational_unit = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'))
|
||||
organization = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'))
|
||||
location = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_LOCATION'))
|
||||
country = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_COUNTRY'))
|
||||
state = fields.String(missing=lambda: current_app.config.get('LEMUR_DEFAULT_STATE'))
|
||||
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
||||
# signing related options
|
||||
type = fields.String(validate=validate.OneOf(['root', 'subca']), missing='root')
|
||||
parent = fields.Nested(AssociatedAuthoritySchema)
|
||||
signing_algorithm = fields.String(validate=validate.OneOf(['sha256WithRSA', 'sha1WithRSA']), missing='sha256WithRSA')
|
||||
key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048')
|
||||
key_name = fields.String()
|
||||
sensitivity = fields.String(validate=validate.OneOf(['medium', 'high']), missing='medium')
|
||||
serial_number = fields.Integer()
|
||||
first_serial = fields.Integer(missing=1)
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
roles = fields.Nested(AssociatedRoleSchema(many=True))
|
||||
|
||||
@validates_schema
|
||||
def validate_dates(self, data):
|
||||
validators.dates(data)
|
||||
|
||||
@validates_schema
|
||||
def validate_subca(self, data):
|
||||
if data['type'] == 'subca':
|
||||
if not data.get('parent'):
|
||||
raise ValidationError("If generating a subca parent 'authority' must be specified.")
|
||||
|
||||
@pre_load
|
||||
def ensure_dates(self, data):
|
||||
return missing.convert_validity_years(data)
|
||||
|
||||
|
||||
class AuthorityUpdateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
roles = fields.Nested(AssociatedRoleSchema(many=True))
|
||||
|
||||
|
||||
class RootAuthorityCertificateOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
bits = fields.Integer()
|
||||
body = fields.String()
|
||||
chain = fields.String()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
cn = fields.String()
|
||||
not_after = fields.DateTime()
|
||||
not_before = fields.DateTime()
|
||||
owner = fields.Email()
|
||||
status = fields.Boolean()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
|
||||
|
||||
class AuthorityOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
owner = fields.Email()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
active = fields.Boolean()
|
||||
options = fields.Dict()
|
||||
roles = fields.List(fields.Nested(AssociatedRoleSchema))
|
||||
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
|
||||
|
||||
|
||||
class AuthorityNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
owner = fields.Email()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
active = fields.Boolean()
|
||||
|
||||
|
||||
authority_update_schema = AuthorityUpdateSchema()
|
||||
authority_input_schema = AuthorityInputSchema()
|
||||
authority_output_schema = AuthorityOutputSchema()
|
||||
authorities_output_schema = AuthorityOutputSchema(many=True)
|
@ -9,17 +9,13 @@
|
||||
|
||||
"""
|
||||
from flask import g
|
||||
from flask import current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.certificates.service import upload
|
||||
|
||||
|
||||
def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
@ -31,8 +27,9 @@ def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
:return:
|
||||
"""
|
||||
authority = get(authority_id)
|
||||
|
||||
if roles:
|
||||
authority = database.update_list(authority, 'roles', Role, roles)
|
||||
authority.roles = roles
|
||||
|
||||
if active:
|
||||
authority.active = active
|
||||
@ -42,45 +39,39 @@ def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
return database.update(authority)
|
||||
|
||||
|
||||
def create(kwargs):
|
||||
def mint(**kwargs):
|
||||
"""
|
||||
Create a new authority.
|
||||
Creates the authority based on the plugin provided.
|
||||
"""
|
||||
issuer = kwargs['plugin']['plugin_object']
|
||||
values = issuer.create_authority(kwargs)
|
||||
|
||||
# support older plugins
|
||||
if len(values) == 3:
|
||||
body, chain, roles = values
|
||||
private_key = None
|
||||
elif len(values) == 4:
|
||||
body, private_key, chain, roles = values
|
||||
|
||||
roles = create_authority_roles(roles, kwargs['owner'], kwargs['plugin']['plugin_object'].title)
|
||||
return body, private_key, chain, roles
|
||||
|
||||
|
||||
def create_authority_roles(roles, owner, plugin_title):
|
||||
"""
|
||||
Creates all of the necessary authority roles.
|
||||
:param roles:
|
||||
:return:
|
||||
"""
|
||||
|
||||
issuer = plugins.get(kwargs.get('pluginName'))
|
||||
|
||||
kwargs['creator'] = g.current_user.email
|
||||
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
|
||||
|
||||
cert = Certificate(cert_body, chain=intermediate)
|
||||
cert.owner = kwargs['ownerEmail']
|
||||
|
||||
if kwargs['caType'] == 'subca':
|
||||
cert.description = "This is the ROOT certificate for the {0} sub certificate authority the parent \
|
||||
authority is {1}.".format(kwargs.get('caName'), kwargs.get('caParent'))
|
||||
else:
|
||||
cert.description = "This is the ROOT certificate for the {0} certificate authority.".format(
|
||||
kwargs.get('caName')
|
||||
)
|
||||
|
||||
cert.user = g.current_user
|
||||
|
||||
cert.notifications = notification_service.create_default_expiration_notifications(
|
||||
'DEFAULT_SECURITY',
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
||||
)
|
||||
|
||||
# we create and attach any roles that the issuer gives us
|
||||
role_objs = []
|
||||
for r in issuer_roles:
|
||||
|
||||
role = role_service.create(
|
||||
r['name'],
|
||||
password=r['password'],
|
||||
description="{0} auto generated role".format(kwargs.get('pluginName')),
|
||||
username=r['username'])
|
||||
for r in roles:
|
||||
role = role_service.get_by_name(r['name'])
|
||||
if not role:
|
||||
role = role_service.create(
|
||||
r['name'],
|
||||
password=r['password'],
|
||||
description="Auto generated role for {0}".format(plugin_title),
|
||||
username=r['username'])
|
||||
|
||||
# the user creating the authority should be able to administer it
|
||||
if role.username == 'admin':
|
||||
@ -88,25 +79,44 @@ 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, private_key, chain, roles = mint(**kwargs)
|
||||
|
||||
g.user.roles = list(set(list(g.user.roles) + roles))
|
||||
|
||||
kwargs['body'] = body
|
||||
kwargs['private_key'] = private_key
|
||||
kwargs['chain'] = chain
|
||||
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert = upload(**kwargs)
|
||||
kwargs['authority_certificate'] = cert
|
||||
|
||||
authority = Authority(**kwargs)
|
||||
authority = database.create(authority)
|
||||
g.user.authorities.append(authority)
|
||||
|
||||
# the owning dl or role should have this authority associated with it
|
||||
owner_role = role_service.get_by_name(kwargs['ownerEmail'])
|
||||
owner_role.authority = authority
|
||||
|
||||
g.current_user.authorities.append(authority)
|
||||
|
||||
metrics.send('authority_created', 'counter', 1, metric_tags=dict(owner=authority.owner))
|
||||
return authority
|
||||
|
||||
|
||||
@ -149,14 +159,9 @@ def get_authority_role(ca_name):
|
||||
:param ca_name:
|
||||
"""
|
||||
if g.current_user.is_admin:
|
||||
authority = get_by_name(ca_name)
|
||||
# TODO we should pick admin ca roles for admin
|
||||
return authority.roles[0]
|
||||
return role_service.get_by_name("{0}_admin".format(ca_name))
|
||||
else:
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
if role.authority.name == ca_name:
|
||||
return role
|
||||
return role_service.get_by_name("{0}_operator".format(ca_name))
|
||||
|
||||
|
||||
def render(args):
|
||||
@ -166,10 +171,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:
|
||||
@ -182,14 +183,12 @@ def render(args):
|
||||
# we make sure that a user can only use an authority they either own are are a member of - admins can see all
|
||||
if not g.current_user.is_admin:
|
||||
authority_ids = []
|
||||
for authority in g.current_user.authorities:
|
||||
authority_ids.append(authority.id)
|
||||
|
||||
for role in g.current_user.roles:
|
||||
if role.authority:
|
||||
authority_ids.append(role.authority.id)
|
||||
for authority in role.authorities:
|
||||
authority_ids.append(authority.id)
|
||||
query = query.filter(Authority.id.in_(authority_ids))
|
||||
|
||||
query = database.find_all(query, Authority, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Authority, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Authority, args)
|
||||
|
@ -5,32 +5,19 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint, g
|
||||
from flask.ext.restful import reqparse, fields, Api
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.authorities import service
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.certificates import service as certificate_service
|
||||
from lemur.common.utils import paginated_parser
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.auth.permissions import AuthorityPermission
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.certificates import service as certificate_service
|
||||
|
||||
from lemur.authorities import service
|
||||
from lemur.authorities.schemas import authority_input_schema, authority_output_schema, authorities_output_schema, authority_update_schema
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'owner': fields.String,
|
||||
'description': fields.String,
|
||||
'options': fields.Raw,
|
||||
'pluginName': fields.String,
|
||||
'body': fields.String,
|
||||
'chain': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
mod = Blueprint('authorities', __name__)
|
||||
api = Api(mod)
|
||||
@ -42,7 +29,7 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(AuthoritiesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, authorities_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /authorities
|
||||
@ -66,20 +53,44 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
@ -87,7 +98,7 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair. format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -98,8 +109,8 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(authority_input_schema, authority_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /authorities
|
||||
|
||||
@ -113,31 +124,30 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"caDN": {
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "A Location",
|
||||
"organization": "ExampleInc",
|
||||
"organizationalUnit": "Operations",
|
||||
"commonName": "a common name"
|
||||
},
|
||||
"caType": "root",
|
||||
"caSigningAlgo": "sha256WithRSA",
|
||||
"caSensitivity": "medium",
|
||||
{
|
||||
"country": "US",
|
||||
"state": "California",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations",
|
||||
"type": "root",
|
||||
"signingAlgorithm": "sha256WithRSA",
|
||||
"sensitivity": "medium",
|
||||
"keyType": "RSA2048",
|
||||
"pluginName": "cloudca",
|
||||
"validityStart": "2015-06-11T07:00:00.000Z",
|
||||
"validityEnd": "2015-06-13T07:00:00.000Z",
|
||||
"caName": "DoctestCA",
|
||||
"ownerEmail": "jimbob@example.com",
|
||||
"caDescription": "Example CA",
|
||||
"extensions": {
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
}
|
||||
"plugin": {
|
||||
"slug": "cloudca-issuer",
|
||||
},
|
||||
}
|
||||
"name": "TimeTestAuthority5",
|
||||
"owner": "secure@example.com",
|
||||
"description": "test",
|
||||
"commonName": "AcommonName",
|
||||
"validityYears": "20",
|
||||
"extensions": {
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
},
|
||||
"custom": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -148,57 +158,67 @@ class AuthoritiesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:arg caName: authority's name
|
||||
:arg caDescription: a sensible description about what the CA with be used for
|
||||
:arg ownerEmail: the team or person who 'owns' this authority
|
||||
|
||||
:arg name: authority's name
|
||||
:arg description: a sensible description about what the CA with be used for
|
||||
:arg owner: the team or person who 'owns' this authority
|
||||
:arg validityStart: when this authority should start issuing certificates
|
||||
:arg validityEnd: when this authority should stop issuing certificates
|
||||
:arg validityYears: starting from `now` how many years into the future the authority should be valid
|
||||
:arg extensions: certificate extensions
|
||||
:arg pluginName: name of the plugin to create the authority
|
||||
:arg caType: the type of authority (root/subca)
|
||||
:arg caParent: the parent authority if this is to be a subca
|
||||
:arg caSigningAlgo: algorithm used to sign the authority
|
||||
:arg plugin: name of the plugin to create the authority
|
||||
:arg type: the type of authority (root/subca)
|
||||
:arg parent: the parent authority if this is to be a subca
|
||||
:arg signingAlgorithm: algorithm used to sign the authority
|
||||
:arg keyType: key type
|
||||
:arg caSensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
|
||||
:arg sensitivity: the sensitivity of the root key, for CloudCA this determines if the root keys are stored
|
||||
in an HSM
|
||||
:arg caKeyName: name of the key to store in the HSM (CloudCA)
|
||||
:arg caSerialNumber: serial number of the authority
|
||||
:arg caFirstSerial: specifies the starting serial number for certificates issued off of this authority
|
||||
:arg keyName: name of the key to store in the HSM (CloudCA)
|
||||
:arg serialNumber: serial number of the authority
|
||||
:arg firstSerial: specifies the starting serial number for certificates issued off of this authority
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('caName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caDescription', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('ownerEmail', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caDN', type=dict, location='json', required=False)
|
||||
self.reqparse.add_argument('validityStart', type=str, location='json', required=False) # TODO validate
|
||||
self.reqparse.add_argument('validityEnd', type=str, location='json', required=False) # TODO validate
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json', required=False)
|
||||
self.reqparse.add_argument('pluginName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('caType', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caParent', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSigningAlgo', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('keyType', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSensitivity', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caKeyName', type=str, location='json', required=False)
|
||||
self.reqparse.add_argument('caSerialNumber', type=int, location='json', required=False)
|
||||
self.reqparse.add_argument('caFirstSerial', type=int, location='json', required=False)
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args)
|
||||
return service.create(**data)
|
||||
|
||||
|
||||
class Authorities(AuthenticatedResource):
|
||||
@ -206,7 +226,7 @@ class Authorities(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Authorities, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, authority_output_schema)
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
.. http:get:: /authorities/1
|
||||
@ -230,26 +250,36 @@ class Authorities(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:arg description: a sensible description about what the CA with be used for
|
||||
:arg owner: the team or person who 'owns' this authority
|
||||
:arg active: set whether this authoritity is currently in use
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.get(authority_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, authority_id):
|
||||
@validate_schema(authority_update_schema, authority_output_schema)
|
||||
def put(self, authority_id, data=None):
|
||||
"""
|
||||
.. http:put:: /authorities/1
|
||||
|
||||
@ -264,11 +294,42 @@ class Authorities(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"roles": [],
|
||||
"active": false,
|
||||
"owner": "bob@example.com",
|
||||
"description": "this is authority1"
|
||||
}
|
||||
"name": "TestAuthority5",
|
||||
"roles": [{
|
||||
"id": 566,
|
||||
"name": "TestAuthority5_admin"
|
||||
}, {
|
||||
"id": 567,
|
||||
"name": "TestAuthority5_operator"
|
||||
}, {
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}],
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----",
|
||||
"status": null,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority5 certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-03T00:00:51+00:00",
|
||||
"notAfter": "2036-06-03T23:59:51+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2280,
|
||||
"name": "TestAuthority5"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 44,
|
||||
"description": "This is the ROOT certificate for the TestAuthority5 certificate authority."
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -279,64 +340,74 @@ class Authorities(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----begin ...",
|
||||
"body": "-----begin ...",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05t17:09:39",
|
||||
"notAfter": "2015-06-10t17:09:39"
|
||||
"options": null
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('roles', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('owner', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json', required=True)
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = service.get(authority_id)
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
|
||||
if not authority:
|
||||
return dict(message='Not Found'), 404
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
permission = AuthorityPermission(authority_id, roles)
|
||||
|
||||
# we want to make sure that we cannot add roles that we are not members of
|
||||
if not g.current_user.is_admin:
|
||||
role_ids = set([r['id'] for r in args['roles']])
|
||||
user_role_ids = set([r.id for r in g.current_user.roles])
|
||||
|
||||
if not role_ids.issubset(user_role_ids):
|
||||
return dict(message="You are not allowed to associate a role which you are not a member of"), 400
|
||||
|
||||
if permission.can():
|
||||
return service.update(
|
||||
authority_id,
|
||||
owner=args['owner'],
|
||||
description=args['description'],
|
||||
active=args['active'],
|
||||
roles=args['roles']
|
||||
owner=data['owner'],
|
||||
description=data['description'],
|
||||
active=data['active'],
|
||||
roles=data['roles']
|
||||
)
|
||||
|
||||
return dict(message="You are not authorized to update this authority"), 403
|
||||
return dict(message="You are not authorized to update this authority."), 403
|
||||
|
||||
|
||||
class CertificateAuthority(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateAuthority, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, authority_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/authority
|
||||
@ -360,16 +431,42 @@ class CertificateAuthority(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginName": null,
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"name": "TestAuthority",
|
||||
"roles": [{
|
||||
"id": 123,
|
||||
"name": "secure@example.com"
|
||||
}, {
|
||||
"id": 564,
|
||||
"name": "TestAuthority_admin"
|
||||
}, {
|
||||
"id": 565,
|
||||
"name": "TestAuthority_operator"
|
||||
}],
|
||||
"options": null,
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39"
|
||||
"options": null
|
||||
"authorityCertificate": {
|
||||
"body": "-----BEGIN CERTIFICATE-----IyMzU5MTVaMHk...",
|
||||
"status": true,
|
||||
"cn": "AcommonName",
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority.",
|
||||
"chain": "",
|
||||
"notBefore": "2016-06-02T00:00:15+00:00",
|
||||
"notAfter": "2023-06-02T23:59:15+00:00",
|
||||
"owner": "secure@example.com",
|
||||
"user": {
|
||||
"username": "joe@example.com",
|
||||
"active": true,
|
||||
"email": "joe@example.com",
|
||||
"id": 3
|
||||
},
|
||||
"active": true,
|
||||
"bits": 2048,
|
||||
"id": 2235,
|
||||
"name": "TestAuthority"
|
||||
},
|
||||
"owner": "secure@example.com",
|
||||
"id": 43,
|
||||
"description": "This is the ROOT certificate for the TestAuthority certificate authority."
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -378,10 +475,35 @@ class CertificateAuthority(AuthenticatedResource):
|
||||
"""
|
||||
cert = certificate_service.get(certificate_id)
|
||||
if not cert:
|
||||
return dict(message="Certificate not found"), 404
|
||||
return dict(message="Certificate not found."), 404
|
||||
|
||||
return cert.authority
|
||||
|
||||
|
||||
class AuthorityVisualizations(AuthenticatedResource):
|
||||
def get(self, authority_id):
|
||||
"""
|
||||
{"name": "flare",
|
||||
"children": [
|
||||
{
|
||||
"name": "analytics",
|
||||
"children": [
|
||||
{
|
||||
"name": "cluster",
|
||||
"children": [
|
||||
{"name": "AgglomerativeCluster", "size": 3938},
|
||||
{"name": "CommunityStructure", "size": 3812},
|
||||
{"name": "HierarchicalCluster", "size": 6714},
|
||||
{"name": "MergeEdge", "size": 743}
|
||||
]
|
||||
}
|
||||
}
|
||||
]}
|
||||
"""
|
||||
authority = service.get(authority_id)
|
||||
return dict(name=authority.name, children=[{"name": c.name} for c in authority.certificates])
|
||||
|
||||
api.add_resource(AuthoritiesList, '/authorities', endpoint='authorities')
|
||||
api.add_resource(Authorities, '/authorities/<int:authority_id>', endpoint='authority')
|
||||
api.add_resource(AuthorityVisualizations, '/authorities/<int:authority_id>/visualize', endpoint='authority_visualizations')
|
||||
api.add_resource(CertificateAuthority, '/certificates/<int:certificate_id>/authority', endpoint='certificateAuthority')
|
||||
|
@ -6,280 +6,147 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import datetime
|
||||
|
||||
import lemur.common.utils
|
||||
from flask import current_app
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql.expression import case
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||
|
||||
from lemur.utils import Vault
|
||||
from lemur.database import db
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||
certificate_destination_associations, certificate_notification_associations, \
|
||||
certificate_replacement_associations
|
||||
certificate_replacement_associations, roles_certificates
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.utils import Vault
|
||||
|
||||
from lemur.common import defaults
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
|
||||
def create_name(issuer, not_before, not_after, subject, san):
|
||||
"""
|
||||
Create a name for our certificate. A naming standard
|
||||
is based on a series of templates. The name includes
|
||||
useful information such as Common Name, Validation dates,
|
||||
and Issuer.
|
||||
def get_or_increase_name(name):
|
||||
name = '-'.join(name.strip().split(' '))
|
||||
count = Certificate.query.filter(Certificate.name.ilike('{0}%'.format(name))).count()
|
||||
|
||||
:param san:
|
||||
:param subject:
|
||||
:param not_after:
|
||||
:param issuer:
|
||||
:param not_before:
|
||||
:rtype : str
|
||||
:return:
|
||||
"""
|
||||
if san:
|
||||
t = SAN_NAMING_TEMPLATE
|
||||
else:
|
||||
t = DEFAULT_NAMING_TEMPLATE
|
||||
if count >= 1:
|
||||
return name + '-' + str(count)
|
||||
|
||||
temp = t.format(
|
||||
subject=subject,
|
||||
issuer=issuer,
|
||||
not_before=not_before.strftime('%Y%m%d'),
|
||||
not_after=not_after.strftime('%Y%m%d')
|
||||
)
|
||||
|
||||
# NOTE we may want to give more control over naming
|
||||
# aws doesn't allow special chars except '-'
|
||||
disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
disallowed_chars = disallowed_chars.replace("-", "")
|
||||
disallowed_chars = disallowed_chars.replace(".", "")
|
||||
temp = temp.replace('*', "WILDCARD")
|
||||
|
||||
for c in disallowed_chars:
|
||||
temp = temp.replace(c, "")
|
||||
|
||||
# white space is silly too
|
||||
return temp.replace(" ", "-")
|
||||
|
||||
|
||||
def get_signing_algorithm(cert):
|
||||
return cert.signature_hash_algorithm.name
|
||||
|
||||
|
||||
def get_cn(cert):
|
||||
"""
|
||||
Attempts to get a sane common name from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Common name or None
|
||||
"""
|
||||
return cert.subject.get_attributes_for_oid(
|
||||
x509.OID_COMMON_NAME
|
||||
)[0].value.strip()
|
||||
|
||||
|
||||
def get_domains(cert):
|
||||
"""
|
||||
Attempts to get an domains listed in a certificate.
|
||||
If 'subjectAltName' extension is not available we simply
|
||||
return the common name.
|
||||
|
||||
:param cert:
|
||||
:return: List of domains
|
||||
"""
|
||||
domains = []
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME)
|
||||
entries = ext.value.get_values_for_type(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e))
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def get_serial(cert):
|
||||
"""
|
||||
Fetch the serial number from the certificate.
|
||||
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
|
||||
|
||||
def is_san(cert):
|
||||
"""
|
||||
Determines if a given certificate is a SAN certificate.
|
||||
SAN certificates are simply certificates that cover multiple domains.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
if len(get_domains(cert)) > 1:
|
||||
return True
|
||||
|
||||
|
||||
def is_wildcard(cert):
|
||||
"""
|
||||
Determines if certificate is a wildcard certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Bool
|
||||
"""
|
||||
domains = get_domains(cert)
|
||||
if len(domains) == 1 and domains[0][0:1] == "*":
|
||||
return True
|
||||
|
||||
if cert.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value[0:1] == "*":
|
||||
return True
|
||||
|
||||
|
||||
def get_bitstrength(cert):
|
||||
"""
|
||||
Calculates a certificates public key bit length.
|
||||
|
||||
:param cert:
|
||||
:return: Integer
|
||||
"""
|
||||
return cert.public_key().key_size
|
||||
|
||||
|
||||
def get_issuer(cert):
|
||||
"""
|
||||
Gets a sane issuer from a given certificate.
|
||||
|
||||
:param cert:
|
||||
:return: Issuer
|
||||
"""
|
||||
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
|
||||
try:
|
||||
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
|
||||
for c in delchars:
|
||||
issuer = issuer.replace(c, "")
|
||||
return issuer
|
||||
except Exception as e:
|
||||
current_app.logger.error("Unable to get issuer! {0}".format(e))
|
||||
|
||||
|
||||
def get_not_before(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_before' field.
|
||||
This field denotes the first date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_before
|
||||
|
||||
|
||||
def get_not_after(cert):
|
||||
"""
|
||||
Gets the naive datetime of the certificates 'not_after' field.
|
||||
This field denotes the last date in time which the given certificate
|
||||
is valid.
|
||||
|
||||
:param cert:
|
||||
:return: Datetime
|
||||
"""
|
||||
return cert.not_valid_after
|
||||
|
||||
|
||||
def get_name_from_arn(arn):
|
||||
"""
|
||||
Extract the certificate name from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: name of the certificate as uploaded to AWS
|
||||
"""
|
||||
return arn.split("/", 1)[1]
|
||||
|
||||
|
||||
def get_account_number(arn):
|
||||
"""
|
||||
Extract the account number from an arn.
|
||||
|
||||
:param arn: IAM SSL arn
|
||||
:return: account number associated with ARN
|
||||
"""
|
||||
return arn.split(":")[4]
|
||||
return name
|
||||
|
||||
|
||||
class Certificate(db.Model):
|
||||
__tablename__ = 'certificates'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
body = Column(Text())
|
||||
private_key = Column(Vault)
|
||||
status = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
name = Column(String(128))
|
||||
owner = Column(String(128), nullable=False)
|
||||
name = Column(String(128), unique=True)
|
||||
description = Column(String(1024))
|
||||
notify = Column(Boolean, default=True)
|
||||
|
||||
body = Column(Text(), nullable=False)
|
||||
chain = Column(Text())
|
||||
bits = Column(Integer())
|
||||
private_key = Column(Vault)
|
||||
|
||||
issuer = Column(String(128))
|
||||
serial = Column(String(128))
|
||||
cn = Column(String(128))
|
||||
description = Column(String(1024))
|
||||
active = Column(Boolean, default=True)
|
||||
san = Column(String(1024))
|
||||
deleted = Column(Boolean, index=True)
|
||||
|
||||
not_before = Column(DateTime)
|
||||
not_after = Column(DateTime)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
|
||||
signing_algorithm = Column(String(128))
|
||||
status = Column(String(128))
|
||||
bits = Column(Integer())
|
||||
san = Column(String(1024)) # TODO this should be migrated to boolean
|
||||
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
|
||||
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
|
||||
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
|
||||
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
|
||||
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
|
||||
roles = relationship("Role", secondary=roles_certificates, backref="certificate")
|
||||
replaces = relationship("Certificate",
|
||||
secondary=certificate_replacement_associations,
|
||||
primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa
|
||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||
backref='replaced')
|
||||
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
|
||||
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
|
||||
|
||||
def __init__(self, body, private_key=None, chain=None):
|
||||
self.body = body
|
||||
# We encrypt the private_key on creation
|
||||
self.private_key = private_key
|
||||
self.chain = chain
|
||||
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
|
||||
self.signing_algorithm = get_signing_algorithm(cert)
|
||||
self.bits = get_bitstrength(cert)
|
||||
self.issuer = get_issuer(cert)
|
||||
self.serial = get_serial(cert)
|
||||
self.cn = get_cn(cert)
|
||||
self.san = is_san(cert)
|
||||
self.not_before = get_not_before(cert)
|
||||
self.not_after = get_not_after(cert)
|
||||
self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san)
|
||||
endpoints = relationship("Endpoint", backref='certificate')
|
||||
|
||||
for domain in get_domains(cert):
|
||||
def __init__(self, **kwargs):
|
||||
cert = lemur.common.utils.parse_certificate(kwargs['body'])
|
||||
|
||||
self.issuer = defaults.issuer(cert)
|
||||
self.cn = defaults.common_name(cert)
|
||||
self.san = defaults.san(cert)
|
||||
self.not_before = defaults.not_before(cert)
|
||||
self.not_after = defaults.not_after(cert)
|
||||
|
||||
# when destinations are appended they require a valid name.
|
||||
if kwargs.get('name'):
|
||||
self.name = get_or_increase_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'].strip()
|
||||
|
||||
if kwargs.get('private_key'):
|
||||
self.private_key = kwargs['private_key'].strip()
|
||||
|
||||
if kwargs.get('chain'):
|
||||
self.chain = kwargs['chain'].strip()
|
||||
|
||||
self.notify = kwargs.get('notify', True)
|
||||
self.destinations = kwargs.get('destinations', [])
|
||||
self.notifications = kwargs.get('notifications', [])
|
||||
self.description = kwargs.get('description')
|
||||
self.roles = list(set(kwargs.get('roles', [])))
|
||||
self.replaces = kwargs.get('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
|
||||
def is_expired(self):
|
||||
if self.not_after < datetime.datetime.now():
|
||||
def active(self):
|
||||
return self.notify
|
||||
|
||||
@hybrid_property
|
||||
def expired(self):
|
||||
if self.not_after <= datetime.datetime.now():
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_unused(self):
|
||||
if self.elb_listeners.count() == 0:
|
||||
@expired.expression
|
||||
def expired(cls):
|
||||
return case(
|
||||
[
|
||||
(cls.now_after <= datetime.datetime.now(), True)
|
||||
],
|
||||
else_=False
|
||||
)
|
||||
|
||||
@hybrid_property
|
||||
def revoked(self):
|
||||
if 'revoked' == self.status:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_revoked(self):
|
||||
# we might not yet know the condition of the cert
|
||||
if self.status:
|
||||
if 'revoked' in self.status:
|
||||
return True
|
||||
@revoked.expression
|
||||
def revoked(cls):
|
||||
return case(
|
||||
[
|
||||
(cls.status == 'revoked', True)
|
||||
],
|
||||
else_=False
|
||||
)
|
||||
|
||||
def get_arn(self, account_number):
|
||||
"""
|
||||
@ -291,6 +158,9 @@ class Certificate(db.Model):
|
||||
"""
|
||||
return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "Certificate(name={name})".format(name=self.name)
|
||||
|
||||
|
||||
@event.listens_for(Certificate.destinations, 'append')
|
||||
def update_destinations(target, value, initiator):
|
||||
@ -303,32 +173,38 @@ def update_destinations(target, value, initiator):
|
||||
:return:
|
||||
"""
|
||||
destination_plugin = plugins.get(value.plugin_name)
|
||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||
|
||||
try:
|
||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
|
||||
|
||||
@event.listens_for(Certificate.replaces, 'append')
|
||||
def update_replacement(target, value, initiator):
|
||||
"""
|
||||
When a certificate is marked as 'replaced' it is then marked as in-active
|
||||
When a certificate is marked as 'replaced' we should not notify.
|
||||
|
||||
:param target:
|
||||
:param value:
|
||||
:param initiator:
|
||||
:return:
|
||||
"""
|
||||
value.active = False
|
||||
value.notify = False
|
||||
|
||||
|
||||
@event.listens_for(Certificate, 'before_update')
|
||||
def protect_active(mapper, connection, target):
|
||||
"""
|
||||
When a certificate has a replacement do not allow it to be marked as 'active'
|
||||
|
||||
:param connection:
|
||||
:param mapper:
|
||||
:param target:
|
||||
:return:
|
||||
"""
|
||||
if target.active:
|
||||
if target.replaced:
|
||||
raise Exception("Cannot mark certificate as active, certificate has been marked as replaced.")
|
||||
# @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 not target.notify:
|
||||
# raise Exception(
|
||||
# "Cannot silence notification for a certificate Lemur has been found to be currently deployed onto endpoints"
|
||||
# )
|
||||
|
186
lemur/certificates/schemas.py
Normal file
186
lemur/certificates/schemas.py
Normal file
@ -0,0 +1,186 @@
|
||||
"""
|
||||
.. 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, pre_load
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \
|
||||
AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema
|
||||
|
||||
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, missing
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.common.fields import ArrowDateTime
|
||||
|
||||
|
||||
class CertificateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String()
|
||||
|
||||
|
||||
class CertificateCreationSchema(CertificateSchema):
|
||||
@post_load
|
||||
def default_notification(self, data):
|
||||
if not data['notifications']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
return data
|
||||
|
||||
|
||||
class CertificateInputSchema(CertificateCreationSchema):
|
||||
name = fields.String()
|
||||
common_name = fields.String(required=True, validate=validators.sensitive_domain)
|
||||
authority = fields.Nested(AssociatedAuthoritySchema, required=True)
|
||||
|
||||
validity_start = ArrowDateTime()
|
||||
validity_end = ArrowDateTime()
|
||||
validity_years = fields.Integer()
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
csr = fields.String(validate=validators.csr)
|
||||
|
||||
notify = fields.Boolean(default=True)
|
||||
|
||||
# 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)
|
||||
|
||||
@pre_load
|
||||
def ensure_dates(self, data):
|
||||
return missing.convert_validity_years(data)
|
||||
|
||||
|
||||
class CertificateEditInputSchema(CertificateSchema):
|
||||
notify = fields.Boolean()
|
||||
owner = fields.String()
|
||||
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)
|
||||
|
||||
@post_load
|
||||
def enforce_notifications(self, data):
|
||||
"""
|
||||
Ensures that when an owner changes, default notifications are added for the new owner.
|
||||
Old owner notifications are retained unless explicitly removed.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
if data['owner']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
return data
|
||||
|
||||
|
||||
class CertificateNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
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 CertificateCloneSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
description = fields.String()
|
||||
common_name = fields.String()
|
||||
|
||||
|
||||
class CertificateOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
notify = 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.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class CertificateUploadInputSchema(CertificateCreationSchema):
|
||||
name = fields.String()
|
||||
notify = fields.Boolean(missing=True)
|
||||
|
||||
private_key = fields.String(validate=validators.private_key)
|
||||
body = fields.String(required=True, validate=validators.public_certificate)
|
||||
chain = fields.String(validate=validators.public_certificate) # TODO this could be multiple certificates
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
@validates_schema
|
||||
def keys(self, data):
|
||||
if data.get('destinations'):
|
||||
if not data.get('private_key'):
|
||||
raise ValidationError('Destinations require private key.')
|
||||
|
||||
|
||||
class CertificateExportInputSchema(LemurInputSchema):
|
||||
plugin = fields.Nested(PluginInputSchema)
|
||||
|
||||
|
||||
certificate_input_schema = CertificateInputSchema()
|
||||
certificate_output_schema = CertificateOutputSchema()
|
||||
certificates_output_schema = CertificateOutputSchema(many=True)
|
||||
certificate_upload_input_schema = CertificateUploadInputSchema()
|
||||
certificate_export_input_schema = CertificateExportInputSchema()
|
||||
certificate_edit_input_schema = CertificateEditInputSchema()
|
@ -11,6 +11,7 @@ from sqlalchemy import func, or_
|
||||
from flask import g, current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.certificates.models import Certificate
|
||||
|
||||
@ -20,6 +21,7 @@ from lemur.authorities.models import Authority
|
||||
from lemur.domains.models import Domain
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@ -65,16 +67,26 @@ def get_all_certs():
|
||||
return Certificate.query.all()
|
||||
|
||||
|
||||
def find_duplicates(cert_body):
|
||||
def get_by_source(source_label):
|
||||
"""
|
||||
Retrieves all certificates from a given source.
|
||||
|
||||
:param source_label:
|
||||
:return:
|
||||
"""
|
||||
return Certificate.query.filter(Certificate.sources.any(label=source_label))
|
||||
|
||||
|
||||
def find_duplicates(cert):
|
||||
"""
|
||||
Finds certificates that already exist within Lemur. We do this by looking for
|
||||
certificate bodies that are the same. This is the most reliable way to determine
|
||||
if a certificate is already being tracked by Lemur.
|
||||
|
||||
:param cert_body:
|
||||
:param cert:
|
||||
:return:
|
||||
"""
|
||||
return Certificate.query.filter_by(body=cert_body).all()
|
||||
return Certificate.query.filter_by(body=cert['body'].strip(), chain=cert['chain'].strip()).all()
|
||||
|
||||
|
||||
def export(cert, export_plugin):
|
||||
@ -87,75 +99,64 @@ def export(cert, export_plugin):
|
||||
:return:
|
||||
"""
|
||||
plugin = plugins.get(export_plugin['slug'])
|
||||
|
||||
return plugin.export(cert.body, cert.chain, cert.private_key, export_plugin['pluginOptions'])
|
||||
|
||||
|
||||
def update(cert_id, owner, description, active, destinations, notifications, replaces):
|
||||
def update(cert_id, owner, description, notify, destinations, notifications, replaces, roles):
|
||||
"""
|
||||
Updates a certificate
|
||||
:param cert_id:
|
||||
:param owner:
|
||||
:param description:
|
||||
:param active:
|
||||
:param notify:
|
||||
:param destinations:
|
||||
:param notifications:
|
||||
:param replaces:
|
||||
:return:
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = get(cert_id)
|
||||
cert.active = active
|
||||
cert.notify = notify
|
||||
cert.description = description
|
||||
|
||||
# we might have to create new notifications if the owner changes
|
||||
new_notifications = []
|
||||
# get existing names to remove
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
for n in notifications:
|
||||
if notification_name not in n.label:
|
||||
new_notifications.append(n)
|
||||
|
||||
notification_name = "DEFAULT_{0}".format(owner.split('@')[0].upper())
|
||||
new_notifications += notification_service.create_default_expiration_notifications(notification_name, owner)
|
||||
|
||||
cert.notifications = new_notifications
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, destinations)
|
||||
database.update_list(cert, 'replaces', Certificate, replaces)
|
||||
|
||||
cert.destinations = destinations
|
||||
cert.notifications = notifications
|
||||
cert.roles = roles
|
||||
cert.replaces = replaces
|
||||
cert.owner = owner
|
||||
|
||||
return database.update(cert)
|
||||
|
||||
|
||||
def mint(issuer_options):
|
||||
def create_certificate_roles(**kwargs):
|
||||
# create an role for the owner and assign it
|
||||
owner_role = role_service.get_by_name(kwargs['owner'])
|
||||
if not owner_role:
|
||||
owner_role = role_service.create(
|
||||
kwargs['owner'],
|
||||
description="Auto generated role based on owner: {0}".format(kwargs['owner'])
|
||||
)
|
||||
|
||||
return [owner_role]
|
||||
|
||||
|
||||
def mint(**kwargs):
|
||||
"""
|
||||
Minting is slightly different for each authority.
|
||||
Support for multiple authorities is handled by individual plugins.
|
||||
|
||||
:param issuer_options:
|
||||
"""
|
||||
authority = issuer_options['authority']
|
||||
authority = kwargs['authority']
|
||||
|
||||
issuer = plugins.get(authority.plugin_name)
|
||||
|
||||
# allow the CSR to be specified by the user
|
||||
if not issuer_options.get('csr'):
|
||||
csr, private_key = create_csr(issuer_options)
|
||||
if not kwargs.get('csr'):
|
||||
csr, private_key = create_csr(**kwargs)
|
||||
else:
|
||||
csr = issuer_options.get('csr')
|
||||
csr = str(kwargs.get('csr'))
|
||||
private_key = None
|
||||
|
||||
issuer_options['creator'] = g.user.email
|
||||
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
|
||||
|
||||
cert = Certificate(cert_body, private_key, cert_chain)
|
||||
|
||||
cert.user = g.user
|
||||
cert.authority = authority
|
||||
database.update(cert)
|
||||
return cert, private_key, cert_chain,
|
||||
cert_body, cert_chain = issuer.create_certificate(csr, kwargs)
|
||||
return cert_body, private_key, cert_chain,
|
||||
|
||||
|
||||
def import_certificate(**kwargs):
|
||||
@ -171,106 +172,64 @@ 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'])
|
||||
if not kwargs.get('owner'):
|
||||
kwargs['owner'] = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0]
|
||||
|
||||
# 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'))
|
||||
|
||||
# NOTE existing certs may not follow our naming standard we will
|
||||
# overwrite the generated name with the actual cert name
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs.get('name')
|
||||
|
||||
if kwargs.get('user'):
|
||||
cert.user = kwargs.get('user')
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications = notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
|
||||
if kwargs.get('replacements'):
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
|
||||
cert.notifications = notifications
|
||||
|
||||
cert = database.create(cert)
|
||||
return cert
|
||||
return upload(**kwargs)
|
||||
|
||||
|
||||
def upload(**kwargs):
|
||||
"""
|
||||
Allows for pre-made certificates to be imported into Lemur.
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = Certificate(
|
||||
kwargs.get('public_cert'),
|
||||
kwargs.get('private_key'),
|
||||
kwargs.get('intermediate_cert'),
|
||||
)
|
||||
roles = create_certificate_roles(**kwargs)
|
||||
|
||||
# we override the generated name if one is provided
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs['name']
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert.description = kwargs.get('description')
|
||||
if kwargs.get('private_key'):
|
||||
private_key = kwargs['private_key']
|
||||
if not isinstance(private_key, bytes):
|
||||
kwargs['private_key'] = private_key.encode('utf-8')
|
||||
|
||||
cert = Certificate(**kwargs)
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
cert = database.create(cert)
|
||||
|
||||
g.user.certificates.append(cert)
|
||||
try:
|
||||
g.user.certificates.append(cert)
|
||||
except AttributeError:
|
||||
current_app.logger.debug("No user to associate uploaded certificate to.")
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
|
||||
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
|
||||
# create default notifications for this certificate if none are provided
|
||||
notifications = []
|
||||
if not kwargs.get('notifications'):
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
cert.notifications = notifications
|
||||
|
||||
database.update(cert)
|
||||
return cert
|
||||
return database.update(cert)
|
||||
|
||||
|
||||
def create(**kwargs):
|
||||
"""
|
||||
Creates a new certificate.
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert, private_key, cert_chain = mint(kwargs)
|
||||
kwargs['creator'] = g.user.email
|
||||
cert_body, private_key, cert_chain = mint(**kwargs)
|
||||
kwargs['body'] = cert_body
|
||||
kwargs['private_key'] = private_key
|
||||
kwargs['chain'] = cert_chain
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
roles = create_certificate_roles(**kwargs)
|
||||
|
||||
if kwargs.get('roles'):
|
||||
kwargs['roles'] += roles
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert = Certificate(**kwargs)
|
||||
|
||||
database.create(cert)
|
||||
cert.description = kwargs['description']
|
||||
g.user.certificates.append(cert)
|
||||
database.update(g.user)
|
||||
cert.authority = kwargs['authority']
|
||||
database.commit()
|
||||
|
||||
# do this after the certificate has already been created because if it fails to upload to the third party
|
||||
# we do not want to lose the certificate information.
|
||||
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
|
||||
database.update_list(cert, 'replaces', Certificate, kwargs['replacements'])
|
||||
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
|
||||
|
||||
# create default notifications for this certificate if none are provided
|
||||
notifications = cert.notifications
|
||||
if not kwargs.get('notifications'):
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name,
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
cert.notifications = notifications
|
||||
|
||||
database.update(cert)
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
return cert
|
||||
|
||||
|
||||
@ -311,7 +270,7 @@ def render(args):
|
||||
|
||||
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??
|
||||
elif 'active' in filt:
|
||||
query = query.filter(Certificate.active == terms[1])
|
||||
elif 'cn' in terms:
|
||||
query = query.filter(
|
||||
@ -346,7 +305,7 @@ def render(args):
|
||||
return database.sort_and_page(query, Certificate, args)
|
||||
|
||||
|
||||
def create_csr(csr_config):
|
||||
def create_csr(**csr_config):
|
||||
"""
|
||||
Given a list of domains create the appropriate csr
|
||||
for those domains
|
||||
@ -362,12 +321,13 @@ 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']),
|
||||
x509.NameAttribute(x509.OID_EMAIL_ADDRESS, csr_config['owner'])
|
||||
]))
|
||||
|
||||
builder = builder.add_extension(
|
||||
@ -376,11 +336,11 @@ def create_csr(csr_config):
|
||||
|
||||
if csr_config.get('extensions'):
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
if k == 'sub_alt_names':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
if name['name_type'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
|
||||
builder = builder.add_extension(
|
||||
@ -435,17 +395,20 @@ 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()
|
||||
)
|
||||
|
||||
if isinstance(private_key, bytes):
|
||||
private_key = private_key.decode('utf-8')
|
||||
|
||||
csr = request.public_bytes(
|
||||
encoding=serialization.Encoding.PEM
|
||||
)
|
||||
|
||||
return csr, pem
|
||||
return csr, private_key
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
@ -476,3 +439,69 @@ 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]
|
||||
|
||||
|
||||
def calculate_reissue_range(start, end):
|
||||
"""
|
||||
Determine what the new validity_start and validity_end dates should be.
|
||||
:param start:
|
||||
:param end:
|
||||
:return:
|
||||
"""
|
||||
span = end - start
|
||||
|
||||
new_start = arrow.utcnow().date()
|
||||
new_end = new_start + span
|
||||
|
||||
return new_start, new_end
|
||||
|
||||
|
||||
# TODO pull the OU, O, CN, etc + other extensions.
|
||||
def get_certificate_primitives(certificate):
|
||||
"""
|
||||
Retrieve key primitive from a certificate such that the certificate
|
||||
could be recreated with new expiration or be used to build upon.
|
||||
:param certificate:
|
||||
:return: dict of certificate primitives, should be enough to effectively re-issue
|
||||
certificate via `create`.
|
||||
"""
|
||||
start, end = calculate_reissue_range(certificate.not_before, certificate.not_after)
|
||||
names = [{'name_type': 'DNSName', 'value': x.name} for x in certificate.domains]
|
||||
|
||||
extensions = {
|
||||
'sub_alt_names': {
|
||||
'names': names
|
||||
}
|
||||
}
|
||||
|
||||
return dict(
|
||||
authority=certificate.authority,
|
||||
common_name=certificate.cn,
|
||||
description=certificate.description,
|
||||
validity_start=start,
|
||||
validity_end=end,
|
||||
destinations=certificate.destinations,
|
||||
roles=certificate.roles,
|
||||
extensions=extensions,
|
||||
owner=certificate.owner
|
||||
)
|
||||
|
@ -9,129 +9,24 @@ import base64
|
||||
from builtins import str
|
||||
|
||||
from flask import Blueprint, make_response, jsonify
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from lemur.plugins import plugins
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import ViewKeyPermission
|
||||
from lemur.auth.permissions import AuthorityPermission
|
||||
from lemur.auth.permissions import UpdateCertificatePermission
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \
|
||||
certificate_upload_input_schema, certificates_output_schema, certificate_export_input_schema, certificate_edit_input_schema
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.domains import service as domain_service
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
from lemur.notifications.views import notification_list
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'id': fields.Integer,
|
||||
'bits': fields.Integer,
|
||||
'deleted': fields.String,
|
||||
'issuer': fields.String,
|
||||
'serial': fields.String,
|
||||
'owner': fields.String,
|
||||
'chain': fields.String,
|
||||
'san': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'description': fields.String,
|
||||
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
|
||||
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
|
||||
'cn': fields.String,
|
||||
'signingAlgorithm': fields.String(attribute='signing_algorithm'),
|
||||
'status': fields.String,
|
||||
'body': fields.String
|
||||
}
|
||||
|
||||
|
||||
def valid_authority(authority_options):
|
||||
"""
|
||||
Defends against invalid authorities
|
||||
|
||||
:param authority_options:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
name = authority_options['name']
|
||||
authority = Authority.query.filter(Authority.name == name).one()
|
||||
|
||||
if not authority:
|
||||
raise ValueError("Unable to find authority specified")
|
||||
|
||||
if not authority.active:
|
||||
raise ValueError("Selected authority [{0}] is not currently active".format(name))
|
||||
|
||||
return authority
|
||||
|
||||
|
||||
def get_domains_from_options(options):
|
||||
"""
|
||||
Retrive all domains from certificate options
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
domains = [options['commonName']]
|
||||
if options.get('extensions'):
|
||||
if options['extensions'].get('subAltNames'):
|
||||
for k, v in options['extensions']['subAltNames']['names']:
|
||||
if k == 'DNSName':
|
||||
domains.append(v)
|
||||
return domains
|
||||
|
||||
|
||||
def check_sensitive_domains(domains):
|
||||
"""
|
||||
Determines if any certificates in the given certificate
|
||||
are marked as sensitive
|
||||
:param domains:
|
||||
:return:
|
||||
"""
|
||||
for domain in domains:
|
||||
domain_objs = domain_service.get_by_name(domain)
|
||||
for d in domain_objs:
|
||||
if d.sensitive:
|
||||
raise ValueError("The domain {0} has been marked as sensitive. Contact an administrator to "
|
||||
"issue this certificate".format(d.name))
|
||||
|
||||
|
||||
def pem_str(value, name):
|
||||
"""
|
||||
Used to validate that the given string is a PEM formatted string
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
x509.load_pem_x509_certificate(bytes(value), default_backend())
|
||||
except Exception:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid PEM string".format(name))
|
||||
return value
|
||||
|
||||
|
||||
def private_key_str(value, name):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param value:
|
||||
:param name:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
serialization.load_pem_private_key(bytes(value), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValueError("The parameter '{0}' needs to be a valid RSA private key".format(name))
|
||||
return value
|
||||
|
||||
|
||||
class CertificatesList(AuthenticatedResource):
|
||||
""" Defines the 'certificates' endpoint """
|
||||
@ -140,7 +35,7 @@ class CertificatesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /certificates
|
||||
@ -164,26 +59,53 @@ class CertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
@ -191,10 +113,11 @@ class CertificatesList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number. default is 10
|
||||
:query count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
@ -208,8 +131,8 @@ class CertificatesList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(certificate_input_schema, certificate_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates
|
||||
|
||||
@ -224,90 +147,38 @@ class CertificatesList(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "A Place",
|
||||
"organization": "ExampleInc.",
|
||||
"organizationalUnit": "Operations",
|
||||
"owner": "bob@example.com",
|
||||
"description": "test",
|
||||
"selectedAuthority": "timetest2",
|
||||
"csr",
|
||||
"authority": {
|
||||
"body": "-----BEGIN...",
|
||||
"name": "timetest2",
|
||||
"chain": "",
|
||||
"notBefore": "2015-06-05T15:20:59",
|
||||
"active": true,
|
||||
"id": 50,
|
||||
"notAfter": "2015-06-17T15:21:08",
|
||||
"description": "dsfdsf"
|
||||
},
|
||||
"notifications": [
|
||||
{
|
||||
"description": "Default 30 day expiration notification",
|
||||
"notificationOptions": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
"value": 30,
|
||||
"helpMessage": "Number of days to be alert before expiration.",
|
||||
"validation": "^\\d+$",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"available": [
|
||||
"days",
|
||||
"weeks",
|
||||
"months"
|
||||
],
|
||||
"name": "unit",
|
||||
"required": true,
|
||||
"value": "days",
|
||||
"helpMessage": "Interval unit",
|
||||
"validation": "",
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"name": "recipients",
|
||||
"required": true,
|
||||
"value": "bob@example.com",
|
||||
"helpMessage": "Comma delimited list of email addresses",
|
||||
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
|
||||
"type": "str"
|
||||
}
|
||||
],
|
||||
"label": "DEFAULT_KGLISSON_30_DAY",
|
||||
"pluginName": "email-notification",
|
||||
"active": true,
|
||||
"id": 7
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"basicConstraints": {},
|
||||
"keyUsage": {
|
||||
"isCritical": true,
|
||||
"useKeyEncipherment": true,
|
||||
"useDigitalSignature": true
|
||||
},
|
||||
"extendedKeyUsage": {
|
||||
"isCritical": true,
|
||||
"useServerAuthentication": true
|
||||
},
|
||||
"subjectKeyIdentifier": {
|
||||
"includeSKI": true
|
||||
},
|
||||
"owner": "secure@example.net",
|
||||
"commonName": "test.example.net",
|
||||
"country": "US",
|
||||
"extensions": {
|
||||
"subAltNames": {
|
||||
"names": []
|
||||
"names": [
|
||||
{
|
||||
"nameType": "DNSName",
|
||||
"value": "*.test.example.net"
|
||||
},
|
||||
{
|
||||
"nameType": "DNSName",
|
||||
"value": "www.test.example.net"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"commonName": "test",
|
||||
"validityStart": "2015-06-05T07:00:00.000Z",
|
||||
"validityEnd": "2015-06-16T07:00:00.000Z",
|
||||
"replacements": [
|
||||
{'id': 123}
|
||||
]
|
||||
}
|
||||
},
|
||||
"replacements": [{
|
||||
"id": 1
|
||||
},
|
||||
"notify": true,
|
||||
"validityEnd": "2026-01-01T08:00:00.000Z",
|
||||
"authority": {
|
||||
"name": "verisign"
|
||||
},
|
||||
"organization": "Netflix, Inc.",
|
||||
"location": "Los Gatos",
|
||||
"state": "California",
|
||||
"validityStart": "2016-11-11T04:19:48.000Z",
|
||||
"organizationalUnit": "Operations"
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
@ -318,24 +189,56 @@ 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": [{
|
||||
"id": 1
|
||||
}],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
|
||||
:arg extensions: extensions to be used in the certificate
|
||||
:arg description: description for new certificate
|
||||
:arg owner: owner email
|
||||
@ -346,47 +249,25 @@ class CertificatesList(AuthenticatedResource):
|
||||
:arg state: state for the CSR
|
||||
:arg location: location for the CSR
|
||||
:arg organization: organization for CSR
|
||||
:arg commonName: certiifcate common name
|
||||
:arg commonName: certificate common name
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
|
||||
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
|
||||
self.reqparse.add_argument('authority', type=valid_authority, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('country', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('state', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('location', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('organization', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('organizationalUnit', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('owner', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('commonName', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('csr', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = args['authority']
|
||||
role = role_service.get_by_name(authority.owner)
|
||||
role = role_service.get_by_name(data['authority'].owner)
|
||||
|
||||
# all the authority role members should be allowed
|
||||
roles = [x.name for x in authority.roles]
|
||||
roles = [x.name for x in data['authority'].roles]
|
||||
|
||||
# allow "owner" roles by team DL
|
||||
roles.append(role)
|
||||
authority_permission = AuthorityPermission(authority.id, roles)
|
||||
authority_permission = AuthorityPermission(data['authority'].id, roles)
|
||||
|
||||
if authority_permission.can():
|
||||
# if we are not admins lets make sure we aren't issuing anything sensitive
|
||||
if not SensitiveDomainPermission().can():
|
||||
check_sensitive_domains(get_domains_from_options(args))
|
||||
return service.create(**args)
|
||||
return service.create(**data)
|
||||
|
||||
return dict(message="You are not authorized to use {0}".format(args['authority'].name)), 403
|
||||
return dict(message="You are not authorized to use {0}".format(data['authority'].name)), 403
|
||||
|
||||
|
||||
class CertificatesUpload(AuthenticatedResource):
|
||||
@ -396,8 +277,8 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesUpload, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(certificate_upload_input_schema, certificate_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates/upload
|
||||
|
||||
@ -431,23 +312,51 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "joe@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2"
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:arg owner: owner email for certificate
|
||||
@ -458,24 +367,14 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 403: unauthenticated
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
|
||||
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
if args.get('destinations'):
|
||||
if args.get('private_key'):
|
||||
return service.upload(**args)
|
||||
"""
|
||||
if data.get('destinations'):
|
||||
if data.get('private_key'):
|
||||
return service.upload(**data)
|
||||
else:
|
||||
raise Exception("Private key must be provided in order to upload certificate to AWS")
|
||||
return service.upload(**args)
|
||||
return service.upload(**data)
|
||||
|
||||
|
||||
class CertificatesStats(AuthenticatedResource):
|
||||
@ -535,9 +434,8 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||
if not cert:
|
||||
return dict(message="Cannot find specified certificate"), 404
|
||||
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = ViewKeyPermission(certificate_id, getattr(role, 'name', None))
|
||||
owner_role = role_service.get_by_name(cert.owner)
|
||||
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
||||
|
||||
if permission.can():
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
@ -553,7 +451,7 @@ class Certificates(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Certificates, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificate_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1
|
||||
@ -577,33 +475,62 @@ class Certificates(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "bob@example.com",
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
return service.get(certificate_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, certificate_id):
|
||||
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
|
||||
def put(self, certificate_id, data=None):
|
||||
"""
|
||||
.. http:put:: /certificates/1
|
||||
|
||||
@ -634,50 +561,78 @@ class Certificates(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"cn": "example.com",
|
||||
"status": "unknown",
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
self.reqparse.add_argument('active', type=bool, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=notification_list, default=[], location='json')
|
||||
self.reqparse.add_argument('replacements', type=list, default=[], location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||
owner_role = role_service.get_by_name(cert.owner)
|
||||
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
||||
|
||||
if permission.can():
|
||||
for destination in data['destinations']:
|
||||
if destination.plugin.requires_key:
|
||||
if not cert.private_key:
|
||||
return dict('Unable to add destination: {0}. Certificate does not have required private key.'.format(destination.label))
|
||||
|
||||
return service.update(
|
||||
certificate_id,
|
||||
args['owner'],
|
||||
args['description'],
|
||||
args['active'],
|
||||
args['destinations'],
|
||||
args['notifications'],
|
||||
args['replacements']
|
||||
data['owner'],
|
||||
data['description'],
|
||||
data['notify'],
|
||||
data['destinations'],
|
||||
data['notifications'],
|
||||
data['replacements'],
|
||||
data['roles']
|
||||
)
|
||||
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
@ -690,7 +645,7 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(NotificationCertificatesList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self, notification_id):
|
||||
"""
|
||||
.. http:get:: /notifications/1/certificates
|
||||
@ -714,27 +669,53 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": 'bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}
|
||||
]
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
@ -742,10 +723,11 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
@ -766,7 +748,7 @@ class CertificatesReplacementsList(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificatesReplacementsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, certificates_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/replacements
|
||||
@ -789,29 +771,61 @@ class CertificatesReplacementsList(AuthenticatedResource):
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
[{
|
||||
"id": 1,
|
||||
"name": "cert1",
|
||||
"description": "this is cert1",
|
||||
"bits": 2048,
|
||||
"deleted": false,
|
||||
"issuer": "ExampeInc.",
|
||||
"serial": "123450",
|
||||
"chain": "-----Begin ...",
|
||||
"body": "-----Begin ...",
|
||||
"san": true,
|
||||
"owner": "bob@example.com",
|
||||
"active": true,
|
||||
"notBefore": "2015-06-05T17:09:39",
|
||||
"notAfter": "2015-06-10T17:09:39",
|
||||
"signingAlgorithm": "sha2",
|
||||
"cn": "example.com",
|
||||
"status": "unknown"
|
||||
}]
|
||||
{
|
||||
"items": [{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"bits": 2048,
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
return service.get(certificate_id).replaces
|
||||
|
||||
@ -821,7 +835,8 @@ class CertificateExport(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateExport, self).__init__()
|
||||
|
||||
def post(self, certificate_id):
|
||||
@validate_schema(certificate_export_input_schema, None)
|
||||
def post(self, certificate_id, data=None):
|
||||
"""
|
||||
.. http:post:: /certificates/1/export
|
||||
|
||||
@ -885,26 +900,40 @@ class CertificateExport(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
self.reqparse.add_argument('export', type=dict, required=True, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||
owner_role = role_service.get_by_name(cert.owner)
|
||||
permission = CertificatePermission(cert.id, owner_role, [x.name for x in cert.roles])
|
||||
|
||||
options = data['plugin']['plugin_options']
|
||||
plugin = data['plugin']['plugin_object']
|
||||
|
||||
plugin = plugins.get(args['export']['plugin']['slug'])
|
||||
if plugin.requires_key:
|
||||
if permission.can():
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, args['export']['plugin']['pluginOptions'])
|
||||
if cert.private_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:
|
||||
return dict(message='You are not authorized to export this certificate'), 403
|
||||
return dict(message='Unable to export certificate, plugin: {0} requires a private key but no key was found.'.format(plugin.slug))
|
||||
else:
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, args['export']['plugin']['pluginOptions'])
|
||||
extension, passphrase, data = plugin.export(cert.body, cert.chain, cert.private_key, options)
|
||||
|
||||
# we take a hit in message size when b64 encoding
|
||||
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data))
|
||||
return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data).decode('utf-8'))
|
||||
|
||||
|
||||
class CertificateClone(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(CertificateExport, self).__init__()
|
||||
|
||||
@validate_schema(None, certificate_output_schema)
|
||||
def get(self, certificate_id):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||
@ -913,6 +942,7 @@ api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificate
|
||||
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
||||
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
||||
api.add_resource(CertificateExport, '/certificates/<int:certificate_id>/export', endpoint='exportCertificate')
|
||||
api.add_resource(CertificateClone, '/certificates/<int:certificate_id>/clone', endpoint='cloneCertificate')
|
||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates',
|
||||
endpoint='notificationCertificates')
|
||||
api.add_resource(CertificatesReplacementsList, '/certificates/<int:certificate_id>/replacements',
|
||||
|
169
lemur/common/defaults.py
Normal file
169
lemur/common/defaults.py
Normal file
@ -0,0 +1,169 @@
|
||||
from cryptography import x509
|
||||
from flask import current_app
|
||||
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
try:
|
||||
return cert.public_key().key_size
|
||||
except AttributeError:
|
||||
current_app.logger.debug('Unable to get bitstrength.')
|
||||
|
||||
|
||||
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
|
93
lemur/common/fields.py
Normal file
93
lemur/common/fields.py
Normal file
@ -0,0 +1,93 @@
|
||||
import arrow
|
||||
import warnings
|
||||
from datetime import datetime as dt
|
||||
from marshmallow.fields import Field
|
||||
from marshmallow import utils
|
||||
|
||||
|
||||
class ArrowDateTime(Field):
|
||||
"""A formatted datetime string in UTC.
|
||||
|
||||
Example: ``'2014-12-22T03:12:58.019077+00:00'``
|
||||
|
||||
Timezone-naive `datetime` objects are converted to
|
||||
UTC (+00:00) by :meth:`Schema.dump <marshmallow.Schema.dump>`.
|
||||
:meth:`Schema.load <marshmallow.Schema.load>` returns `datetime`
|
||||
objects that are timezone-aware.
|
||||
|
||||
:param str format: Either ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601),
|
||||
or a date format string. If `None`, defaults to "iso".
|
||||
:param kwargs: The same keyword arguments that :class:`Field` receives.
|
||||
|
||||
"""
|
||||
|
||||
DATEFORMAT_SERIALIZATION_FUNCS = {
|
||||
'iso': utils.isoformat,
|
||||
'iso8601': utils.isoformat,
|
||||
'rfc': utils.rfcformat,
|
||||
'rfc822': utils.rfcformat,
|
||||
}
|
||||
|
||||
DATEFORMAT_DESERIALIZATION_FUNCS = {
|
||||
'iso': utils.from_iso,
|
||||
'iso8601': utils.from_iso,
|
||||
'rfc': utils.from_rfc,
|
||||
'rfc822': utils.from_rfc,
|
||||
}
|
||||
|
||||
DEFAULT_FORMAT = 'iso'
|
||||
|
||||
localtime = False
|
||||
default_error_messages = {
|
||||
'invalid': 'Not a valid datetime.',
|
||||
'format': '"{input}" cannot be formatted as a datetime.',
|
||||
}
|
||||
|
||||
def __init__(self, format=None, **kwargs):
|
||||
super(ArrowDateTime, self).__init__(**kwargs)
|
||||
# Allow this to be None. It may be set later in the ``_serialize``
|
||||
# or ``_desrialize`` methods This allows a Schema to dynamically set the
|
||||
# dateformat, e.g. from a Meta option
|
||||
self.dateformat = format
|
||||
|
||||
def _add_to_schema(self, field_name, schema):
|
||||
super(ArrowDateTime, self)._add_to_schema(field_name, schema)
|
||||
self.dateformat = self.dateformat or schema.opts.dateformat
|
||||
|
||||
def _serialize(self, value, attr, obj):
|
||||
if value is None:
|
||||
return None
|
||||
self.dateformat = self.dateformat or self.DEFAULT_FORMAT
|
||||
format_func = self.DATEFORMAT_SERIALIZATION_FUNCS.get(self.dateformat, None)
|
||||
if format_func:
|
||||
try:
|
||||
return format_func(value, localtime=self.localtime)
|
||||
except (AttributeError, ValueError) as err:
|
||||
self.fail('format', input=value)
|
||||
else:
|
||||
return value.strftime(self.dateformat)
|
||||
|
||||
def _deserialize(self, value, attr, data):
|
||||
if not value: # Falsy values, e.g. '', None, [] are not valid
|
||||
raise self.fail('invalid')
|
||||
self.dateformat = self.dateformat or self.DEFAULT_FORMAT
|
||||
func = self.DATEFORMAT_DESERIALIZATION_FUNCS.get(self.dateformat)
|
||||
if func:
|
||||
try:
|
||||
return arrow.get(func(value))
|
||||
except (TypeError, AttributeError, ValueError):
|
||||
raise self.fail('invalid')
|
||||
elif self.dateformat:
|
||||
try:
|
||||
return dt.datetime.strptime(value, self.dateformat)
|
||||
except (TypeError, AttributeError, ValueError):
|
||||
raise self.fail('invalid')
|
||||
elif utils.dateutil_available:
|
||||
try:
|
||||
return arrow.get(utils.from_datestring(value))
|
||||
except TypeError:
|
||||
raise self.fail('invalid')
|
||||
else:
|
||||
warnings.warn('It is recommended that you install python-dateutil '
|
||||
'for improved datetime deserialization.')
|
||||
raise self.fail('invalid')
|
@ -58,8 +58,8 @@ class InstanceManager(object):
|
||||
results.append(cls())
|
||||
else:
|
||||
results.append(cls)
|
||||
except Exception:
|
||||
current_app.logger.exception('Unable to import %s', cls_path)
|
||||
except Exception as e:
|
||||
current_app.logger.exception('Unable to import %s. Reason: %s', cls_path, e)
|
||||
continue
|
||||
self.cache = results
|
||||
|
||||
|
24
lemur/common/missing.py
Normal file
24
lemur/common/missing.py
Normal file
@ -0,0 +1,24 @@
|
||||
import arrow
|
||||
from flask import current_app
|
||||
|
||||
from lemur.common.utils import is_weekend
|
||||
|
||||
|
||||
def convert_validity_years(data):
|
||||
"""
|
||||
Convert validity years to validity_start and validity_end
|
||||
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
if data.get('validity_years'):
|
||||
now = arrow.utcnow()
|
||||
data['validity_start'] = now.isoformat()
|
||||
|
||||
end = now.replace(years=+int(data['validity_years']))
|
||||
if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True):
|
||||
if is_weekend(end):
|
||||
end = end.replace(days=-2)
|
||||
|
||||
data['validity_end'] = end.isoformat()
|
||||
return data
|
153
lemur/common/schema.py
Normal file
153
lemur/common/schema.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""
|
||||
.. module: lemur.common.schema
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import request, current_app
|
||||
|
||||
from sqlalchemy.orm.collections import InstrumentedList
|
||||
|
||||
from inflection import camelize, underscore
|
||||
from marshmallow import Schema, post_dump, pre_load, pre_dump
|
||||
|
||||
|
||||
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=str(e)), 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
|
@ -6,15 +6,23 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import six
|
||||
import sys
|
||||
import string
|
||||
import random
|
||||
from functools import wraps
|
||||
|
||||
from flask import current_app
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from flask.ext.restful import marshal
|
||||
from flask.ext.restful.reqparse import RequestParser
|
||||
from flask.ext.sqlalchemy import Pagination
|
||||
|
||||
paginated_parser = RequestParser()
|
||||
|
||||
paginated_parser.add_argument('count', type=int, default=10, location='args')
|
||||
paginated_parser.add_argument('page', type=int, default=1, location='args')
|
||||
paginated_parser.add_argument('sortDir', type=str, dest='sort_dir', location='args')
|
||||
paginated_parser.add_argument('sortBy', type=str, dest='sort_by', location='args')
|
||||
paginated_parser.add_argument('filter', type=str, location='args')
|
||||
|
||||
|
||||
def get_psuedo_random_string():
|
||||
@ -28,51 +36,22 @@ def get_psuedo_random_string():
|
||||
return challenge
|
||||
|
||||
|
||||
class marshal_items(object):
|
||||
def __init__(self, fields, envelope=None):
|
||||
self.fields = fields
|
||||
self.envelop = envelope
|
||||
def parse_certificate(body):
|
||||
if sys.version_info[0] <= 2:
|
||||
return x509.load_pem_x509_certificate(bytes(body), default_backend())
|
||||
|
||||
def __call__(self, f):
|
||||
def _filter_items(items):
|
||||
filtered_items = []
|
||||
for item in items:
|
||||
filtered_items.append(marshal(item, self.fields))
|
||||
return filtered_items
|
||||
if isinstance(body, six.string_types):
|
||||
body = body.encode('utf-8')
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
resp = f(*args, **kwargs)
|
||||
|
||||
# this is a bit weird way to handle non standard error codes returned from the marshaled function
|
||||
if isinstance(resp, tuple):
|
||||
return resp[0], resp[1]
|
||||
|
||||
if isinstance(resp, Pagination):
|
||||
return {'items': _filter_items(resp.items), 'total': resp.total}
|
||||
|
||||
if isinstance(resp, list):
|
||||
return {'items': _filter_items(resp), 'total': len(resp)}
|
||||
|
||||
return marshal(resp, self.fields)
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
# this is a little weird hack to respect flask restful parsing errors on marshaled functions
|
||||
if hasattr(e, 'code'):
|
||||
if hasattr(e, 'data'):
|
||||
return {'message': e.data['message']}, 400
|
||||
else:
|
||||
return {'message': {'exception': 'unknown'}}, 400
|
||||
else:
|
||||
return {'message': {'exception': str(e)}}, 400
|
||||
return wrapper
|
||||
return x509.load_pem_x509_certificate(body, default_backend())
|
||||
|
||||
|
||||
paginated_parser = RequestParser()
|
||||
def is_weekend(date):
|
||||
"""
|
||||
Determines if a given date is on a weekend.
|
||||
|
||||
paginated_parser.add_argument('count', type=int, default=10, location='args')
|
||||
paginated_parser.add_argument('page', type=int, default=1, location='args')
|
||||
paginated_parser.add_argument('sortDir', type=str, dest='sort_dir', location='args')
|
||||
paginated_parser.add_argument('sortBy', type=str, dest='sort_by', location='args')
|
||||
paginated_parser.add_argument('filter', type=str, location='args')
|
||||
:param date:
|
||||
:return:
|
||||
"""
|
||||
if date.weekday() > 5:
|
||||
return True
|
||||
|
118
lemur/common/validators.py
Normal file
118
lemur/common/validators.py
Normal file
@ -0,0 +1,118 @@
|
||||
import re
|
||||
|
||||
from flask import current_app
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
from lemur.common.utils import parse_certificate, is_weekend
|
||||
from lemur.domains import service as domain_service
|
||||
|
||||
|
||||
def public_certificate(body):
|
||||
"""
|
||||
Determines if specified string is valid public certificate.
|
||||
|
||||
:param body:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
parse_certificate(body)
|
||||
except Exception:
|
||||
raise ValidationError('Public certificate presented is not valid.')
|
||||
|
||||
|
||||
def private_key(key):
|
||||
"""
|
||||
User to validate that a given string is a RSA private key
|
||||
|
||||
:param key:
|
||||
:return: :raise ValueError:
|
||||
"""
|
||||
try:
|
||||
if isinstance(key, bytes):
|
||||
serialization.load_pem_private_key(key, None, backend=default_backend())
|
||||
else:
|
||||
serialization.load_pem_private_key(key.encode('utf-8'), None, backend=default_backend())
|
||||
except Exception:
|
||||
raise ValidationError('Private key presented is not valid.')
|
||||
|
||||
|
||||
def sensitive_domain(domain):
|
||||
"""
|
||||
Determines if domain has been marked as sensitive.
|
||||
:param domain:
|
||||
:return:
|
||||
"""
|
||||
restricted_domains = current_app.config.get('LEMUR_RESTRICTED_DOMAINS', [])
|
||||
if restricted_domains:
|
||||
domains = domain_service.get_by_name(domain)
|
||||
for domain in domains:
|
||||
# we only care about non-admins
|
||||
if not SensitiveDomainPermission().can():
|
||||
if domain.sensitive or any([re.match(pattern, domain.name) for pattern in restricted_domains]):
|
||||
raise ValidationError(
|
||||
'Domain {0} has been marked as sensitive, contact and administrator \
|
||||
to issue the certificate.'.format(domain))
|
||||
|
||||
|
||||
def encoding(oid_encoding):
|
||||
"""
|
||||
Determines if the specified oid type is valid.
|
||||
:param oid_encoding:
|
||||
:return:
|
||||
"""
|
||||
valid_types = ['b64asn1', 'string', 'ia5string']
|
||||
if oid_encoding.lower() not in [o_type.lower() for o_type in valid_types]:
|
||||
raise ValidationError('Invalid Oid Encoding: {0} choose from {1}'.format(oid_encoding, ",".join(valid_types)))
|
||||
|
||||
|
||||
def sub_alt_type(alt_type):
|
||||
"""
|
||||
Determines if the specified subject alternate type is valid.
|
||||
:param alt_type:
|
||||
:return:
|
||||
"""
|
||||
valid_types = ['DNSName', 'IPAddress', 'uniFormResourceIdentifier', 'directoryName', 'rfc822Name', 'registrationID',
|
||||
'otherName', 'x400Address', 'EDIPartyName']
|
||||
if alt_type.lower() not in [a_type.lower() for a_type in valid_types]:
|
||||
raise ValidationError('Invalid SubAltName Type: {0} choose from {1}'.format(type, ",".join(valid_types)))
|
||||
|
||||
|
||||
def csr(data):
|
||||
"""
|
||||
Determines if the CSR is valid.
|
||||
: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_start') and data.get('validity_end'):
|
||||
if not current_app.config.get('LEMUR_ALLOW_WEEKEND_EXPIRATION', True):
|
||||
if is_weekend(data.get('validity_end')):
|
||||
raise ValidationError('Validity end must not land on a weekend.')
|
||||
|
||||
if not data['validity_start'] < data['validity_end']:
|
||||
raise ValidationError('Validity start must be before validity end.')
|
||||
|
||||
if data.get('authority'):
|
||||
if data.get('validity_start').date() < data['authority'].authority_certificate.not_before.date():
|
||||
raise ValidationError('Validity start must not be before {0}'.format(data['authority'].authority_certificate.not_before))
|
||||
|
||||
if data.get('validity_end').date() > data['authority'].authority_certificate.not_after.date():
|
||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
||||
|
||||
return data
|
@ -12,8 +12,6 @@
|
||||
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
|
||||
@ -125,10 +123,7 @@ def get(model, value, field="id"):
|
||||
:return:
|
||||
"""
|
||||
query = session_query(model)
|
||||
try:
|
||||
return query.filter(getattr(model, field) == value).one()
|
||||
except NoResultFound as e:
|
||||
return
|
||||
return query.filter(getattr(model, field) == value).scalar()
|
||||
|
||||
|
||||
def get_all(model, value, field="id"):
|
||||
@ -239,9 +234,6 @@ def update_list(model, model_attr, item_model, items):
|
||||
"""
|
||||
ids = []
|
||||
|
||||
for i in items:
|
||||
ids.append(i['id'])
|
||||
|
||||
for i in getattr(model, model_attr):
|
||||
if i.id not in ids:
|
||||
getattr(model, model_attr).remove(i)
|
||||
@ -287,4 +279,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)
|
||||
|
@ -3,8 +3,6 @@
|
||||
import os
|
||||
_basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
ADMINS = frozenset([''])
|
||||
|
||||
THREADS_PER_PAGE = 8
|
||||
|
||||
# General
|
||||
|
@ -57,7 +57,8 @@ class LemurDefaults(AuthenticatedResource):
|
||||
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')
|
||||
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
|
||||
issuerPlugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN')
|
||||
)
|
||||
|
||||
api.add_resource(LemurDefaults, '/defaults', endpoint='default')
|
||||
|
@ -5,7 +5,6 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import copy
|
||||
from sqlalchemy import Column, Integer, String, Text
|
||||
from sqlalchemy_utils import JSONType
|
||||
from lemur.database import db
|
||||
@ -23,7 +22,7 @@ class Destination(db.Model):
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
p = plugins.get(self.plugin_name)
|
||||
c = copy.deepcopy(p)
|
||||
c.options = self.options
|
||||
return c
|
||||
return plugins.get(self.plugin_name)
|
||||
|
||||
def __repr__(self):
|
||||
return "Destination(label={label})".format(label=self.label)
|
||||
|
42
lemur/destinations/schemas.py
Normal file
42
lemur/destinations/schemas.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
.. module: lemur.destinations.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
||||
from marshmallow import fields, post_dump
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema
|
||||
|
||||
|
||||
class DestinationInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String(required=True)
|
||||
description = fields.String(required=True)
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginInputSchema, required=True)
|
||||
|
||||
|
||||
class DestinationOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
options = fields.List(fields.Dict())
|
||||
|
||||
@post_dump
|
||||
def fill_object(self, data):
|
||||
data['plugin']['pluginOptions'] = data['options']
|
||||
return data
|
||||
|
||||
|
||||
class DestinationNestedOutputSchema(DestinationOutputSchema):
|
||||
__envelope__ = False
|
||||
|
||||
|
||||
destination_input_schema = DestinationInputSchema()
|
||||
destinations_output_schema = DestinationOutputSchema(many=True)
|
||||
destination_output_schema = DestinationOutputSchema()
|
@ -86,10 +86,6 @@ def get_all():
|
||||
|
||||
|
||||
def render(args):
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -103,12 +99,7 @@ def render(args):
|
||||
terms = filt.split(';')
|
||||
query = database.filter(query, Destination, terms)
|
||||
|
||||
query = database.find_all(query, Destination, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Destination, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Destination, args)
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
|
@ -7,34 +7,28 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.destinations import service
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.destinations.schemas import destinations_output_schema, destination_input_schema, destination_output_schema
|
||||
|
||||
|
||||
mod = Blueprint('destinations', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'description': fields.String,
|
||||
'destinationOptions': fields.Raw(attribute='options'),
|
||||
'pluginName': fields.String(attribute='plugin_name'),
|
||||
'label': fields.String,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
class DestinationsList(AuthenticatedResource):
|
||||
""" Defines the 'destinations' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(DestinationsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destinations_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /destinations
|
||||
@ -58,24 +52,32 @@ class DestinationsList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
}
|
||||
],
|
||||
"items": [{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
@ -83,7 +85,7 @@ class DestinationsList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -92,8 +94,8 @@ class DestinationsList(AuthenticatedResource):
|
||||
return service.render(args)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(destination_input_schema, destination_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /destinations
|
||||
|
||||
@ -108,20 +110,30 @@ class DestinationsList(AuthenticatedResource):
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -133,20 +145,30 @@ class DestinationsList(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:arg label: human readable account label
|
||||
@ -154,12 +176,7 @@ class DestinationsList(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.create(data['label'], data['plugin']['slug'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
|
||||
class Destinations(AuthenticatedResource):
|
||||
@ -167,7 +184,7 @@ class Destinations(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Destinations, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destination_output_schema)
|
||||
def get(self, destination_id):
|
||||
"""
|
||||
.. http:get:: /destinations/1
|
||||
@ -191,20 +208,30 @@ class Destinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -213,8 +240,8 @@ class Destinations(AuthenticatedResource):
|
||||
return service.get(destination_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, destination_id):
|
||||
@validate_schema(destination_input_schema, destination_output_schema)
|
||||
def put(self, destination_id, data=None):
|
||||
"""
|
||||
.. http:put:: /destinations/1
|
||||
|
||||
@ -228,23 +255,35 @@ class Destinations(AuthenticatedResource):
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test33",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "34324324",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
@ -254,20 +293,30 @@ class Destinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
|
||||
:arg accountNumber: aws account number
|
||||
@ -276,12 +325,7 @@ class Destinations(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(destination_id, args['label'], args['plugin']['pluginOptions'], args['description'])
|
||||
return service.update(destination_id, data['label'], data['plugin']['plugin_options'], data['description'])
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, destination_id):
|
||||
@ -294,7 +338,7 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateDestinations, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, destination_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/destinations
|
||||
@ -318,24 +362,32 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"destinationOptions": [
|
||||
{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": 111111111112,
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"pluginName": "aws-destination",
|
||||
"id": 3,
|
||||
"description": "test",
|
||||
"label": "test"
|
||||
}
|
||||
],
|
||||
"items": [{
|
||||
"description": "test",
|
||||
"options": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"id": 4,
|
||||
"plugin": {
|
||||
"pluginOptions": [{
|
||||
"name": "accountNumber",
|
||||
"required": true,
|
||||
"value": "111111111111111",
|
||||
"helpMessage": "Must be a valid AWS account number!",
|
||||
"validation": "/^[0-9]{12,12}$/",
|
||||
"type": "str"
|
||||
}],
|
||||
"description": "Allow the uploading of certificates to AWS IAM",
|
||||
"slug": "aws-destination",
|
||||
"title": "AWS"
|
||||
},
|
||||
"label": "test546"
|
||||
}
|
||||
"total": 1
|
||||
}
|
||||
|
||||
@ -343,7 +395,7 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
|
@ -17,3 +17,6 @@ class Domain(db.Model):
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(256))
|
||||
sensitive = Column(Boolean, default=False)
|
||||
|
||||
def __repr__(self):
|
||||
return "Domain(name={name})".format(name=self.name)
|
||||
|
35
lemur/domains/schemas.py
Normal file
35
lemur/domains/schemas.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
.. module: lemur.domains.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import AssociatedCertificateSchema
|
||||
|
||||
# from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
|
||||
|
||||
class DomainInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String(required=True)
|
||||
sensitive = fields.Boolean()
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class DomainOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
sensitive = fields.Boolean()
|
||||
# certificates = fields.Nested(CertificateNestedOutputSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class DomainNestedOutputSchema(DomainOutputSchema):
|
||||
__envelope__ = False
|
||||
|
||||
|
||||
domain_input_schema = DomainInputSchema()
|
||||
domain_output_schema = DomainOutputSchema()
|
||||
domains_output_schema = DomainOutputSchema(many=True)
|
@ -77,11 +77,6 @@ def render(args):
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Domain).join(Certificate, Domain.certificate)
|
||||
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -92,9 +87,4 @@ def render(args):
|
||||
if certificate_id:
|
||||
query = query.filter(Certificate.id == certificate_id)
|
||||
|
||||
query = database.find_all(query, Domain, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Domain, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Domain, args)
|
||||
|
@ -8,19 +8,16 @@
|
||||
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.domains import service
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import SensitiveDomainPermission
|
||||
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
FIELDS = {
|
||||
'id': fields.Integer,
|
||||
'name': fields.String,
|
||||
'sensitive': fields.Boolean
|
||||
}
|
||||
from lemur.domains.schemas import domain_input_schema, domain_output_schema, domains_output_schema
|
||||
|
||||
mod = Blueprint('domains', __name__)
|
||||
api = Api(mod)
|
||||
@ -31,7 +28,7 @@ class DomainsList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(DomainsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domains_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /domains
|
||||
@ -74,7 +71,7 @@ class DomainsList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number. default is 10
|
||||
:query count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
@ -83,8 +80,8 @@ class DomainsList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(domain_input_schema, domain_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /domains
|
||||
|
||||
@ -121,15 +118,12 @@ class DomainsList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['name'], args['sensitive'])
|
||||
return service.create(data['name'], data['sensitive'])
|
||||
|
||||
|
||||
class Domains(AuthenticatedResource):
|
||||
@ -137,7 +131,7 @@ class Domains(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Domains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domain_output_schema)
|
||||
def get(self, domain_id):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
@ -172,8 +166,8 @@ class Domains(AuthenticatedResource):
|
||||
"""
|
||||
return service.get(domain_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, domain_id):
|
||||
@validate_schema(domain_input_schema, domain_output_schema)
|
||||
def put(self, domain_id, data=None):
|
||||
"""
|
||||
.. http:get:: /domains/1
|
||||
|
||||
@ -210,12 +204,8 @@ class Domains(AuthenticatedResource):
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('sensitive', type=bool, default=False, location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
if SensitiveDomainPermission().can():
|
||||
return service.update(domain_id, args['name'], args['sensitive'])
|
||||
return service.update(domain_id, data['name'], data['sensitive'])
|
||||
|
||||
return dict(message='You are not authorized to modify this domain'), 403
|
||||
|
||||
@ -225,7 +215,7 @@ class CertificateDomains(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateDomains, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, domains_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/domains
|
||||
@ -268,7 +258,7 @@ class CertificateDomains(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
0
lemur/endpoints/__init__.py
Normal file
0
lemur/endpoints/__init__.py
Normal file
86
lemur/endpoints/models.py
Normal file
86
lemur/endpoints/models.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models need to create a authority within Lemur.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, func, DateTime, PassiveDefault, Boolean, ForeignKey
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.sql.expression import case
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
from lemur.models import policies_ciphers
|
||||
|
||||
|
||||
BAD_CIPHERS = [
|
||||
'Protocol-SSLv3',
|
||||
'Protocol-SSLv2',
|
||||
'Protocol-TLSv1'
|
||||
]
|
||||
|
||||
|
||||
class Cipher(db.Model):
|
||||
__tablename__ = 'ciphers'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(128), nullable=False)
|
||||
|
||||
@hybrid_property
|
||||
def deprecated(self):
|
||||
return self.name in BAD_CIPHERS
|
||||
|
||||
@deprecated.expression
|
||||
def deprecated(cls):
|
||||
return case(
|
||||
[
|
||||
(cls.name in BAD_CIPHERS, True)
|
||||
],
|
||||
else_=False
|
||||
)
|
||||
|
||||
|
||||
class Policy(db.Model):
|
||||
___tablename__ = 'policies'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(128), nullable=True)
|
||||
ciphers = relationship('Cipher', secondary=policies_ciphers, backref='policy')
|
||||
|
||||
|
||||
class Endpoint(db.Model):
|
||||
__tablename__ = 'endpoints'
|
||||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
name = Column(String(128))
|
||||
dnsname = Column(String(256))
|
||||
type = Column(String(128))
|
||||
active = Column(Boolean, default=True)
|
||||
port = Column(Integer)
|
||||
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
|
||||
policy_id = Column(Integer, ForeignKey('policy.id'))
|
||||
policy = relationship('Policy', backref='endpoint')
|
||||
certificate_id = Column(Integer, ForeignKey('certificates.id'))
|
||||
source_id = Column(Integer, ForeignKey('sources.id'))
|
||||
sensitive = Column(Boolean, default=False)
|
||||
source = relationship('Source', back_populates='endpoints')
|
||||
|
||||
@property
|
||||
def issues(self):
|
||||
issues = []
|
||||
|
||||
for cipher in self.policy.ciphers:
|
||||
if cipher.deprecated:
|
||||
issues.append({'name': 'deprecated cipher', 'value': '{0} has been deprecated consider removing it.'.format(cipher.name)})
|
||||
|
||||
if self.certificate.expired:
|
||||
issues.append({'name': 'expired certificate', 'value': 'There is an expired certificate attached to this endpoint consider replacing it.'})
|
||||
|
||||
if self.certificate.revoked:
|
||||
issues.append({'name': 'revoked', 'value': 'There is a revoked certificate attached to this endpoint consider replacing it.'})
|
||||
|
||||
return issues
|
||||
|
||||
def __repr__(self):
|
||||
return "Endpoint(name={name})".format(name=self.name)
|
43
lemur/endpoints/schemas.py
Normal file
43
lemur/endpoints/schemas.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.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 LemurOutputSchema
|
||||
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
|
||||
|
||||
class CipherNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
deprecated = fields.Boolean()
|
||||
name = fields.String()
|
||||
|
||||
|
||||
class PolicyNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
ciphers = fields.Nested(CipherNestedOutputSchema, many=True)
|
||||
|
||||
|
||||
class EndpointOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
name = fields.String()
|
||||
dnsname = fields.String()
|
||||
owner = fields.Email()
|
||||
type = fields.String()
|
||||
port = fields.Integer()
|
||||
active = fields.Boolean()
|
||||
certificate = fields.Nested(CertificateNestedOutputSchema)
|
||||
policy = fields.Nested(PolicyNestedOutputSchema)
|
||||
|
||||
issues = fields.List(fields.Dict())
|
||||
|
||||
endpoint_output_schema = EndpointOutputSchema()
|
||||
endpoints_output_schema = EndpointOutputSchema(many=True)
|
143
lemur/endpoints/service.py
Normal file
143
lemur/endpoints/service.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.service
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer endpoints in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.endpoints.models import Endpoint, Policy, Cipher
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
def get_all():
|
||||
"""
|
||||
Get all endpoints that are currently in Lemur.
|
||||
|
||||
:rtype : List
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Endpoint)
|
||||
return database.find_all(query, Endpoint, {}).all()
|
||||
|
||||
|
||||
def get(endpoint_id):
|
||||
"""
|
||||
Retrieves an endpoint given it's ID
|
||||
|
||||
:param endpoint_id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Endpoint, endpoint_id)
|
||||
|
||||
|
||||
def get_by_dnsname(endpoint_dnsname):
|
||||
"""
|
||||
Retrieves an endpoint given it's name.
|
||||
|
||||
:param endpoint_dnsname:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Endpoint, endpoint_dnsname, field='dnsname')
|
||||
|
||||
|
||||
def get_by_source(source_label):
|
||||
"""
|
||||
Retrieves all endpoints for a given source.
|
||||
:param source_label:
|
||||
:return:
|
||||
"""
|
||||
return Endpoint.query.filter(Endpoint.source.label == source_label).all() # noqa
|
||||
|
||||
|
||||
def create(**kwargs):
|
||||
"""
|
||||
Creates a new endpoint.
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
endpoint = Endpoint(**kwargs)
|
||||
database.create(endpoint)
|
||||
metrics.send('endpoint_added', 'counter', 1)
|
||||
return endpoint
|
||||
|
||||
|
||||
def get_or_create_policy(**kwargs):
|
||||
policy = database.get(Policy, kwargs['name'], field='name')
|
||||
|
||||
if not policy:
|
||||
policy = Policy(**kwargs)
|
||||
database.create(policy)
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def get_or_create_cipher(**kwargs):
|
||||
cipher = database.get(Cipher, kwargs['name'], field='name')
|
||||
|
||||
if not cipher:
|
||||
cipher = Cipher(**kwargs)
|
||||
database.create(cipher)
|
||||
|
||||
return cipher
|
||||
|
||||
|
||||
def update(endpoint_id, **kwargs):
|
||||
endpoint = database.get(Endpoint, endpoint_id)
|
||||
|
||||
endpoint.policy = kwargs['policy']
|
||||
endpoint.certificate = kwargs['certificate']
|
||||
database.update(endpoint)
|
||||
return endpoint
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper that helps us render the REST Api responses.
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(Endpoint)
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
if 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
query = query.filter(Endpoint.active == terms[1])
|
||||
elif 'port' in filt:
|
||||
if terms[1] != 'null': # ng-table adds 'null' if a number is removed
|
||||
query = query.filter(Endpoint.port == terms[1])
|
||||
elif 'ciphers' in filt:
|
||||
query = query.filter(
|
||||
Cipher.name == terms[1]
|
||||
)
|
||||
else:
|
||||
query = database.filter(query, Endpoint, terms)
|
||||
|
||||
return database.sort_and_page(query, Endpoint, args)
|
||||
|
||||
|
||||
def stats(**kwargs):
|
||||
"""
|
||||
Helper that defines some useful statistics about endpoints.
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
attr = getattr(Endpoint, kwargs.get('metric'))
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for key, count in items:
|
||||
keys.append(key)
|
||||
values.append(count)
|
||||
|
||||
return {'labels': keys, 'values': values}
|
106
lemur/endpoints/views.py
Normal file
106
lemur/endpoints/views.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.views
|
||||
: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 Blueprint
|
||||
from flask.ext.restful import reqparse, Api
|
||||
|
||||
from lemur.common.utils import paginated_parser
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
from lemur.endpoints import service
|
||||
from lemur.endpoints.schemas import endpoint_output_schema, endpoints_output_schema
|
||||
|
||||
|
||||
mod = Blueprint('endpoints', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class EndpointsList(AuthenticatedResource):
|
||||
""" Defines the 'endpoints' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(EndpointsList, self).__init__()
|
||||
|
||||
@validate_schema(None, endpoints_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /endpoints
|
||||
|
||||
The current list of endpoints
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /endpoints HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: 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
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
:note: this will only show certificates that the current user is authorized to use
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class Endpoints(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Endpoints, self).__init__()
|
||||
|
||||
@validate_schema(None, endpoint_output_schema)
|
||||
def get(self, endpoint_id):
|
||||
"""
|
||||
.. http:get:: /endpoints/1
|
||||
|
||||
One endpoint
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /endpoints/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return service.get(endpoint_id)
|
||||
|
||||
|
||||
api.add_resource(EndpointsList, '/endpoints', endpoint='endpoints')
|
||||
api.add_resource(Endpoints, '/endpoints/<int:endpoint_id>', endpoint='endpoint')
|
@ -36,6 +36,14 @@ class IntegrityError(LemurException):
|
||||
return repr(self.message)
|
||||
|
||||
|
||||
class AssociatedObjectNotFound(LemurException):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.message)
|
||||
|
||||
|
||||
class InvalidListener(LemurException):
|
||||
def __str__(self):
|
||||
return repr("Invalid listener, ensure you select a certificate if you are using a secure protocol")
|
||||
|
@ -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()
|
||||
|
@ -14,12 +14,12 @@ import imp
|
||||
import errno
|
||||
import pkg_resources
|
||||
|
||||
from logging import Formatter
|
||||
from logging import Formatter, StreamHandler
|
||||
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 = (
|
||||
@ -90,12 +90,15 @@ def configure_app(app, config=None):
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
# respect the config first
|
||||
if config and config != 'None':
|
||||
app.config.from_object(from_file(config))
|
||||
|
||||
try:
|
||||
app.config.from_envvar("LEMUR_CONF")
|
||||
except RuntimeError:
|
||||
if config and config != 'None':
|
||||
app.config.from_object(from_file(config))
|
||||
elif os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")):
|
||||
# look in default paths
|
||||
if os.path.isfile(os.path.expanduser("~/.lemur/lemur.conf.py")):
|
||||
app.config.from_object(from_file(os.path.expanduser("~/.lemur/lemur.conf.py")))
|
||||
else:
|
||||
app.config.from_object(from_file(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default.conf.py')))
|
||||
@ -112,6 +115,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):
|
||||
@ -143,14 +147,19 @@ def configure_logging(app):
|
||||
app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG'))
|
||||
app.logger.addHandler(handler)
|
||||
|
||||
stream_handler = StreamHandler()
|
||||
stream_handler.setLevel(app.config.get('LOG_LEVEL'))
|
||||
app.logger.addHandler(stream_handler)
|
||||
|
||||
|
||||
def install_plugins(app):
|
||||
"""
|
||||
Installs new issuers that are not currently bundled with Lemur.
|
||||
|
||||
:param settings:
|
||||
:param app:
|
||||
:return:
|
||||
"""
|
||||
from lemur.plugins import plugins
|
||||
from lemur.plugins.base import register
|
||||
# entry_points={
|
||||
# 'lemur.plugins': [
|
||||
@ -165,3 +174,11 @@ def install_plugins(app):
|
||||
app.logger.error("Failed to load plugin %r:\n%s\n" % (ep.name, traceback.format_exc()))
|
||||
else:
|
||||
register(plugin)
|
||||
|
||||
# ensure that we have some way to notify
|
||||
with app.app_context():
|
||||
try:
|
||||
slug = app.config.get("LEMUR_DEFAULT_NOTIFICATION_PLUGIN", "email-notification")
|
||||
plugins.get(slug)
|
||||
except KeyError:
|
||||
raise Exception("Unable to location notification plugin: {slug}. Ensure that LEMUR_DEFAULT_NOTIFICATION_PLUGIN is set to a valid and installed notification plugin.".format(slug=slug))
|
||||
|
289
lemur/manage.py
289
lemur/manage.py
@ -1,36 +1,39 @@
|
||||
from __future__ import unicode_literals # at top of module
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from collections import Counter
|
||||
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import arrow
|
||||
import requests
|
||||
import json
|
||||
|
||||
from tabulate import tabulate
|
||||
from gunicorn.config import make_settings
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from lockfile import LockFile, LockTimeout
|
||||
|
||||
from flask import current_app
|
||||
from flask.ext.script import Manager, Command, Option, prompt_pass
|
||||
from flask.ext.migrate import Migrate, MigrateCommand, stamp
|
||||
from flask_script.commands import ShowUrls, Clean, Server
|
||||
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics
|
||||
from lemur.users import service as user_service
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.certificates import service as cert_service
|
||||
from lemur.sources import service as source_service
|
||||
from lemur.authorities import service as authority_service
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.certificates.models import get_name_from_arn
|
||||
from lemur.certificates.service import get_name_from_arn
|
||||
from lemur.certificates.verify import verify_string
|
||||
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
from lemur.sources.service import sync
|
||||
from lemur.sources import service as source_service
|
||||
|
||||
from lemur import create_app
|
||||
|
||||
@ -62,8 +65,6 @@ CONFIG_TEMPLATE = """
|
||||
import os
|
||||
_basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
ADMINS = frozenset([''])
|
||||
|
||||
THREADS_PER_PAGE = 8
|
||||
|
||||
# General
|
||||
@ -188,58 +189,6 @@ def generate_settings():
|
||||
return output
|
||||
|
||||
|
||||
@manager.option('-s', '--sources', dest='labels')
|
||||
def sync_sources(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 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(
|
||||
label=source.label,
|
||||
description=source.description,
|
||||
active=source.active
|
||||
)
|
||||
)
|
||||
else:
|
||||
start_time = time.time()
|
||||
lock_file = "/tmp/.lemur_lock"
|
||||
sync_lock = LockFile(lock_file)
|
||||
|
||||
while not sync_lock.i_am_locking():
|
||||
try:
|
||||
sync_lock.acquire(timeout=10) # wait up to 10 seconds
|
||||
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
|
||||
if labels[0] == 'all':
|
||||
sync()
|
||||
else:
|
||||
sync(labels=labels)
|
||||
|
||||
sys.stdout.write(
|
||||
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
||||
time=(time.time() - start_time)
|
||||
)
|
||||
)
|
||||
except LockTimeout:
|
||||
sys.stderr.write(
|
||||
"[!] Unable to acquire file lock on {file}, is there another sync running?\n".format(
|
||||
file=lock_file
|
||||
)
|
||||
)
|
||||
sync_lock.break_lock()
|
||||
sync_lock.acquire()
|
||||
sync_lock.release()
|
||||
|
||||
sync_lock.release()
|
||||
|
||||
|
||||
@manager.command
|
||||
def notify():
|
||||
"""
|
||||
@ -317,7 +266,7 @@ class InitializeApp(Command):
|
||||
|
||||
class CreateUser(Command):
|
||||
"""
|
||||
This command allows for the creation of a new user within Lemur
|
||||
This command allows for the creation of a new user within Lemur.
|
||||
"""
|
||||
option_list = (
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
@ -333,18 +282,46 @@ class CreateUser(Command):
|
||||
if role_obj:
|
||||
role_objs.append(role_obj)
|
||||
else:
|
||||
sys.stderr.write("[!] Cannot find role {0}".format(r))
|
||||
sys.stderr.write("[!] Cannot find role {0}\n".format(r))
|
||||
sys.exit(1)
|
||||
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match")
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
user_service.create(username, password1, email, active, None, role_objs)
|
||||
sys.stdout.write("[+] Created new user: {0}".format(username))
|
||||
sys.stdout.write("[+] Created new user: {0}\n".format(username))
|
||||
|
||||
|
||||
class ResetPassword(Command):
|
||||
"""
|
||||
This command allows you to reset a user's password.
|
||||
"""
|
||||
option_list = (
|
||||
Option('-u', '--username', dest='username', required=True),
|
||||
)
|
||||
|
||||
def run(self, username):
|
||||
user = user_service.get_by_username(username)
|
||||
|
||||
if not user:
|
||||
sys.stderr.write("[!] No user found for username: {0}\n".format(username))
|
||||
sys.exit(1)
|
||||
|
||||
sys.stderr.write("[+] Resetting password for {0}\n".format(username))
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match\n")
|
||||
sys.exit(1)
|
||||
|
||||
user.password = password1
|
||||
user.hash_password()
|
||||
database.commit()
|
||||
|
||||
|
||||
class CreateRole(Command):
|
||||
@ -391,7 +368,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
|
||||
@ -791,60 +768,180 @@ def publish_verisign_units():
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
class Rolling(Command):
|
||||
@manager.command
|
||||
def publish_unapproved_verisign_certificates():
|
||||
"""
|
||||
Rotates existing certificates to a new one on an ELB
|
||||
Query the Verisign for any certificates that need to be approved.
|
||||
:return:
|
||||
"""
|
||||
from lemur.plugins import plugins
|
||||
from lemur.extensions import metrics
|
||||
v = plugins.get('verisign-issuer')
|
||||
certs = v.get_pending_certificates()
|
||||
metrics.send('pending_certificates', 'gauge', certs)
|
||||
|
||||
|
||||
class Report(Command):
|
||||
"""
|
||||
Defines a set of reports to be run periodically against Lemur.
|
||||
"""
|
||||
option_list = (
|
||||
Option('-w', '--window', dest='window', default=24),
|
||||
Option('-n', '--name', dest='name', default=None, help='Name of the report to run.'),
|
||||
Option('-d', '--duration', dest='duration', default=356, help='Number of days to run the report'),
|
||||
)
|
||||
|
||||
def run(self, window):
|
||||
def run(self, name, duration):
|
||||
|
||||
end = datetime.utcnow()
|
||||
start = end - timedelta(days=duration)
|
||||
self.certificates_issued(name, start, end)
|
||||
|
||||
@staticmethod
|
||||
def certificates_issued(name=None, start=None, end=None):
|
||||
"""
|
||||
Simple function that queries verisign for API units and posts the mertics to
|
||||
Atlas API for other teams to consume.
|
||||
Generates simple report of number of certificates issued by the authority, if no authority
|
||||
is specified report on total number of certificates.
|
||||
|
||||
:param name:
|
||||
:param start:
|
||||
:param end:
|
||||
: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
|
||||
def _calculate_row(authority):
|
||||
day_cnt = Counter()
|
||||
month_cnt = Counter()
|
||||
year_cnt = Counter()
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
for cert in authority.certificates:
|
||||
date = cert.date_created.date()
|
||||
day_cnt[date.day] += 1
|
||||
month_cnt[date.month] += 1
|
||||
year_cnt[date.year] += 1
|
||||
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
try:
|
||||
day_avg = int(sum(day_cnt.values()) / len(day_cnt.keys()))
|
||||
except ZeroDivisionError:
|
||||
day_avg = 0
|
||||
|
||||
try:
|
||||
month_avg = int(sum(month_cnt.values()) / len(month_cnt.keys()))
|
||||
except ZeroDivisionError:
|
||||
month_avg = 0
|
||||
|
||||
try:
|
||||
year_avg = int(sum(year_cnt.values()) / len(year_cnt.keys()))
|
||||
except ZeroDivisionError:
|
||||
year_avg = 0
|
||||
|
||||
return [authority.name, authority.description, day_avg, month_avg, year_avg]
|
||||
|
||||
rows = []
|
||||
if not name:
|
||||
for authority in authority_service.get_all():
|
||||
rows.append(_calculate_row(authority))
|
||||
|
||||
else:
|
||||
authority = authority_service.get_by_name(name)
|
||||
|
||||
if not authority:
|
||||
sys.stderr.write('[!] Authority {0} was not found.'.format(name))
|
||||
sys.exit(1)
|
||||
|
||||
rows.append(_calculate_row(authority))
|
||||
|
||||
sys.stdout.write(tabulate(rows, headers=["Authority Name", "Description", "Daily Average", "Monthy Average", "Yearly Average"]) + "\n")
|
||||
|
||||
|
||||
class Sources(Command):
|
||||
"""
|
||||
Defines a set of actions to take against Lemur's sources.
|
||||
"""
|
||||
option_list = (
|
||||
Option('-s', '--sources', dest='source_strings', action='append', help='Sources to operate on.'),
|
||||
Option('-a', '--action', choices=['sync', 'clean'], dest='action', help='Action to take on source.')
|
||||
)
|
||||
|
||||
def run(self, source_strings, action):
|
||||
sources = []
|
||||
if not source_strings:
|
||||
table = []
|
||||
for source in source_service.get_all():
|
||||
table.append([source.label, source.active, source.description])
|
||||
|
||||
sys.stdout.write(tabulate(table, headers=['Label', 'Active', 'Description']))
|
||||
sys.exit(1)
|
||||
|
||||
elif 'all' in source_strings:
|
||||
sources = source_service.get_all()
|
||||
|
||||
else:
|
||||
for source_str in source_strings:
|
||||
source = source_service.get_by_label(source_str)
|
||||
|
||||
if not source:
|
||||
sys.stderr.write("Unable to find specified source with label: {0}".format(source_str))
|
||||
|
||||
sources.append(source)
|
||||
|
||||
for source in sources:
|
||||
if action == 'sync':
|
||||
self.sync(source)
|
||||
|
||||
if action == 'clean':
|
||||
self.clean(source)
|
||||
|
||||
@staticmethod
|
||||
def sync(source):
|
||||
start_time = time.time()
|
||||
sys.stdout.write("[+] Staring to sync source: {label}!\n".format(label=source.label))
|
||||
|
||||
user = user_service.get_by_username('lemur')
|
||||
|
||||
try:
|
||||
source_service.sync(source, user)
|
||||
sys.stdout.write(
|
||||
"[+] Finished syncing source: {label}. Run Time: {time}\n".format(
|
||||
label=source.label,
|
||||
time=(time.time() - start_time)
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
|
||||
sys.stdout.write(
|
||||
"[X] Failed syncing source {label}!\n".format(label=source.label)
|
||||
)
|
||||
|
||||
metrics.send('sync_failed', 'counter', 1, metric_tags={'source': source.label})
|
||||
|
||||
@staticmethod
|
||||
def clean(source):
|
||||
start_time = time.time()
|
||||
sys.stdout.write("[+] Staring to clean source: {label}!\n".format(label=source.label))
|
||||
source_service.clean(source)
|
||||
sys.stdout.write(
|
||||
"[+] Finished cleaning source: {label}. Run Time: {time}\n".format(
|
||||
label=source.label,
|
||||
time=(time.time() - start_time)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1'))
|
||||
manager.add_command("runserver", Server(host='127.0.0.1', threaded=True))
|
||||
manager.add_command("clean", Clean())
|
||||
manager.add_command("show_urls", ShowUrls())
|
||||
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.add_command("sources", Sources())
|
||||
manager.add_command("report", Report())
|
||||
manager.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
32
lemur/metrics.py
Normal file
32
lemur/metrics.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""
|
||||
.. module: lemur.metrics
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
class Metrics(object):
|
||||
"""
|
||||
:param app: The Flask application object. Defaults to None.
|
||||
"""
|
||||
_providers = []
|
||||
|
||||
def __init__(self, app=None):
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Initializes the application with the extension.
|
||||
|
||||
:param app: The Flask application object.
|
||||
"""
|
||||
self._providers = app.config.get('METRIC_PROVIDERS', [])
|
||||
|
||||
def send(self, metric_name, metric_type, metric_value, *args, **kwargs):
|
||||
for provider in self._providers:
|
||||
current_app.logger.debug(
|
||||
"Sending metric '{metric}' to the {provider} provider.".format(metric=metric_name, provider=provider))
|
||||
p = plugins.get(provider)
|
||||
p.submit(metric_name, metric_type, metric_value, *args, **kwargs)
|
62
lemur/migrations/versions/29d8c8455c86_.py
Normal file
62
lemur/migrations/versions/29d8c8455c86_.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""Adding endpoint tables
|
||||
|
||||
Revision ID: 29d8c8455c86
|
||||
Revises: 3307381f3b88
|
||||
Create Date: 2016-06-28 16:05:25.720213
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '29d8c8455c86'
|
||||
down_revision = '3307381f3b88'
|
||||
|
||||
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('ciphers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('policy',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('policies_ciphers',
|
||||
sa.Column('cipher_id', sa.Integer(), nullable=True),
|
||||
sa.Column('policy_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['cipher_id'], ['ciphers.id'], ),
|
||||
sa.ForeignKeyConstraint(['policy_id'], ['policy.id'], )
|
||||
)
|
||||
op.create_index('policies_ciphers_ix', 'policies_ciphers', ['cipher_id', 'policy_id'], unique=False)
|
||||
op.create_table('endpoints',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('owner', sa.String(length=128), nullable=True),
|
||||
sa.Column('name', sa.String(length=128), nullable=True),
|
||||
sa.Column('dnsname', sa.String(length=256), nullable=True),
|
||||
sa.Column('type', sa.String(length=128), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=True),
|
||||
sa.Column('port', sa.Integer(), nullable=True),
|
||||
sa.Column('date_created', sa.DateTime(), server_default=sa.text(u'now()'), nullable=False),
|
||||
sa.Column('policy_id', sa.Integer(), nullable=True),
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
|
||||
sa.ForeignKeyConstraint(['policy_id'], ['policy.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('endpoints')
|
||||
op.drop_index('policies_ciphers_ix', table_name='policies_ciphers')
|
||||
op.drop_table('policies_ciphers')
|
||||
op.drop_table('policy')
|
||||
op.drop_table('ciphers')
|
||||
### end Alembic commands ###
|
131
lemur/migrations/versions/3307381f3b88_.py
Normal file
131
lemur/migrations/versions/3307381f3b88_.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""
|
||||
Refactor authority columns and associates an authorities root certificate with a certificate stored in the
|
||||
certificate tables.
|
||||
|
||||
Migrates existing authority owners to associated roles.
|
||||
Migrates existing certificate owners to associated role.
|
||||
|
||||
Revision ID: 3307381f3b88
|
||||
Revises: 412b22cb656a
|
||||
Create Date: 2016-05-20 17:33:04.360687
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3307381f3b88'
|
||||
down_revision = '412b22cb656a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('authorities', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
op.drop_column('authorities', 'not_after')
|
||||
op.drop_column('authorities', 'bits')
|
||||
op.drop_column('authorities', 'cn')
|
||||
op.drop_column('authorities', 'not_before')
|
||||
op.add_column('certificates', sa.Column('root_authority_id', sa.Integer(), nullable=True))
|
||||
op.alter_column('certificates', 'body',
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=False)
|
||||
op.alter_column('certificates', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
op.drop_constraint(u'certificates_authority_id_fkey', 'certificates', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'certificates', 'authorities', ['authority_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key(None, 'certificates', 'authorities', ['root_authority_id'], ['id'], ondelete='CASCADE')
|
||||
### end Alembic commands ###
|
||||
|
||||
# link existing certificate to their authority certificates
|
||||
conn = op.get_bind()
|
||||
for id, body, owner in conn.execute(text('select id, body, owner from authorities')):
|
||||
if not owner:
|
||||
owner = "lemur@nobody"
|
||||
|
||||
# look up certificate by body, if duplications are found, pick one
|
||||
stmt = text('select id from certificates where body=:body')
|
||||
stmt = stmt.bindparams(body=body)
|
||||
root_certificate = conn.execute(stmt).fetchone()
|
||||
if root_certificate:
|
||||
stmt = text('update certificates set root_authority_id=:root_authority_id where id=:id')
|
||||
stmt = stmt.bindparams(root_authority_id=id, id=root_certificate[0])
|
||||
op.execute(stmt)
|
||||
|
||||
# link owner roles to their authorities
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
if not owner_role:
|
||||
stmt = text('insert into roles (name, description) values (:name, :description)')
|
||||
stmt = stmt.bindparams(name=owner, description='Lemur generated role or existing owner.')
|
||||
op.execute(stmt)
|
||||
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
stmt = text('select * from roles_authorities where role_id=:role_id and authority_id=:authority_id')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], authority_id=id)
|
||||
exists = conn.execute(stmt).fetchone()
|
||||
|
||||
if not exists:
|
||||
stmt = text('insert into roles_authorities (role_id, authority_id) values (:role_id, :authority_id)')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], authority_id=id)
|
||||
op.execute(stmt)
|
||||
|
||||
# link owner roles to their certificates
|
||||
for id, owner in conn.execute(text('select id, owner from certificates')):
|
||||
if not owner:
|
||||
owner = "lemur@nobody"
|
||||
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
if not owner_role:
|
||||
stmt = text('insert into roles (name, description) values (:name, :description)')
|
||||
stmt = stmt.bindparams(name=owner, description='Lemur generated role or existing owner.')
|
||||
op.execute(stmt)
|
||||
|
||||
# link owner roles to their authorities
|
||||
stmt = text('select id from roles where name=:name')
|
||||
stmt = stmt.bindparams(name=owner)
|
||||
owner_role = conn.execute(stmt).fetchone()
|
||||
|
||||
stmt = text('select * from roles_certificates where role_id=:role_id and certificate_id=:certificate_id')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], certificate_id=id)
|
||||
exists = conn.execute(stmt).fetchone()
|
||||
|
||||
if not exists:
|
||||
stmt = text('insert into roles_certificates (role_id, certificate_id) values (:role_id, :certificate_id)')
|
||||
stmt = stmt.bindparams(role_id=owner_role[0], certificate_id=id)
|
||||
op.execute(stmt)
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'certificates', type_='foreignkey')
|
||||
op.drop_constraint(None, 'certificates', type_='foreignkey')
|
||||
op.create_foreign_key(u'certificates_authority_id_fkey', 'certificates', 'authorities', ['authority_id'], ['id'])
|
||||
op.alter_column('certificates', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
op.alter_column('certificates', 'body',
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=True)
|
||||
op.drop_column('certificates', 'root_authority_id')
|
||||
op.add_column('authorities', sa.Column('not_before', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
|
||||
op.add_column('authorities', sa.Column('cn', sa.VARCHAR(length=128), autoincrement=False, nullable=True))
|
||||
op.add_column('authorities', sa.Column('bits', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('authorities', sa.Column('not_after', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
|
||||
op.alter_column('authorities', 'owner',
|
||||
existing_type=sa.VARCHAR(length=128),
|
||||
nullable=True)
|
||||
### end Alembic commands ###
|
63
lemur/migrations/versions/412b22cb656a_.py
Normal file
63
lemur/migrations/versions/412b22cb656a_.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
|
||||
Revision ID: 412b22cb656a
|
||||
Revises: 4c50b903d1ae
|
||||
Create Date: 2016-05-17 17:37:41.210232
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '412b22cb656a'
|
||||
down_revision = '4c50b903d1ae'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('roles_authorities',
|
||||
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||
)
|
||||
op.create_index('roles_authorities_ix', 'roles_authorities', ['authority_id', 'role_id'], unique=True)
|
||||
op.create_table('roles_certificates',
|
||||
sa.Column('certificate_id', sa.Integer(), nullable=True),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||
)
|
||||
op.create_index('roles_certificates_ix', 'roles_certificates', ['certificate_id', 'role_id'], unique=True)
|
||||
op.create_index('certificate_associations_ix', 'certificate_associations', ['domain_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_destination_associations_ix', 'certificate_destination_associations', ['destination_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_notification_associations_ix', 'certificate_notification_associations', ['notification_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_replacement_associations_ix', 'certificate_replacement_associations', ['certificate_id', 'certificate_id'], unique=True)
|
||||
op.create_index('certificate_source_associations_ix', 'certificate_source_associations', ['source_id', 'certificate_id'], unique=True)
|
||||
op.create_index('roles_users_ix', 'roles_users', ['user_id', 'role_id'], unique=True)
|
||||
|
||||
### end Alembic commands ###
|
||||
|
||||
# migrate existing authority_id relationship to many_to_many
|
||||
conn = op.get_bind()
|
||||
for id, authority_id in conn.execute(text('select id, authority_id from roles where authority_id is not null')):
|
||||
stmt = text('insert into roles_authoritties (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 ###
|
35
lemur/migrations/versions/7f71c0cea31a_.py
Normal file
35
lemur/migrations/versions/7f71c0cea31a_.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Ensures that certificate name is unique.
|
||||
If duplicates are found, we follow the standard naming convention of appending '-X'
|
||||
with x being the number of duplicates starting at 1.
|
||||
|
||||
Revision ID: 7f71c0cea31a
|
||||
Revises: 29d8c8455c86
|
||||
Create Date: 2016-07-28 09:39:12.736506
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7f71c0cea31a'
|
||||
down_revision = '29d8c8455c86'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
for name in conn.execute(text('select name from certificates group by name having count(*) > 1')):
|
||||
for idx, id in enumerate(conn.execute(text("select id from certificates where certificates.name like :name order by id ASC").bindparams(name=name[0]))):
|
||||
if not idx:
|
||||
continue
|
||||
new_name = name[0] + '-' + str(idx)
|
||||
stmt = text('update certificates set name=:name where id=:id')
|
||||
stmt = stmt.bindparams(name=new_name, id=id[0])
|
||||
op.execute(stmt)
|
||||
|
||||
op.create_unique_constraint(None, 'certificates', ['name'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_constraint(None, 'certificates', type_='unique')
|
21
lemur/migrations/versions/932525b82f1a_.py
Normal file
21
lemur/migrations/versions/932525b82f1a_.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Changing the column name to the more accurately named 'notify'.
|
||||
|
||||
Revision ID: 932525b82f1a
|
||||
Revises: 7f71c0cea31a
|
||||
Create Date: 2016-10-13 20:14:33.928029
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '932525b82f1a'
|
||||
down_revision = '7f71c0cea31a'
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('certificates', 'active', new_column_name='notify')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('certificates', 'notify', new_column_name='active')
|
@ -8,7 +8,8 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Index
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
certificate_associations = db.Table('certificate_associations',
|
||||
@ -16,6 +17,8 @@ certificate_associations = db.Table('certificate_associations',
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id'))
|
||||
)
|
||||
|
||||
Index('certificate_associations_ix', certificate_associations.c.domain_id, certificate_associations.c.certificate_id)
|
||||
|
||||
certificate_destination_associations = db.Table('certificate_destination_associations',
|
||||
Column('destination_id', Integer,
|
||||
ForeignKey('destinations.id', ondelete='cascade')),
|
||||
@ -23,6 +26,8 @@ certificate_destination_associations = db.Table('certificate_destination_associa
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_destination_associations_ix', certificate_destination_associations.c.destination_id, certificate_destination_associations.c.certificate_id)
|
||||
|
||||
certificate_source_associations = db.Table('certificate_source_associations',
|
||||
Column('source_id', Integer,
|
||||
ForeignKey('sources.id', ondelete='cascade')),
|
||||
@ -30,6 +35,8 @@ certificate_source_associations = db.Table('certificate_source_associations',
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_source_associations_ix', certificate_source_associations.c.source_id, certificate_source_associations.c.certificate_id)
|
||||
|
||||
certificate_notification_associations = db.Table('certificate_notification_associations',
|
||||
Column('notification_id', Integer,
|
||||
ForeignKey('notifications.id', ondelete='cascade')),
|
||||
@ -37,6 +44,8 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_notification_associations_ix', certificate_notification_associations.c.notification_id, certificate_notification_associations.c.certificate_id)
|
||||
|
||||
certificate_replacement_associations = db.Table('certificate_replacement_associations',
|
||||
Column('replaced_certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade')),
|
||||
@ -44,7 +53,33 @@ certificate_replacement_associations = db.Table('certificate_replacement_associa
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.certificate_id, certificate_replacement_associations.c.certificate_id)
|
||||
|
||||
roles_authorities = db.Table('roles_authorities',
|
||||
Column('authority_id', Integer, ForeignKey('authorities.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id)
|
||||
|
||||
roles_certificates = db.Table('roles_certificates',
|
||||
Column('certificate_id', Integer, ForeignKey('certificates.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id)
|
||||
|
||||
|
||||
roles_users = db.Table('roles_users',
|
||||
Column('user_id', Integer, ForeignKey('users.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id)
|
||||
|
||||
|
||||
policies_ciphers = db.Table('policies_ciphers',
|
||||
Column('cipher_id', Integer, ForeignKey('ciphers.id')),
|
||||
Column('policy_id', Integer, ForeignKey('policy.id')))
|
||||
|
||||
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
||||
|
@ -33,3 +33,6 @@ class Notification(db.Model):
|
||||
@property
|
||||
def plugin(self):
|
||||
return plugins.get(self.plugin_name)
|
||||
|
||||
def __repr__(self):
|
||||
return "Notification(label={label})".format(label=self.label)
|
||||
|
49
lemur/notifications/schemas.py
Normal file
49
lemur/notifications/schemas.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""
|
||||
.. module: lemur.notifications.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from marshmallow import fields, post_dump
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.schemas import PluginInputSchema, PluginOutputSchema, AssociatedCertificateSchema
|
||||
|
||||
|
||||
class NotificationInputSchema(LemurInputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String(required=True)
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
plugin = fields.Nested(PluginInputSchema, required=True)
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
|
||||
class NotificationOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
options = fields.List(fields.Dict())
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
certificates = fields.Nested(AssociatedCertificateSchema, many=True, missing=[])
|
||||
|
||||
@post_dump
|
||||
def fill_object(self, data):
|
||||
data['plugin']['pluginOptions'] = data['options']
|
||||
return data
|
||||
|
||||
|
||||
class NotificationNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
label = fields.String()
|
||||
description = fields.String()
|
||||
active = fields.Boolean()
|
||||
options = fields.List(fields.Dict())
|
||||
plugin = fields.Nested(PluginOutputSchema)
|
||||
|
||||
|
||||
notification_input_schema = NotificationInputSchema()
|
||||
notification_output_schema = NotificationOutputSchema()
|
||||
notifications_output_schema = NotificationOutputSchema(many=True)
|
@ -45,6 +45,7 @@ def _get_message_data(cert):
|
||||
cert_dict['owner'] = cert.owner
|
||||
cert_dict['name'] = cert.name
|
||||
cert_dict['body'] = cert.body
|
||||
cert_dict['endpoints'] = [{'name': x.name, 'dnsname': x.dnsname} for x in cert.endpoints]
|
||||
|
||||
return cert_dict
|
||||
|
||||
@ -159,6 +160,9 @@ def _is_eligible_for_notifications(cert):
|
||||
:param cert:
|
||||
:return:
|
||||
"""
|
||||
if not cert.notify:
|
||||
return
|
||||
|
||||
now = arrow.utcnow()
|
||||
days = (cert.not_after - now.naive).days
|
||||
|
||||
@ -231,7 +235,7 @@ def create_default_expiration_notifications(name, recipients):
|
||||
inter.extend(options)
|
||||
n = create(
|
||||
label="{name}_{interval}_DAY".format(name=name, interval=i),
|
||||
plugin_name="email-notification",
|
||||
plugin_name=current_app.config.get("LEMUR_DEFAULT_NOTIFICATION_PLUGIN", "email-notification"),
|
||||
options=list(inter),
|
||||
description="Default {interval} day expiration notification".format(interval=i),
|
||||
certificates=[]
|
||||
@ -243,27 +247,31 @@ def create_default_expiration_notifications(name, recipients):
|
||||
|
||||
def create(label, plugin_name, options, description, certificates):
|
||||
"""
|
||||
Creates a new destination, that can then be used as a destination for certificates.
|
||||
Creates a new notification.
|
||||
|
||||
:param label: Notification common name
|
||||
:param label: Notification label
|
||||
:param plugin_name:
|
||||
:param options:
|
||||
:param description:
|
||||
:param certificates:
|
||||
:rtype : Notification
|
||||
:return:
|
||||
"""
|
||||
notification = Notification(label=label, options=options, plugin_name=plugin_name, description=description)
|
||||
notification = database.update_list(notification, 'certificates', Certificate, certificates)
|
||||
notification.certificates = certificates
|
||||
return database.create(notification)
|
||||
|
||||
|
||||
def update(notification_id, label, options, description, active, certificates):
|
||||
"""
|
||||
Updates an existing destination.
|
||||
Updates an existing notification.
|
||||
|
||||
:param label: Notification common name
|
||||
:param notification_id:
|
||||
:param label: Notification label
|
||||
:param options:
|
||||
:param description:
|
||||
:param active:
|
||||
:param certificates:
|
||||
:rtype : Notification
|
||||
:return:
|
||||
"""
|
||||
@ -273,7 +281,7 @@ def update(notification_id, label, options, description, active, certificates):
|
||||
notification.options = options
|
||||
notification.description = description
|
||||
notification.active = active
|
||||
notification = database.update_list(notification, 'certificates', Certificate, certificates)
|
||||
notification.certificates = certificates
|
||||
|
||||
return database.update(notification)
|
||||
|
||||
@ -319,10 +327,6 @@ def get_all():
|
||||
|
||||
|
||||
def render(args):
|
||||
sort_by = args.pop('sort_by')
|
||||
sort_dir = args.pop('sort_dir')
|
||||
page = args.pop('page')
|
||||
count = args.pop('count')
|
||||
filt = args.pop('filter')
|
||||
certificate_id = args.pop('certificate_id', None)
|
||||
|
||||
@ -341,9 +345,4 @@ def render(args):
|
||||
else:
|
||||
query = database.filter(query, Notification, terms)
|
||||
|
||||
query = database.find_all(query, Notification, args)
|
||||
|
||||
if sort_by and sort_dir:
|
||||
query = database.sort(query, Notification, sort_by, sort_dir)
|
||||
|
||||
return database.paginate(query, page, count)
|
||||
return database.sort_and_page(query, Notification, args)
|
||||
|
@ -7,64 +7,27 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint
|
||||
from flask.ext.restful import Api, reqparse, fields
|
||||
from flask.ext.restful import Api, reqparse
|
||||
from lemur.notifications import service
|
||||
from lemur.notifications.schemas import notification_input_schema, notification_output_schema, notifications_output_schema
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.common.utils import paginated_parser, marshal_items
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
|
||||
|
||||
mod = Blueprint('notifications', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'description': fields.String,
|
||||
'notificationOptions': fields.Raw(attribute='options'),
|
||||
'pluginName': fields.String(attribute='plugin_name'),
|
||||
'label': fields.String,
|
||||
'active': fields.Boolean,
|
||||
'id': fields.Integer,
|
||||
}
|
||||
|
||||
|
||||
def notification(value, name):
|
||||
"""
|
||||
Validates a given notification exits
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
n = service.get(value)
|
||||
if not n:
|
||||
raise ValueError("Unable to find notification specified")
|
||||
return n
|
||||
|
||||
|
||||
def notification_list(value, name):
|
||||
"""
|
||||
Validates a given notification exists and returns a list
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
notifications = []
|
||||
for v in value:
|
||||
try:
|
||||
notifications.append(notification(v['id'], 'id'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
class NotificationsList(AuthenticatedResource):
|
||||
""" Defines the 'notifications' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(NotificationsList, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, notifications_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /notifications
|
||||
@ -91,7 +54,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
"items": [
|
||||
{
|
||||
"description": "An example",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -135,7 +98,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
@ -144,8 +107,8 @@ class NotificationsList(AuthenticatedResource):
|
||||
args = parser.parse_args()
|
||||
return service.render(args)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def post(self):
|
||||
@validate_schema(notification_input_schema, notification_output_schema)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
.. http:post:: /notifications
|
||||
|
||||
@ -161,7 +124,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -208,7 +171,7 @@ class NotificationsList(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -251,18 +214,12 @@ class NotificationsList(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(
|
||||
args['label'],
|
||||
args['plugin']['slug'],
|
||||
args['plugin']['pluginOptions'],
|
||||
args['description'],
|
||||
args['certificates']
|
||||
data['label'],
|
||||
data['plugin']['slug'],
|
||||
data['plugin']['plugin_options'],
|
||||
data['description'],
|
||||
data['certificates']
|
||||
)
|
||||
|
||||
|
||||
@ -271,7 +228,7 @@ class Notifications(AuthenticatedResource):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(Notifications, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, notification_output_schema)
|
||||
def get(self, notification_id):
|
||||
"""
|
||||
.. http:get:: /notifications/1
|
||||
@ -296,7 +253,7 @@ class Notifications(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"description": "a test",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -338,8 +295,8 @@ class Notifications(AuthenticatedResource):
|
||||
"""
|
||||
return service.get(notification_id)
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
def put(self, notification_id):
|
||||
@validate_schema(notification_input_schema, notification_output_schema)
|
||||
def put(self, notification_id, data=None):
|
||||
"""
|
||||
.. http:put:: /notifications/1
|
||||
|
||||
@ -375,20 +332,13 @@ class Notifications(AuthenticatedResource):
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
self.reqparse.add_argument('label', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
|
||||
self.reqparse.add_argument('active', type=bool, location='json')
|
||||
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.update(
|
||||
notification_id,
|
||||
args['label'],
|
||||
args['plugin']['pluginOptions'],
|
||||
args['description'],
|
||||
args['active'],
|
||||
args['certificates']
|
||||
data['label'],
|
||||
data['plugin']['plugin_options'],
|
||||
data['description'],
|
||||
data['active'],
|
||||
data['certificates']
|
||||
)
|
||||
|
||||
def delete(self, notification_id):
|
||||
@ -401,7 +351,7 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(CertificateNotifications, self).__init__()
|
||||
|
||||
@marshal_items(FIELDS)
|
||||
@validate_schema(None, notifications_output_schema)
|
||||
def get(self, certificate_id):
|
||||
"""
|
||||
.. http:get:: /certificates/1/notifications
|
||||
@ -428,7 +378,7 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
"items": [
|
||||
{
|
||||
"description": "An example",
|
||||
"notificationOptions": [
|
||||
"options": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
@ -472,15 +422,11 @@ class CertificateNotifications(AuthenticatedResource):
|
||||
:query sortDir: acs or desc
|
||||
:query page: int default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query limit: limit number default is 10
|
||||
:query count: count number default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('active', type=bool, location='args')
|
||||
args = parser.parse_args()
|
||||
args['certificate_id'] = certificate_id
|
||||
return service.render(args)
|
||||
return service.render({'certificate_id': certificate_id})
|
||||
|
||||
|
||||
api.add_resource(NotificationsList, '/notifications', endpoint='notifications')
|
||||
|
@ -112,7 +112,7 @@ class IPlugin(local):
|
||||
def get_option(name, options):
|
||||
for o in options:
|
||||
if o.get('name') == name:
|
||||
return o.get('value')
|
||||
return o.get('value', o.get('default'))
|
||||
|
||||
|
||||
class Plugin(IPlugin):
|
||||
|
@ -11,6 +11,7 @@ from lemur.plugins.base import Plugin
|
||||
|
||||
class DestinationPlugin(Plugin):
|
||||
type = 'destination'
|
||||
requires_key = True
|
||||
|
||||
def upload(self):
|
||||
raise NotImplemented
|
||||
|
16
lemur/plugins/bases/metric.py
Normal file
16
lemur/plugins/bases/metric.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
.. module: lemur.bases.metric
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from lemur.plugins.base import Plugin
|
||||
|
||||
|
||||
class MetricPlugin(Plugin):
|
||||
type = 'metric'
|
||||
|
||||
def submit(self, *args, **kwargs):
|
||||
raise NotImplemented
|
@ -25,6 +25,12 @@ class SourcePlugin(Plugin):
|
||||
def get_certificates(self):
|
||||
raise NotImplemented
|
||||
|
||||
def get_endpoints(self):
|
||||
raise NotImplemented
|
||||
|
||||
def clean(self):
|
||||
raise NotImplemented
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return list(self.default_options) + self.additional_options
|
||||
|
5
lemur/plugins/lemur_acme/__init__.py
Normal file
5
lemur/plugins/lemur_acme/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
210
lemur/plugins/lemur_acme/plugin.py
Normal file
210
lemur/plugins/lemur_acme/plugin.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_acme.acme
|
||||
:platform: Unix
|
||||
:synopsis: This module is responsible for communicating with a ACME CA.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
Snippets from https://raw.githubusercontent.com/alex/letsencrypt-aws/master/letsencrypt-aws.py
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from acme.client import Client
|
||||
from acme import jose
|
||||
from acme import messages
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
import OpenSSL.crypto
|
||||
|
||||
from lemur.plugins.bases import IssuerPlugin
|
||||
from lemur.plugins import lemur_acme as acme
|
||||
|
||||
from .route53 import delete_txt_record, create_txt_record, wait_for_change
|
||||
|
||||
|
||||
def find_dns_challenge(authz):
|
||||
for combo in authz.body.resolved_combinations:
|
||||
if (
|
||||
len(combo) == 1 and
|
||||
isinstance(combo[0].chall, acme.challenges.DNS01)
|
||||
):
|
||||
yield combo[0]
|
||||
|
||||
|
||||
class AuthorizationRecord(object):
|
||||
def __init__(self, host, authz, dns_challenge, change_id):
|
||||
self.host = host
|
||||
self.authz = authz
|
||||
self.dns_challenge = dns_challenge
|
||||
self.change_id = change_id
|
||||
|
||||
|
||||
def start_dns_challenge(acme_client, host):
|
||||
authz = acme_client.request_domain_challenges(
|
||||
host, acme_client.directory.new_authz
|
||||
)
|
||||
|
||||
[dns_challenge] = find_dns_challenge(authz)
|
||||
|
||||
change_id = create_txt_record(
|
||||
dns_challenge.validation_domain_name(host),
|
||||
dns_challenge.validation(acme_client.key),
|
||||
|
||||
)
|
||||
return AuthorizationRecord(
|
||||
host,
|
||||
authz,
|
||||
dns_challenge,
|
||||
change_id,
|
||||
)
|
||||
|
||||
|
||||
def complete_dns_challenge(acme_client, authz_record):
|
||||
wait_for_change(authz_record.change_id)
|
||||
|
||||
response = authz_record.dns_challenge.response(acme_client.key)
|
||||
|
||||
verified = response.simple_verify(
|
||||
authz_record.dns_challenge.chall,
|
||||
authz_record.host,
|
||||
acme_client.key.public_key()
|
||||
)
|
||||
if not verified:
|
||||
raise ValueError("Failed verification")
|
||||
|
||||
acme_client.answer_challenge(authz_record.dns_challenge, response)
|
||||
|
||||
|
||||
def request_certificate(acme_client, authorizations, csr):
|
||||
cert_response, _ = acme_client.poll_and_request_issuance(
|
||||
jose.util.ComparableX509(
|
||||
OpenSSL.crypto.load_certificate_request(
|
||||
OpenSSL.crypto.FILETYPE_ASN1,
|
||||
csr.public_bytes(serialization.Encoding.DER),
|
||||
)
|
||||
),
|
||||
authzrs=[authz_record.authz for authz_record in authorizations],
|
||||
)
|
||||
pem_certificate = OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_PEM, cert_response.body
|
||||
)
|
||||
pem_certificate_chain = "\n".join(
|
||||
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
|
||||
for cert in acme_client.fetch_chain(cert_response)
|
||||
)
|
||||
return pem_certificate, pem_certificate_chain
|
||||
|
||||
|
||||
def generate_rsa_private_key():
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=default_backend()
|
||||
)
|
||||
|
||||
|
||||
def setup_acme_client():
|
||||
key = current_app.config.get('ACME_PRIVATE_KEY').strip()
|
||||
acme_email = current_app.config.get('ACME_EMAIL')
|
||||
acme_tel = current_app.config.get('ACME_TEL')
|
||||
acme_directory_url = current_app.config.get('ACME_DIRECTORY_URL'),
|
||||
contact = ('mailto:{}'.format(acme_email), 'tel:{}'.format(acme_tel))
|
||||
|
||||
key = serialization.load_pem_private_key(
|
||||
key, password=None, backend=default_backend()
|
||||
)
|
||||
return acme_client_for_private_key(acme_directory_url, key)
|
||||
|
||||
|
||||
def acme_client_for_private_key(acme_directory_url, private_key):
|
||||
return Client(
|
||||
acme_directory_url, key=jose.JWKRSA(key=private_key)
|
||||
)
|
||||
|
||||
|
||||
def register(email):
|
||||
private_key = generate_rsa_private_key()
|
||||
acme_client = acme_client_for_private_key(current_app.config('ACME_DIRECTORY_URL'), private_key)
|
||||
|
||||
registration = acme_client.register(
|
||||
messages.NewRegistration.from_data(email=email)
|
||||
)
|
||||
acme_client.agree_to_tos(registration)
|
||||
return private_key
|
||||
|
||||
|
||||
def get_domains(options):
|
||||
"""
|
||||
Fetches all domains currently requested
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
domains = [options['common_name']]
|
||||
for name in options['extensions']['sub_alt_name']['names']:
|
||||
domains.append(name)
|
||||
return domains
|
||||
|
||||
|
||||
def get_authorizations(acme_client, domains):
|
||||
authorizations = []
|
||||
try:
|
||||
for domain in domains:
|
||||
authz_record = start_dns_challenge(acme_client, domain)
|
||||
authorizations.append(authz_record)
|
||||
|
||||
for authz_record in authorizations:
|
||||
complete_dns_challenge(acme_client, authz_record)
|
||||
finally:
|
||||
for authz_record in authorizations:
|
||||
dns_challenge = authz_record.dns_challenge
|
||||
delete_txt_record(
|
||||
authz_record.change_id,
|
||||
dns_challenge.validation_domain_name(authz_record.host),
|
||||
dns_challenge.validation(acme_client.key),
|
||||
)
|
||||
|
||||
return authorizations
|
||||
|
||||
|
||||
class ACMEIssuerPlugin(IssuerPlugin):
|
||||
title = 'Acme'
|
||||
slug = 'acme-issuer'
|
||||
description = 'Enables the creation of certificates via ACME CAs (including Let\'s Encrypt)'
|
||||
version = acme.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur.git'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
"""
|
||||
Creates a ACME certificate.
|
||||
|
||||
:param csr:
|
||||
:param issuer_options:
|
||||
:return: :raise Exception:
|
||||
"""
|
||||
current_app.logger.debug("Requesting a new acme certificate: {0}".format(issuer_options))
|
||||
acme_client = setup_acme_client()
|
||||
domains = get_domains(issuer_options)
|
||||
authorizations = get_authorizations(acme_client, domains)
|
||||
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr)
|
||||
return pem_certificate, pem_certificate_chain
|
||||
|
||||
@staticmethod
|
||||
def create_authority(options):
|
||||
"""
|
||||
Creates an authority, this authority is then used by Lemur to allow a user
|
||||
to specify which Certificate Authority they want to sign their certificate.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
role = {'username': '', 'password': '', 'name': 'acme'}
|
||||
return current_app.config.get('ACME_ROOT'), "", [role]
|
86
lemur/plugins/lemur_acme/route53.py
Normal file
86
lemur/plugins/lemur_acme/route53.py
Normal file
@ -0,0 +1,86 @@
|
||||
import time
|
||||
from lemur.plugins.lemur_aws.sts import sts_client
|
||||
|
||||
|
||||
@sts_client('route53')
|
||||
def wait_for_r53_change(change_id, client=None):
|
||||
_, change_id = change_id
|
||||
|
||||
while True:
|
||||
response = client.get_change(Id=change_id)
|
||||
if response["ChangeInfo"]["Status"] == "INSYNC":
|
||||
return
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
@sts_client('route53')
|
||||
def find_zone_id(domain, client=None):
|
||||
paginator = client.get_paginator("list_hosted_zones")
|
||||
zones = []
|
||||
for page in paginator.paginate():
|
||||
for zone in page["HostedZones"]:
|
||||
if domain.endswith(zone["Name"]) or (domain + ".").endswith(zone["Name"]):
|
||||
if not zone["Config"]["PrivateZone"]:
|
||||
zones.append((zone["Name"], zone["Id"]))
|
||||
|
||||
if not zones:
|
||||
raise ValueError(
|
||||
"Unable to find a Route53 hosted zone for {}".format(domain)
|
||||
)
|
||||
|
||||
|
||||
@sts_client('route53')
|
||||
def change_txt_record(action, zone_id, domain, value, client=None):
|
||||
response = client.change_resource_record_sets(
|
||||
HostedZoneId=zone_id,
|
||||
ChangeBatch={
|
||||
"Changes": [
|
||||
{
|
||||
"Action": action,
|
||||
"ResourceRecordSet": {
|
||||
"Name": domain,
|
||||
"Type": "TXT",
|
||||
"TTL": 300,
|
||||
"ResourceRecords": [
|
||||
# For some reason TXT records need to be
|
||||
# manually quoted.
|
||||
{"Value": '"{}"'.format(value)}
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
return response["ChangeInfo"]["Id"]
|
||||
|
||||
|
||||
def create_txt_record(host, value):
|
||||
zone_id = find_zone_id(host)
|
||||
change_id = change_txt_record(
|
||||
"CREATE",
|
||||
zone_id,
|
||||
host,
|
||||
value,
|
||||
)
|
||||
return zone_id, change_id
|
||||
|
||||
|
||||
def delete_txt_record(change_id, host, value):
|
||||
zone_id, _ = change_id
|
||||
change_txt_record(
|
||||
"DELETE",
|
||||
zone_id,
|
||||
host,
|
||||
value
|
||||
)
|
||||
|
||||
|
||||
@sts_client('route53')
|
||||
def wait_for_change(change_id, client=None):
|
||||
_, change_id = change_id
|
||||
|
||||
while True:
|
||||
response = client.get_change(Id=change_id)
|
||||
if response["ChangeInfo"]["Status"] == "INSYNC":
|
||||
return
|
||||
time.sleep(5)
|
1
lemur/plugins/lemur_acme/tests/conftest.py
Normal file
1
lemur/plugins/lemur_acme/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
4
lemur/plugins/lemur_acme/tests/test_acme.py
Normal file
4
lemur/plugins/lemur_acme/tests/test_acme.py
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
def test_get_certificates(app):
|
||||
from lemur.plugins.base import plugins
|
||||
p = plugins.get('acme-issuer')
|
5
lemur/plugins/lemur_atlas/__init__.py
Normal file
5
lemur/plugins/lemur_atlas/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
107
lemur/plugins/lemur_atlas/plugin.py
Normal file
107
lemur/plugins/lemur_atlas/plugin.py
Normal file
@ -0,0 +1,107 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_atlas.plugin
|
||||
:platform: Unix
|
||||
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
from datetime import datetime
|
||||
|
||||
from flask import current_app
|
||||
from lemur.plugins import lemur_atlas as atlas
|
||||
from lemur.plugins.bases.metric import MetricPlugin
|
||||
|
||||
|
||||
def millis_since_epoch():
|
||||
"""
|
||||
current time since epoch in milliseconds
|
||||
"""
|
||||
epoch = datetime.utcfromtimestamp(0)
|
||||
delta = datetime.now() - epoch
|
||||
return int(delta.total_seconds() * 1000.0)
|
||||
|
||||
|
||||
class AtlasMetricPlugin(MetricPlugin):
|
||||
title = 'Atlas'
|
||||
slug = 'atlas-metric'
|
||||
description = 'Adds support for sending key metrics to Atlas'
|
||||
version = atlas.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'sidecar_host',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'help_message': 'If no host is provided localhost is assumed',
|
||||
'default': 'localhost'
|
||||
},
|
||||
{
|
||||
'name': 'sidecar_port',
|
||||
'type': 'int',
|
||||
'required': False,
|
||||
'default': 8078
|
||||
}
|
||||
]
|
||||
|
||||
metric_data = {}
|
||||
sidecar_host = None
|
||||
sidecar_port = None
|
||||
|
||||
def submit(self, metric_name, metric_type, metric_value, metric_tags=None, options=None):
|
||||
if not options:
|
||||
options = self.options
|
||||
|
||||
# TODO marshmallow schema?
|
||||
valid_types = ['COUNTER', 'GAUGE', 'TIMER']
|
||||
if metric_type.upper() not in valid_types:
|
||||
raise Exception(
|
||||
"Invalid Metric Type for Atlas: '{metric}' choose from: {options}".format(
|
||||
metric=metric_type, options=','.join(valid_types)
|
||||
)
|
||||
)
|
||||
|
||||
if metric_tags:
|
||||
if not isinstance(metric_tags, dict):
|
||||
raise Exception(
|
||||
"Invalid Metric Tags for Atlas: Tags must be in dict format"
|
||||
)
|
||||
|
||||
if metric_value == "NaN" or isinstance(metric_value, int) or isinstance(metric_value, float):
|
||||
self.metric_data['value'] = metric_value
|
||||
else:
|
||||
raise Exception(
|
||||
"Invalid Metric Value for Atlas: Metric must be a number"
|
||||
)
|
||||
|
||||
self.metric_data['type'] = metric_type.upper()
|
||||
self.metric_data['name'] = str(metric_name)
|
||||
self.metric_data['tags'] = metric_tags
|
||||
self.metric_data['timestamp'] = millis_since_epoch()
|
||||
|
||||
self.sidecar_host = self.get_option('sidecar_host', options)
|
||||
self.sidecar_port = self.get_option('sidecar_port', options)
|
||||
|
||||
try:
|
||||
res = requests.post(
|
||||
'http://{host}:{port}/metrics'.format(
|
||||
host=self.sidecar_host,
|
||||
port=self.sidecar_port),
|
||||
data=json.dumps([self.metric_data])
|
||||
)
|
||||
|
||||
if res.status_code != 200:
|
||||
current_app.logger.warning("Failed to publish altas metric. {0}".format(res.content))
|
||||
|
||||
except ConnectionError:
|
||||
current_app.logger.warning(
|
||||
"AtlasMetrics: could not connect to sidecar at {host}:{port}".format(
|
||||
host=self.sidecar_host, port=self.sidecar_port
|
||||
)
|
||||
)
|
23
lemur/plugins/lemur_aws/ec2.py
Normal file
23
lemur/plugins/lemur_aws/ec2.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_aws.elb
|
||||
:synopsis: Module contains some often used and helpful classes that
|
||||
are used to deal with ELBs
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from lemur.plugins.lemur_aws.sts import sts_client
|
||||
|
||||
|
||||
@sts_client('ec2')
|
||||
def get_regions(**kwargs):
|
||||
regions = kwargs['client'].describe_regions()
|
||||
return [x['RegionName'] for x in regions['Regions']]
|
||||
|
||||
|
||||
@sts_client('ec2')
|
||||
def get_all_instances(**kwargs):
|
||||
"""
|
||||
Fetches all instance objects for a given account and region.
|
||||
"""
|
||||
paginator = kwargs['client'].get_paginator('describe_instances')
|
||||
return paginator.paginate()
|
@ -1,16 +1,29 @@
|
||||
"""
|
||||
.. module: elb
|
||||
.. module: lemur.plugins.lemur_aws.elb
|
||||
:synopsis: Module contains some often used and helpful classes that
|
||||
are used to deal with ELBs
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import boto.ec2
|
||||
|
||||
import botocore
|
||||
from flask import current_app
|
||||
|
||||
from retrying import retry
|
||||
|
||||
from lemur.exceptions import InvalidListener
|
||||
from lemur.plugins.lemur_aws.sts import assume_service
|
||||
from lemur.plugins.lemur_aws.sts import sts_client, assume_service
|
||||
|
||||
|
||||
def retry_throttled(exception):
|
||||
"""
|
||||
Determiens if this exception is due to throttling
|
||||
:param exception:
|
||||
:return:
|
||||
"""
|
||||
if isinstance(exception, botocore.exceptions.ClientError):
|
||||
if 'Throttling' in exception.message:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_valid(listener_tuple):
|
||||
@ -28,8 +41,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)
|
||||
if lb_protocol.lower() in ['ssl', 'https']:
|
||||
@ -39,41 +50,57 @@ def is_valid(listener_tuple):
|
||||
return listener_tuple
|
||||
|
||||
|
||||
def get_all_regions():
|
||||
@sts_client('elb')
|
||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
||||
def get_elbs(**kwargs):
|
||||
"""
|
||||
Retrieves all current EC2 regions.
|
||||
Fetches one page elb objects for a given account and region.
|
||||
"""
|
||||
client = kwargs.pop('client')
|
||||
return client.describe_load_balancers(**kwargs)
|
||||
|
||||
|
||||
def get_all_elbs(**kwargs):
|
||||
"""
|
||||
Fetches all elbs for a given account/region
|
||||
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
regions = []
|
||||
for r in boto.ec2.regions():
|
||||
regions.append(r.name)
|
||||
return regions
|
||||
|
||||
|
||||
def get_all_elbs(account_number, region):
|
||||
"""
|
||||
Fetches all elb objects for a given account and region.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
"""
|
||||
marker = None
|
||||
elbs = []
|
||||
return assume_service(account_number, 'elb', region).get_all_load_balancers()
|
||||
# TODO create pull request for boto to include elb marker support
|
||||
# while True:
|
||||
# app.logger.debug(response.__dict__)
|
||||
# raise Exception
|
||||
# result = response['list_server_certificates_response']['list_server_certificates_result']
|
||||
#
|
||||
# for elb in result['server_certificate_metadata_list']:
|
||||
# elbs.append(elb)
|
||||
#
|
||||
# if result['is_truncated'] == 'true':
|
||||
# marker = result['marker']
|
||||
# else:
|
||||
# return elbs
|
||||
|
||||
while True:
|
||||
response = get_elbs(**kwargs)
|
||||
|
||||
elbs += response['LoadBalancerDescriptions']
|
||||
|
||||
if not response.get('IsTruncated'):
|
||||
return elbs
|
||||
|
||||
if response['NextMarker']:
|
||||
kwargs.update(dict(marker=response['NextMarker']))
|
||||
|
||||
|
||||
@sts_client('elb')
|
||||
def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
||||
"""
|
||||
Fetching all policies currently associated with an ELB.
|
||||
|
||||
:param load_balancer_name:
|
||||
:return:
|
||||
"""
|
||||
return kwargs['client'].describe_load_balancer_policies(LoadBalancerName=load_balancer_name, PolicyNames=policy_names)
|
||||
|
||||
|
||||
@sts_client('elb')
|
||||
def describe_load_balancer_types(policies, **kwargs):
|
||||
"""
|
||||
Describe the policies with policy details.
|
||||
|
||||
:param policies:
|
||||
:return:
|
||||
"""
|
||||
return kwargs['client'].describe_load_balancer_policy_types(PolicyTypeNames=policies)
|
||||
|
||||
|
||||
def attach_certificate(account_number, region, name, port, certificate_id):
|
||||
@ -90,67 +117,67 @@ def attach_certificate(account_number, region, name, port, certificate_id):
|
||||
return assume_service(account_number, 'elb', region).set_lb_listener_SSL_certificate(name, port, certificate_id)
|
||||
|
||||
|
||||
def create_new_listeners(account_number, region, name, listeners=None):
|
||||
"""
|
||||
Creates a new listener and attaches it to the ELB.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param listeners:
|
||||
:return:
|
||||
"""
|
||||
listeners = [is_valid(x) for x in listeners]
|
||||
return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
|
||||
|
||||
|
||||
def update_listeners(account_number, region, name, listeners, ports):
|
||||
"""
|
||||
We assume that a listener with a specified port already exists. We can then
|
||||
delete the old listener on the port and create a new one in it's place.
|
||||
|
||||
If however we are replacing a listener e.g. changing a port from 80 to 443 we need
|
||||
to make sure we kept track of which ports we needed to delete so that we don't create
|
||||
two listeners (one 80 and one 443)
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param listeners:
|
||||
:param ports:
|
||||
"""
|
||||
# you cannot update a listeners port/protocol instead we remove the only one and
|
||||
# create a new one in it's place
|
||||
listeners = [is_valid(x) for x in listeners]
|
||||
|
||||
assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
return create_new_listeners(account_number, region, name, listeners=listeners)
|
||||
|
||||
|
||||
def delete_listeners(account_number, region, name, ports):
|
||||
"""
|
||||
Deletes a listener from an ELB.
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:param 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
|
||||
# def create_new_listeners(account_number, region, name, listeners=None):
|
||||
# """
|
||||
# Creates a new listener and attaches it to the ELB.
|
||||
#
|
||||
# :param account_number:
|
||||
# :param region:
|
||||
# :param name:
|
||||
# :param listeners:
|
||||
# :return:
|
||||
# """
|
||||
# listeners = [is_valid(x) for x in listeners]
|
||||
# return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners)
|
||||
#
|
||||
#
|
||||
# def update_listeners(account_number, region, name, listeners, ports):
|
||||
# """
|
||||
# We assume that a listener with a specified port already exists. We can then
|
||||
# delete the old listener on the port and create a new one in it's place.
|
||||
#
|
||||
# If however we are replacing a listener e.g. changing a port from 80 to 443 we need
|
||||
# to make sure we kept track of which ports we needed to delete so that we don't create
|
||||
# two listeners (one 80 and one 443)
|
||||
#
|
||||
# :param account_number:
|
||||
# :param region:
|
||||
# :param name:
|
||||
# :param listeners:
|
||||
# :param ports:
|
||||
# """
|
||||
# # you cannot update a listeners port/protocol instead we remove the only one and
|
||||
# # create a new one in it's place
|
||||
# listeners = [is_valid(x) for x in listeners]
|
||||
#
|
||||
# assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
# return create_new_listeners(account_number, region, name, listeners=listeners)
|
||||
#
|
||||
#
|
||||
# def delete_listeners(account_number, region, name, ports):
|
||||
# """
|
||||
# Deletes a listener from an ELB.
|
||||
#
|
||||
# :param account_number:
|
||||
# :param region:
|
||||
# :param name:
|
||||
# :param 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
|
||||
|
@ -33,15 +33,15 @@ def upload_cert(account_number, name, body, private_key, cert_chain=None):
|
||||
cert_chain=str(cert_chain))
|
||||
|
||||
|
||||
def delete_cert(account_number, cert):
|
||||
def delete_cert(account_number, cert_name):
|
||||
"""
|
||||
Delete a certificate from AWS
|
||||
|
||||
:param account_number:
|
||||
:param cert:
|
||||
:param cert_name:
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'iam').delete_server_cert(cert.name)
|
||||
return assume_service(account_number, 'iam').delete_server_cert(cert_name)
|
||||
|
||||
|
||||
def get_all_server_certs(account_number):
|
||||
|
@ -4,20 +4,44 @@
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
Terraform example to setup the destination bucket:
|
||||
resource "aws_s3_bucket" "certs_log_bucket" {
|
||||
bucket = "certs-log-access-bucket"
|
||||
acl = "log-delivery-write"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "certs_lemur" {
|
||||
bucket = "certs-lemur"
|
||||
acl = "private"
|
||||
|
||||
logging {
|
||||
target_bucket = "${aws_s3_bucket.certs_log_bucket.id}"
|
||||
target_prefix = "log/lemur"
|
||||
}
|
||||
}
|
||||
|
||||
The IAM role Lemur is running as should have the following actions on the destination bucket:
|
||||
|
||||
"S3:PutObject",
|
||||
"S3:PutObjectAcl"
|
||||
|
||||
The reader should have the following actions:
|
||||
"s3:GetObject"
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||
.. moduleauthor:: Harm Weites <harm@weites.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
from boto.exception import BotoServerError
|
||||
|
||||
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
|
||||
from lemur.plugins.lemur_aws import iam, elb
|
||||
from lemur.plugins.lemur_aws.ec2 import get_regions
|
||||
from lemur.plugins.lemur_aws.elb import get_all_elbs, describe_load_balancer_policies, attach_certificate
|
||||
from lemur.plugins.lemur_aws import iam, s3
|
||||
from lemur.plugins import lemur_aws as aws
|
||||
|
||||
|
||||
def find_value(name, options):
|
||||
for o in options:
|
||||
if o['name'] == name:
|
||||
return o['value']
|
||||
|
||||
|
||||
class AWSDestinationPlugin(DestinationPlugin):
|
||||
title = 'AWS'
|
||||
slug = 'aws-destination'
|
||||
@ -36,6 +60,7 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
'helpMessage': 'Must be a valid AWS account number!',
|
||||
}
|
||||
]
|
||||
|
||||
# 'elb': {
|
||||
# 'name': {'type': 'name'},
|
||||
# 'region': {'type': 'str'},
|
||||
@ -43,24 +68,22 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
# }
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
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)
|
||||
try:
|
||||
iam.upload_cert(self.get_option('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'])
|
||||
else:
|
||||
raise Exception("Unable to upload to AWS, private key is required")
|
||||
e = self.get_option('elb', options)
|
||||
if e:
|
||||
attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId'])
|
||||
|
||||
|
||||
class AWSSourcePlugin(SourcePlugin):
|
||||
title = 'AWS'
|
||||
slug = 'aws-source'
|
||||
description = 'Discovers all SSL certificates in an AWS account'
|
||||
description = 'Discovers all SSL certificates and ELB endpoints in an AWS account'
|
||||
version = aws.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
@ -74,18 +97,177 @@ class AWSSourcePlugin(SourcePlugin):
|
||||
'validation': '/^[0-9]{12,12}$/',
|
||||
'helpMessage': 'Must be a valid AWS account number!',
|
||||
},
|
||||
{
|
||||
'name': 'regions',
|
||||
'type': 'str',
|
||||
'helpMessage': 'Comma separated list of regions to search in, if no region is specified we look in all regions.'
|
||||
},
|
||||
]
|
||||
|
||||
def get_certificates(self, options, **kwargs):
|
||||
certs = []
|
||||
arns = iam.get_all_server_certs(find_value('accountNumber', options))
|
||||
arns = iam.get_all_server_certs(self.get_option('accountNumber', options))
|
||||
for arn in arns:
|
||||
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)
|
||||
return certs
|
||||
|
||||
def get_endpoints(self, options, **kwargs):
|
||||
endpoints = []
|
||||
account_number = self.get_option('accountNumber', options)
|
||||
regions = self.get_option('regions', options)
|
||||
|
||||
if not regions:
|
||||
regions = get_regions(account_number=account_number)
|
||||
else:
|
||||
regions = regions.split(',')
|
||||
|
||||
for region in regions:
|
||||
elbs = get_all_elbs(account_number=account_number, region=region)
|
||||
current_app.logger.info("Describing load balancers in {0}-{1}".format(account_number, region))
|
||||
for elb in elbs:
|
||||
for listener in elb['ListenerDescriptions']:
|
||||
if not listener['Listener'].get('SSLCertificateId'):
|
||||
continue
|
||||
|
||||
if listener['Listener']['SSLCertificateId'] == 'Invalid-Certificate':
|
||||
continue
|
||||
|
||||
endpoint = dict(
|
||||
name=elb['LoadBalancerName'],
|
||||
dnsname=elb['DNSName'],
|
||||
type='elb',
|
||||
port=listener['Listener']['LoadBalancerPort'],
|
||||
certificate_name=iam.get_name_from_arn(listener['Listener']['SSLCertificateId'])
|
||||
)
|
||||
|
||||
if listener['PolicyNames']:
|
||||
policy = describe_load_balancer_policies(elb['LoadBalancerName'], listener['PolicyNames'], account_number=account_number, region=region)
|
||||
endpoint['policy'] = format_elb_cipher_policy(policy)
|
||||
|
||||
endpoints.append(endpoint)
|
||||
|
||||
return endpoints
|
||||
|
||||
def clean(self, options, **kwargs):
|
||||
account_number = self.get_option('accountNumber', options)
|
||||
certificates = self.get_certificates(options)
|
||||
endpoints = self.get_endpoints(options)
|
||||
|
||||
orphaned = []
|
||||
for certificate in certificates:
|
||||
for endpoint in endpoints:
|
||||
if certificate['name'] == endpoint['certificate_name']:
|
||||
break
|
||||
else:
|
||||
orphaned.append(certificate['name'])
|
||||
iam.delete_cert(account_number, certificate)
|
||||
|
||||
return orphaned
|
||||
|
||||
|
||||
def format_elb_cipher_policy(policy):
|
||||
"""
|
||||
Attempts to format cipher policy information into a common format.
|
||||
:param policy:
|
||||
:return:
|
||||
"""
|
||||
ciphers = []
|
||||
name = None
|
||||
for descr in policy['PolicyDescriptions']:
|
||||
for attr in descr['PolicyAttributeDescriptions']:
|
||||
if attr['AttributeName'] == 'Reference-Security-Policy':
|
||||
name = attr['AttributeValue']
|
||||
continue
|
||||
|
||||
if attr['AttributeValue'] == 'true':
|
||||
ciphers.append(attr['AttributeName'])
|
||||
|
||||
return dict(name=name, ciphers=ciphers)
|
||||
|
||||
|
||||
class S3DestinationPlugin(DestinationPlugin):
|
||||
title = 'AWS-S3'
|
||||
slug = 'aws-s3'
|
||||
description = 'Allow the uploading of certificates to Amazon S3'
|
||||
|
||||
author = 'Mikhail Khodorovskiy, Harm Weites <harm@weites.com>'
|
||||
author_url = 'https://github.com/Netflix/lemur'
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'bucket',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^$|\s+/',
|
||||
'helpMessage': 'Must be a valid S3 bucket name!',
|
||||
},
|
||||
{
|
||||
'name': 'accountNumber',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'validation': '/^[0-9]{12,12}$/',
|
||||
'helpMessage': 'A valid AWS account number with permission to access S3',
|
||||
},
|
||||
{
|
||||
'name': 'region',
|
||||
'type': 'str',
|
||||
'default': 'eu-west-1',
|
||||
'required': False,
|
||||
'validation': '/^\w+-\w+-\d+$/',
|
||||
'helpMessage': 'Availability zone to use',
|
||||
},
|
||||
{
|
||||
'name': 'encrypt',
|
||||
'type': 'bool',
|
||||
'required': False,
|
||||
'helpMessage': 'Availability zone to use',
|
||||
'default': True
|
||||
},
|
||||
{
|
||||
'name': 'key',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '/^$|\s+/',
|
||||
'helpMessage': 'Must be a valid S3 object key!',
|
||||
},
|
||||
{
|
||||
'name': 'caKey',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '/^$|\s+/',
|
||||
'helpMessage': 'Must be a valid S3 object key!',
|
||||
},
|
||||
{
|
||||
'name': 'certKey',
|
||||
'type': 'str',
|
||||
'required': False,
|
||||
'validation': '/^$|\s+/',
|
||||
'helpMessage': 'Must be a valid S3 object key!',
|
||||
}
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(S3DestinationPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
account_number = self.get_option('accountNumber', options)
|
||||
encrypt = self.get_option('encrypt', options)
|
||||
bucket = self.get_option('bucket', options)
|
||||
key = self.get_option('key', options)
|
||||
ca_key = self.get_option('caKey', options)
|
||||
cert_key = self.get_option('certKey', options)
|
||||
|
||||
if key and ca_key and cert_key:
|
||||
s3.write_to_s3(account_number, bucket, key, private_key, encrypt=encrypt)
|
||||
s3.write_to_s3(account_number, bucket, ca_key, cert_chain, encrypt=encrypt)
|
||||
s3.write_to_s3(account_number, bucket, cert_key, body, encrypt=encrypt)
|
||||
else:
|
||||
pem_body = key + '\n' + body + '\n' + cert_chain + '\n'
|
||||
s3.write_to_s3(account_number, bucket, name, pem_body, encrypt=encrypt)
|
||||
|
26
lemur/plugins/lemur_aws/s3.py
Normal file
26
lemur/plugins/lemur_aws/s3.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_aws.s3
|
||||
:platform: Unix
|
||||
:synopsis: Contains helper functions for interactive with AWS S3 Apis.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from boto.s3.key import Key
|
||||
from lemur.plugins.lemur_aws.sts import assume_service
|
||||
|
||||
|
||||
def write_to_s3(account_number, bucket_name, key, data, encrypt=True):
|
||||
"""
|
||||
Use STS to write to an S3 bucket
|
||||
|
||||
:param account_number:
|
||||
:param bucket_name:
|
||||
:param data:
|
||||
"""
|
||||
conn = assume_service(account_number, 's3')
|
||||
b = conn.get_bucket(bucket_name, validate=False) # validate=False removes need for ListObjects permission
|
||||
|
||||
k = Key(bucket=b, name=key)
|
||||
k.set_contents_from_string(data, encrypt_key=encrypt)
|
||||
k.set_canned_acl("bucket-owner-read")
|
@ -5,13 +5,16 @@
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from functools import wraps
|
||||
|
||||
import boto
|
||||
import boto.ec2.elb
|
||||
import boto3
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def assume_service(account_number, service, region=None):
|
||||
def assume_service(account_number, service, region='us-east-1'):
|
||||
conn = boto.connect_sts()
|
||||
|
||||
role = conn.assume_role('arn:aws:iam::{0}:role/{1}'.format(
|
||||
@ -35,3 +38,47 @@ def assume_service(account_number, service, region=None):
|
||||
aws_access_key_id=role.credentials.access_key,
|
||||
aws_secret_access_key=role.credentials.secret_key,
|
||||
security_token=role.credentials.session_token)
|
||||
|
||||
elif service in 's3':
|
||||
return boto.s3.connect_to_region(
|
||||
region,
|
||||
aws_access_key_id=role.credentials.access_key,
|
||||
aws_secret_access_key=role.credentials.secret_key,
|
||||
security_token=role.credentials.session_token)
|
||||
|
||||
|
||||
def sts_client(service, service_type='client'):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
sts = boto3.client('sts')
|
||||
arn = 'arn:aws:iam::{0}:role/{1}'.format(
|
||||
kwargs.pop('account_number'),
|
||||
current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')
|
||||
)
|
||||
# TODO add user specific information to RoleSessionName
|
||||
role = sts.assume_role(RoleArn=arn, RoleSessionName='lemur')
|
||||
|
||||
if service_type == 'client':
|
||||
client = boto3.client(
|
||||
service,
|
||||
region_name=kwargs.pop('region', 'us-east-1'),
|
||||
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||
aws_session_token=role['Credentials']['SessionToken']
|
||||
)
|
||||
kwargs['client'] = client
|
||||
elif service_type == 'resource':
|
||||
resource = boto3.resource(
|
||||
service,
|
||||
region_name=kwargs.pop('region', 'us-east-1'),
|
||||
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||
aws_session_token=role['Credentials']['SessionToken']
|
||||
)
|
||||
kwargs['resource'] = resource
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
1
lemur/plugins/lemur_aws/tests/conftest.py
Normal file
1
lemur/plugins/lemur_aws/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
14
lemur/plugins/lemur_aws/tests/test_elb.py
Normal file
14
lemur/plugins/lemur_aws/tests/test_elb.py
Normal file
@ -0,0 +1,14 @@
|
||||
import boto
|
||||
from moto import mock_sts, mock_elb
|
||||
|
||||
|
||||
@mock_sts()
|
||||
@mock_elb()
|
||||
def test_get_all_elbs(app):
|
||||
from lemur.plugins.lemur_aws.elb import get_all_elbs
|
||||
conn = boto.ec2.elb.connect_to_region('us-east-1')
|
||||
elbs = get_all_elbs(account_number='123456789012', region='us-east-1')
|
||||
assert not elbs
|
||||
conn.create_load_balancer('example-lb', ['us-east-1a', 'us-east-1b'], [(443, 5443, 'tcp')])
|
||||
elbs = get_all_elbs(account_number='123456789012', region='us-east-1')
|
||||
assert elbs
|
@ -1,8 +1,7 @@
|
||||
import pytest
|
||||
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():
|
||||
@ -11,12 +10,12 @@ def test_get_name_from_arn():
|
||||
assert get_name_from_arn(arn) == 'tttt2.netflixtest.net-NetflixInc-20150624-20150625'
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason="this fails because moto is not currently returning what boto does")
|
||||
@mock_sts()
|
||||
@mock_iam()
|
||||
def test_get_all_server_certs(app):
|
||||
from lemur.plugins.lemur_aws.iam import upload_cert, get_all_server_certs
|
||||
cert = Certificate(EXTERNAL_VALID_STR)
|
||||
upload_cert('123456789012', cert, PRIVATE_KEY_STR)
|
||||
upload_cert('123456789012', 'testCert', EXTERNAL_VALID_STR.decode('utf-8'), PRIVATE_KEY_STR.decode('utf-8'))
|
||||
certs = get_all_server_certs('123456789012')
|
||||
assert len(certs) == 1
|
||||
|
||||
@ -25,7 +24,6 @@ def test_get_all_server_certs(app):
|
||||
@mock_iam()
|
||||
def test_get_cert_from_arn(app):
|
||||
from lemur.plugins.lemur_aws.iam import upload_cert, get_cert_from_arn
|
||||
cert = Certificate(EXTERNAL_VALID_STR)
|
||||
upload_cert('123456789012', cert, PRIVATE_KEY_STR)
|
||||
body, chain = get_cert_from_arn('arn:aws:iam::123456789012:server-certificate/tttt2.netflixtest.net-NetflixInc-20150624-20150625')
|
||||
assert body.replace('\n', '') == EXTERNAL_VALID_STR.replace('\n', '')
|
||||
upload_cert('123456789012', 'testCert', EXTERNAL_VALID_STR.decode('utf-8'), PRIVATE_KEY_STR.decode('utf-8'))
|
||||
body, chain = get_cert_from_arn('arn:aws:iam::123456789012:server-certificate/testCert')
|
||||
assert body.replace('\n', '') == EXTERNAL_VALID_STR.decode('utf-8').replace('\n', '')
|
||||
|
6
lemur/plugins/lemur_aws/tests/test_plugin.py
Normal file
6
lemur/plugins/lemur_aws/tests/test_plugin.py
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
def test_get_certificates(app):
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
p = plugins.get('aws-s3')
|
||||
assert p
|
5
lemur/plugins/lemur_cfssl/__init__.py
Normal file
5
lemur/plugins/lemur_cfssl/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
64
lemur/plugins/lemur_cfssl/plugin.py
Normal file
64
lemur/plugins/lemur_cfssl/plugin.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_cfssl.plugin
|
||||
:platform: Unix
|
||||
:synopsis: This module is responsible for communicating with the CFSSL private CA.
|
||||
:copyright: (c) 2016 by Thomson Reuters
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Charles Hendrie <chad.hendrie@tr.com>
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.plugins.bases import IssuerPlugin
|
||||
from lemur.plugins import lemur_cfssl as cfssl
|
||||
|
||||
|
||||
class CfsslIssuerPlugin(IssuerPlugin):
|
||||
title = 'CFSSL'
|
||||
slug = 'cfssl-issuer'
|
||||
description = 'Enables the creation of certificates by CFSSL private CA'
|
||||
version = cfssl.VERSION
|
||||
|
||||
author = 'Charles Hendrie'
|
||||
author_url = 'https://github.com/netflix/lemur.git'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = requests.Session()
|
||||
super(CfsslIssuerPlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
"""
|
||||
Creates a CFSSL certificate.
|
||||
|
||||
:param csr:
|
||||
:param issuer_options:
|
||||
:return:
|
||||
"""
|
||||
current_app.logger.info("Requesting a new cfssl certificate with csr: {0}".format(csr))
|
||||
|
||||
url = "{0}{1}".format(current_app.config.get('CFSSL_URL'), '/api/v1/cfssl/sign')
|
||||
|
||||
data = {'certificate_request': csr.decode('utf_8')}
|
||||
data = json.dumps(data)
|
||||
|
||||
response = self.session.post(url, data=data.encode(encoding='utf_8', errors='strict'))
|
||||
response_json = json.loads(response.content.decode('utf_8'))
|
||||
cert = response_json['result']['certificate']
|
||||
|
||||
return cert, current_app.config.get('CFSSL_INTERMEDIATE'),
|
||||
|
||||
@staticmethod
|
||||
def create_authority(options):
|
||||
"""
|
||||
Creates an authority, this authority is then used by Lemur to allow a user
|
||||
to specify which Certificate Authority they want to sign their certificate.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
role = {'username': '', 'password': '', 'name': 'cfssl'}
|
||||
return current_app.config.get('CFSSL_ROOT'), "", [role]
|
1
lemur/plugins/lemur_cfssl/tests/conftest.py
Normal file
1
lemur/plugins/lemur_cfssl/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
6
lemur/plugins/lemur_cfssl/tests/test_cfssl.py
Normal file
6
lemur/plugins/lemur_cfssl/tests/test_cfssl.py
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
def test_get_certificates(app):
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
p = plugins.get('cfssl-issuer')
|
||||
assert p
|
5
lemur/plugins/lemur_cryptography/__init__.py
Normal file
5
lemur/plugins/lemur_cryptography/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
133
lemur/plugins/lemur_cryptography/plugin.py
Normal file
133
lemur/plugins/lemur_cryptography/plugin.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_cryptography.plugin
|
||||
: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 uuid
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
|
||||
from lemur.plugins.bases import IssuerPlugin
|
||||
from lemur.plugins import lemur_cryptography as cryptography_issuer
|
||||
|
||||
|
||||
def build_root_certificate(options):
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
subject = issuer = x509.Name([
|
||||
x509.NameAttribute(x509.OID_COUNTRY_NAME, options['country']),
|
||||
x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, options['state']),
|
||||
x509.NameAttribute(x509.OID_LOCALITY_NAME, options['location']),
|
||||
x509.NameAttribute(x509.OID_ORGANIZATION_NAME, options['organization']),
|
||||
x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, options['organizational_unit']),
|
||||
x509.NameAttribute(x509.OID_COMMON_NAME, options['common_name'])
|
||||
])
|
||||
|
||||
builder = x509.CertificateBuilder(
|
||||
subject_name=subject,
|
||||
issuer_name=issuer,
|
||||
public_key=private_key.public_key(),
|
||||
not_valid_after=options['validity_end'],
|
||||
not_valid_before=options['validity_start'],
|
||||
serial_number=options['first_serial']
|
||||
)
|
||||
|
||||
builder.add_extension(x509.SubjectAlternativeName([x509.DNSName(options['common_name'])]), critical=False)
|
||||
|
||||
cert = builder.sign(private_key, hashes.SHA256(), default_backend())
|
||||
|
||||
cert_pem = cert.public_bytes(
|
||||
encoding=serialization.Encoding.PEM
|
||||
).decode('utf-8')
|
||||
|
||||
private_key_pem = 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()
|
||||
)
|
||||
|
||||
return cert_pem, private_key_pem
|
||||
|
||||
|
||||
def issue_certificate(csr, options):
|
||||
csr = x509.load_pem_x509_csr(csr, default_backend())
|
||||
|
||||
builder = x509.CertificateBuilder(
|
||||
issuer_name=x509.Name([
|
||||
x509.NameAttribute(
|
||||
x509.OID_ORGANIZATION_NAME,
|
||||
options['authority'].authority_certificate.issuer
|
||||
)]
|
||||
),
|
||||
subject_name=csr.subject,
|
||||
public_key=csr.public_key(),
|
||||
not_valid_before=options['validity_start'],
|
||||
not_valid_after=options['validity_end'],
|
||||
extensions=csr.extensions)
|
||||
|
||||
# TODO figure out a better way to increment serial
|
||||
builder = builder.serial_number(int(uuid.uuid4()))
|
||||
|
||||
private_key = serialization.load_pem_private_key(
|
||||
bytes(str(options['authority'].authority_certificate.private_key).encode('utf-8')),
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
cert = builder.sign(private_key, hashes.SHA256(), default_backend())
|
||||
|
||||
return cert.public_bytes(
|
||||
encoding=serialization.Encoding.PEM
|
||||
).decode('utf-8')
|
||||
|
||||
|
||||
class CryptographyIssuerPlugin(IssuerPlugin):
|
||||
title = 'Cryptography'
|
||||
slug = 'cryptography-issuer'
|
||||
description = 'Enables the creation and signing of self-signed certificates'
|
||||
version = cryptography_issuer.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur.git'
|
||||
|
||||
def create_certificate(self, csr, options):
|
||||
"""
|
||||
Creates a certificate.
|
||||
|
||||
:param csr:
|
||||
:param options:
|
||||
:return: :raise Exception:
|
||||
"""
|
||||
current_app.logger.debug("Issuing new cryptography certificate with options: {0}".format(options))
|
||||
cert = issue_certificate(csr, options)
|
||||
return cert, ""
|
||||
|
||||
@staticmethod
|
||||
def create_authority(options):
|
||||
"""
|
||||
Creates an authority, this authority is then used by Lemur to allow a user
|
||||
to specify which Certificate Authority they want to sign their certificate.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
current_app.logger.debug("Issuing new cryptography authority with options: {0}".format(options))
|
||||
cert, private_key = build_root_certificate(options)
|
||||
roles = [
|
||||
{'username': '', 'password': '', 'name': options['name'] + '_admin'},
|
||||
{'username': '', 'password': '', 'name': options['name'] + '_operator'}
|
||||
]
|
||||
return cert, private_key, "", roles
|
1
lemur/plugins/lemur_cryptography/tests/conftest.py
Normal file
1
lemur/plugins/lemur_cryptography/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
5
lemur/plugins/lemur_digicert/__init__.py
Normal file
5
lemur/plugins/lemur_digicert/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user