Compare commits
249 Commits
Author | SHA1 | Date | |
---|---|---|---|
c0c6ff51e2 | |||
4384cbb953 | |||
3397fb6560 | |||
b231521ff6 | |||
3efc709e03 | |||
dda7f54a16 | |||
2d33d3e2b8 | |||
d50c9c7748 | |||
665a0bcffe | |||
a141b8c5ea | |||
9d710702a4 | |||
e835fa6073 | |||
c0d037b9e9 | |||
b2bc431823 | |||
9c5140006b | |||
4e72cb96c9 | |||
f88e81ffef | |||
17861289c8 | |||
b99aad743b | |||
b1ce4d630d | |||
135f2b710c | |||
e8c18bd9b6 | |||
76621e497f | |||
065e0edc5f | |||
d72792ff37 | |||
f9239b008e | |||
b31c7357ed | |||
7c6d6f5297 | |||
e225139011 | |||
37edf80321 | |||
038f5dc554 | |||
5e964fad39 | |||
3800d67d71 | |||
7f5d1a0b6b | |||
92860cffca | |||
2aced5c010 | |||
4e1879715d | |||
403f70d6db | |||
b33256c809 | |||
e39fac32ec | |||
80e3331596 | |||
2a3af5214e | |||
4911d713a5 | |||
5e24f685c1 | |||
97d3621705 | |||
544a02ca3f | |||
ae26e44cc2 | |||
b0f9d33b32 | |||
c5e7e5ab68 | |||
5e3add0b81 | |||
9ccd43c29b | |||
9fc6c9aaf7 | |||
2d61200a05 | |||
268d826158 | |||
a47b6c330d | |||
de52fa7f48 | |||
680f4966a1 | |||
a9b9b27a0b | |||
52e7ff9919 | |||
f4a010e505 | |||
0bd14488bb | |||
6500559f8e | |||
642dbd4098 | |||
a8187d15c6 | |||
df5168765b | |||
c26ae16060 | |||
9ccb8fb838 | |||
e68b3d2cbd | |||
1be3f8368f | |||
3e64dd4653 | |||
48dde287d8 | |||
4da2f33892 | |||
74ca13861c | |||
532872b3c6 | |||
d37f730ee8 | |||
5e744c4c52 | |||
858c4eb808 | |||
3ffeb8ab00 | |||
0579b2935c | |||
c5cb01bd33 | |||
efd5836e43 | |||
f0f2092fb4 | |||
e09b7eb978 | |||
3e5db9eedb | |||
91500d1022 | |||
51d2990eb9 | |||
38b8df4a07 | |||
211027919f | |||
38c33395c8 | |||
7704f51441 | |||
ae63808678 | |||
81e349e07d | |||
49cdf1c7cf | |||
7e36b0e8fd | |||
552c07e932 | |||
44e3b33aaa | |||
a8ce219016 | |||
b9e93065f7 | |||
78f9ceb995 | |||
1904a187e0 | |||
0320a9aece | |||
4e94e51218 | |||
4392657a71 | |||
fbce1ef7c7 | |||
309d10c4e2 | |||
f43100a247 | |||
4d05a09a20 | |||
3538f1a629 | |||
993958c356 | |||
2d6d2357b5 | |||
a66d85b63d | |||
b0bd0435c4 | |||
b2e6938815 | |||
d66dd543bf | |||
de7a5a30d1 | |||
40c35dc77b | |||
5dd03098e5 | |||
672a28bb28 | |||
8ea2f5253a | |||
1e0146a453 | |||
c03133622f | |||
8303cfbd2b | |||
3ef550f738 | |||
c8767e23bf | |||
f302408712 | |||
c88c0b0127 | |||
acb1eab24e | |||
6cd2205f1f | |||
f6fd262618 | |||
5125990c4c | |||
52cb145333 | |||
c6bd93fe85 | |||
6a762d463f | |||
5beb319b27 | |||
12622d5847 | |||
a9baaf4da4 | |||
f61098b874 | |||
8ca4f730e8 | |||
0b5f85469c | |||
8e2b2123f1 | |||
b4b9a913b3 | |||
2dc6478c34 | |||
28614b5793 | |||
4a0103a88d | |||
fb494bc32a | |||
de9c00b293 | |||
3e5cbb40ce | |||
47793635b2 | |||
259800ce35 | |||
a38f286fb9 | |||
b6ffbfa40e | |||
b814a4f009 | |||
4ed6b7727a | |||
c3a2781507 | |||
ffba1d2b85 | |||
248409e43f | |||
a316cbba73 | |||
12135c445b | |||
844202f36b | |||
9b4a124c08 | |||
ab1b31604c | |||
a8b18480aa | |||
b5e4df5c16 | |||
2dbcc7a297 | |||
1730b3bacc | |||
d730ffbc72 | |||
d36fececd6 | |||
0caafea777 | |||
c847339b0e | |||
58bb08b604 | |||
98d303c6c0 | |||
9514edafba | |||
adb9149413 | |||
c51fed5307 | |||
db746f1296 | |||
62046aed59 | |||
5e0e8804c0 | |||
416791d4c5 | |||
5ee11ed4e0 | |||
3b2ef95798 | |||
827e4c65a7 | |||
fef89feb62 | |||
42f92306a5 | |||
44b8fd6ef5 | |||
5a86ebe318 | |||
1e3df62993 | |||
662eaf4933 | |||
3fd82e51bd | |||
154e38b42e | |||
915cdeb426 | |||
e15836e9ca | |||
8e1eae9a45 | |||
d67542d7f5 | |||
a202d082e8 | |||
4087f1c03b | |||
bbacb7e210 | |||
19cf8f6bdd | |||
f05d1750ee | |||
fa696b56c2 | |||
3f52cd9c2b | |||
d44a1934fe | |||
08f66df860 | |||
48d9a3ec8a | |||
de0b4ddc99 | |||
6e1bb0c49c | |||
d4597b6bb6 | |||
52f5930744 | |||
9504ad3b80 | |||
d2c7f8a963 | |||
ff05deaa1f | |||
b233f567ce | |||
b30d2c9536 | |||
5dd37ea696 | |||
49393070e0 | |||
fdb6dd4077 | |||
74a516cde0 | |||
58da68d72f | |||
918250ce78 | |||
c7ca3949f6 | |||
bbf5e95186 | |||
462e757f92 | |||
58798f1513 | |||
087490e26a | |||
c08d3dd82f | |||
430cb5ea1b | |||
9b1c279fd5 | |||
17be8b626d | |||
412757b178 | |||
18c64fafe4 | |||
77a1600c13 | |||
59ce586ea4 | |||
5fe28f6503 | |||
1f641c0ba6 | |||
cca3797669 | |||
c9cb5800ec | |||
a28fdac242 | |||
0724fcffeb | |||
7032abf2e7 | |||
9e8fa5827d | |||
5d18838868 | |||
2578970f7d | |||
f44fe81573 | |||
aa5d97f49b | |||
f262c93912 | |||
763c5e8356 | |||
050295ea20 | |||
77044f56fc | |||
eea413a90f | |||
8cad2f9f56 |
@ -1,6 +1,37 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
|
||||
0.7 - `2018-05-07`
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This release adds LetsEncrypt support with DNS providers Dyn, Route53, and Cloudflare, and expands on the pending certificate functionality.
|
||||
The linux_dst plugin will also be deprecated and removed.
|
||||
|
||||
The pending_dns_authorizations and dns_providers tables were created. New columns
|
||||
were added to the certificates and pending_certificates tables, (For the DNS provider ID), and authorities (For options).
|
||||
Please run a database migration when upgrading.
|
||||
|
||||
The Let's Encrypt flow will run asynchronously. When a certificate is requested through the acme-issuer, a pending certificate
|
||||
will be created. A cron needs to be defined to run `lemur pending_certs fetch_all_acme`. This command will iterate through all of the pending
|
||||
certificates, request a DNS challenge token from Let's Encrypt, and set the appropriate _acme-challenge TXT entry. It will
|
||||
then iterate through and resolve the challenges before requesting a certificate for each pending certificate. If a certificate
|
||||
is successfully obtained, the pending_certificate will be moved to the certificates table with the appropriate properties.
|
||||
|
||||
Special thanks to all who helped with this release, notably:
|
||||
|
||||
- The folks at Cloudflare
|
||||
- dmitryzykov
|
||||
- jchuong
|
||||
- seils
|
||||
- titouanc
|
||||
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
.. note:: This release will need a migration change. Please follow the `documentation <https://lemur.readthedocs.io/en/latest/administration.html#upgrading-lemur>`_ to upgrade Lemur.
|
||||
|
||||
0.6 - `2018-01-02`
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
2
LICENSE
2
LICENSE
@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2014 Netflix, Inc.
|
||||
Copyright 2018 Netflix, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
22
Makefile
22
Makefile
@ -1,6 +1,6 @@
|
||||
NPM_ROOT = ./node_modules
|
||||
STATIC_DIR = src/lemur/static/app
|
||||
|
||||
SHELL=/bin/bash
|
||||
USER := $(shell whoami)
|
||||
|
||||
develop: update-submodules setup-git
|
||||
@ -104,4 +104,24 @@ coverage: develop
|
||||
publish:
|
||||
python setup.py sdist bdist_wheel upload
|
||||
|
||||
up-reqs:
|
||||
ifndef VIRTUAL_ENV
|
||||
$(error Please activate virtualenv first)
|
||||
endif
|
||||
@echo "--> Updating Python requirements"
|
||||
pip install --upgrade pip
|
||||
pip install --upgrade pip-tools
|
||||
pip-compile --output-file requirements-docs.txt requirements-docs.in -U --no-index
|
||||
pip-compile --output-file requirements-dev.txt requirements-dev.in -U --no-index
|
||||
pip-compile --output-file requirements-tests.txt requirements-tests.in -U --no-index
|
||||
pip-compile --output-file requirements.txt requirements.in -U --no-index
|
||||
@echo "--> Done updating Python requirements"
|
||||
@echo "--> Removing python-ldap from requirements-docs.txt"
|
||||
grep -v "python-ldap" requirements-docs.txt > tempreqs && mv tempreqs requirements-docs.txt
|
||||
@echo "--> Installing new dependencies"
|
||||
pip install -e .
|
||||
@echo "--> Done installing new dependencies"
|
||||
@echo ""
|
||||
|
||||
|
||||
.PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish release
|
||||
|
@ -5,7 +5,7 @@ Lemur
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
.. image:: https://readthedocs.io/projects/lemur/badge/?version=latest
|
||||
.. image:: https://readthedocs.org/projects/lemur/badge/?version=latest
|
||||
:target: https://lemur.readthedocs.io
|
||||
:alt: Latest Docs
|
||||
|
||||
@ -14,6 +14,10 @@ Lemur
|
||||
.. image:: https://travis-ci.org/Netflix/lemur.svg
|
||||
:target: https://travis-ci.org/Netflix/lemur
|
||||
|
||||
.. image:: https://coveralls.io/repos/github/Netflix/lemur/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/Netflix/lemur?branch=master
|
||||
|
||||
|
||||
|
||||
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
|
||||
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.
|
||||
|
@ -10,6 +10,7 @@ services:
|
||||
command: make test
|
||||
environment:
|
||||
SQLALCHEMY_DATABASE_URI: postgresql://lemur:lemur@postgres:5432/lemur
|
||||
VIRTUAL_ENV: 'true'
|
||||
|
||||
postgres:
|
||||
image: postgres:9.4
|
||||
|
@ -65,6 +65,36 @@ Basic Configuration
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://<user>:<password>@<hostname>:5432/lemur'
|
||||
|
||||
|
||||
.. data:: SQLALCHEMY_POOL_SIZE
|
||||
:noindex:
|
||||
|
||||
The default connection pool size is 5 for sqlalchemy managed connections. Depending on the number of Lemur instances,
|
||||
please specify per instance connection pool size. Below is an example to set connection pool size to 10.
|
||||
|
||||
::
|
||||
|
||||
SQLALCHEMY_POOL_SIZE = 10
|
||||
|
||||
|
||||
.. warning::
|
||||
This is an optional setting but important to review and set for optimal database connection usage and for overall database performance.
|
||||
|
||||
.. data:: SQLALCHEMY_MAX_OVERFLOW
|
||||
:noindex:
|
||||
|
||||
This setting allows to create connections in addition to specified number of connections in pool size. By default, sqlalchemy
|
||||
allows 10 connections to create in addition to the pool size. This is also an optional setting. If `SQLALCHEMY_POOL_SIZE` and
|
||||
`SQLALCHEMY_MAX_OVERFLOW` are not speficied then each Lemur instance may create maximum of 15 connections.
|
||||
|
||||
::
|
||||
|
||||
SQLALCHECK_MAX_OVERFLOW = 0
|
||||
|
||||
|
||||
.. note::
|
||||
Specifying the `SQLALCHEMY_MAX_OVERFLOW` to 0 will enforce limit to not create connections above specified pool size.
|
||||
|
||||
|
||||
.. data:: LEMUR_ALLOW_WEEKEND_EXPIRATION
|
||||
:noindex:
|
||||
|
||||
@ -518,6 +548,21 @@ For more information about how to use social logins, see: `Satellizer <https://g
|
||||
GOOGLE_SECRET = "somethingsecret"
|
||||
|
||||
|
||||
Metric Providers
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
If you are not using a metric provider you do not need to configure any of these options.
|
||||
|
||||
.. data:: ACTIVE_PROVIDERS
|
||||
:noindex:
|
||||
|
||||
A list of metric plugins slugs to be ativated.
|
||||
|
||||
::
|
||||
|
||||
METRIC_PROVIDERS = ['atlas-metric']
|
||||
|
||||
|
||||
Plugin Specific Options
|
||||
-----------------------
|
||||
|
||||
@ -581,6 +626,12 @@ The following configuration properties are required to use the Digicert issuer p
|
||||
This is the url for the Digicert API (e.g. https://www.digicert.com)
|
||||
|
||||
|
||||
.. data:: DIGICERT_ORDER_TYPE
|
||||
:noindex:
|
||||
|
||||
This is the type of certificate to order. (e.g. ssl_plus, ssl_ev_plus see: https://www.digicert.com/services/v2/documentation/order/overview-submit)
|
||||
|
||||
|
||||
.. data:: DIGICERT_API_KEY
|
||||
:noindex:
|
||||
|
||||
@ -1135,6 +1186,31 @@ Digicert
|
||||
https://github.com/opendns/lemur-digicert
|
||||
|
||||
|
||||
InfluxDB
|
||||
--------
|
||||
|
||||
:Authors:
|
||||
Titouan Christophe
|
||||
:Type:
|
||||
Metric
|
||||
:Description:
|
||||
Sends key metrics to InfluxDB
|
||||
:Links:
|
||||
https://github.com/titouanc/lemur-influxdb
|
||||
|
||||
Hashicorp Vault
|
||||
---------------
|
||||
|
||||
:Authors:
|
||||
Ron Cohen
|
||||
:Type:
|
||||
Issuer
|
||||
:Description:
|
||||
Adds support for basic Vault PKI secret backend.
|
||||
:Links:
|
||||
https://github.com/RcRonco/lemur_vault
|
||||
|
||||
|
||||
Have an extension that should be listed here? Submit a `pull request <https://github.com/netflix/lemur>`_ and we'll
|
||||
get it added.
|
||||
|
||||
|
@ -59,7 +59,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'lemur'
|
||||
copyright = u'2015, Netflix Inc.'
|
||||
copyright = u'2018, Netflix Inc.'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
@ -48,7 +48,7 @@ of Lemur. You'll want to make sure you have a few things on your local system fi
|
||||
* pip
|
||||
* virtualenv (ideally virtualenvwrapper)
|
||||
* node.js (for npm and building css/javascript)
|
||||
* (Optional) PostgreSQL
|
||||
+* `PostgreSQL <https://lemur.readthedocs.io/en/latest/quickstart/index.html#setup-postgres>`_
|
||||
|
||||
Once you've got all that, the rest is simple:
|
||||
|
||||
@ -77,6 +77,7 @@ Create a default Lemur configuration just as if this were a production instance:
|
||||
|
||||
::
|
||||
|
||||
lemur create_config
|
||||
lemur init
|
||||
|
||||
You'll likely want to make some changes to the default configuration (we recommend developing against Postgres, for example). Once done, migrate your database using the following command:
|
||||
|
@ -110,6 +110,5 @@ Subpackages
|
||||
lemur.plugins.lemur_digicert
|
||||
lemur.plugins.lemur_java
|
||||
lemur.plugins.lemur_kubernetes
|
||||
lemur.plugins.lemur_linuxdst
|
||||
lemur.plugins.lemur_openssl
|
||||
lemur.plugins.lemur_slack
|
||||
|
@ -100,10 +100,16 @@ If you have a third party or internal service that creates authorities (EJBCA, e
|
||||
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
||||
|
||||
|
||||
The `IssuerPlugin` exposes two functions::
|
||||
The `IssuerPlugin` exposes four functions functions::
|
||||
|
||||
def create_certificate(self, csr, issuer_options):
|
||||
# requests.get('a third party')
|
||||
def revoke_certificate(self, certificate, comments):
|
||||
# requests.put('a third party')
|
||||
def get_ordered_certificate(self, order_id):
|
||||
# requests.get('already existing certificate')
|
||||
def canceled_ordered_certificate(self, pending_cert, **kwargs):
|
||||
# requests.put('cancel an order that has yet to be issued')
|
||||
|
||||
Lemur will pass a dictionary of all possible options for certificate creation. Including a valid CSR, and the raw options associated with the request.
|
||||
|
||||
@ -139,6 +145,19 @@ The `IssuerPlugin` doesn't have any options like Destination, Source, and Notifi
|
||||
any fields you might need to submit a request to a third party. If there are additional options you need
|
||||
in your plugin feel free to open an issue, or look into adding additional options to issuers yourself.
|
||||
|
||||
Asynchronous Certificates
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
An issuer may take some time to actually issue a certificate for an order. In this case, a `PendingCertificate` is returned, which holds information to recreate a `Certificate` object at a later time. Then, `get_ordered_certificate()` should be run periodically via `python manage.py pending_certs fetch -i all` to attempt to retrieve an ordered certificate::
|
||||
|
||||
def get_ordered_ceriticate(self, order_id):
|
||||
# order_id is the external id of the order, not the external_id of the certificate
|
||||
# retrieve an order, and check if there is an issued certificate attached to it
|
||||
|
||||
`cancel_ordered_certificate()` should be implemented to allow an ordered certificate to be canceled before it is issued::
|
||||
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||
# pending_cert should contain the necessary information to match an order
|
||||
# kwargs can be given to provide information to the issuer for canceling
|
||||
|
||||
Destination
|
||||
-----------
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
Flask==0.12
|
||||
Flask-RESTful==0.3.6
|
||||
Flask-SQLAlchemy==2.1
|
||||
Flask-Script==2.0.5
|
||||
Flask-Migrate==2.1.1
|
||||
Flask-Bcrypt==0.7.1
|
||||
Flask-Principal==0.4.0
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy-Utils==0.32.14
|
||||
requests==2.11.1
|
||||
ndg-httpsclient==0.4.2
|
||||
psycopg2==2.7.3
|
||||
arrow==0.10.0
|
||||
six==1.10.0
|
||||
marshmallow-sqlalchemy==0.13.1
|
||||
gunicorn==19.7.1
|
||||
marshmallow==2.13.6
|
||||
cryptography==1.9
|
||||
xmltodict==0.11.0
|
||||
pyjwt==1.5.2
|
||||
lockfile==0.12.2
|
||||
inflection==0.3.1
|
||||
future==0.16.0
|
||||
boto3==1.4.6
|
||||
acme==0.18.1
|
||||
retrying==1.3.3
|
||||
tabulate==0.7.7
|
||||
pem==17.1.0
|
||||
raven[flask]==6.1.0
|
||||
jinja2==2.9.6
|
||||
# pyldap==2.4.37 # cannot be installed on rtd - required by ldap auth provider
|
||||
paramiko==2.2.1 # required for lemur_linuxdst plugin
|
||||
sphinx
|
||||
sphinxcontrib-httpdomain
|
||||
sphinx-rtd-theme
|
@ -9,10 +9,10 @@ __title__ = "lemur"
|
||||
__summary__ = ("Certificate management and orchestration service")
|
||||
__uri__ = "https://github.com/Netflix/lemur"
|
||||
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.7.0"
|
||||
|
||||
__author__ = "The Lemur developers"
|
||||
__email__ = "security@netflix.com"
|
||||
|
||||
__license__ = "Apache License, Version 2.0"
|
||||
__copyright__ = "Copyright 2017 {0}".format(__author__)
|
||||
__copyright__ = "Copyright 2018 {0}".format(__author__)
|
||||
|
@ -1,14 +1,15 @@
|
||||
"""
|
||||
.. module: lemur
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import time
|
||||
from flask import g, request
|
||||
|
||||
from lemur import factory
|
||||
from lemur.extensions import metrics
|
||||
@ -27,6 +28,8 @@ from lemur.sources.views import mod as sources_bp
|
||||
from lemur.endpoints.views import mod as endpoints_bp
|
||||
from lemur.logs.views import mod as logs_bp
|
||||
from lemur.api_keys.views import mod as api_key_bp
|
||||
from lemur.pending_certificates.views import mod as pending_certificates_bp
|
||||
from lemur.dns_providers.views import mod as dns_providers_bp
|
||||
|
||||
from lemur.__about__ import (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
@ -53,7 +56,9 @@ LEMUR_BLUEPRINTS = (
|
||||
sources_bp,
|
||||
endpoints_bp,
|
||||
logs_bp,
|
||||
api_key_bp
|
||||
api_key_bp,
|
||||
pending_certificates_bp,
|
||||
dns_providers_bp,
|
||||
)
|
||||
|
||||
|
||||
@ -71,17 +76,6 @@ def configure_hook(app):
|
||||
"""
|
||||
from flask import jsonify
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from lemur.decorators import crossdomain
|
||||
if app.config.get('CORS'):
|
||||
@app.after_request
|
||||
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
def after(response):
|
||||
return response
|
||||
|
||||
@app.after_request
|
||||
def log_status(response):
|
||||
metrics.send('status_code_{}'.format(response.status_code), 'counter', 1)
|
||||
return response
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_error(e):
|
||||
@ -91,3 +85,29 @@ def configure_hook(app):
|
||||
|
||||
app.logger.exception(e)
|
||||
return jsonify(error=str(e)), code
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.request_start_time = time.time()
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
# Return early if we don't have the start time
|
||||
if not hasattr(g, 'request_start_time'):
|
||||
return response
|
||||
|
||||
# Get elapsed time in milliseconds
|
||||
elapsed = time.time() - g.request_start_time
|
||||
elapsed = int(round(1000 * elapsed))
|
||||
|
||||
# Collect request/response tags
|
||||
tags = {
|
||||
'endpoint': request.endpoint,
|
||||
'request_method': request.method.lower(),
|
||||
'status_code': response.status_code
|
||||
}
|
||||
|
||||
# Record our response time metric
|
||||
metrics.send('response_time', 'TIMER', elapsed, metric_tags=tags)
|
||||
metrics.send('status_code_{}'.format(response.status_code), 'counter', 1)
|
||||
return response
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.api_keys.cli
|
||||
:platform: Unix
|
||||
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.api_keys.models
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the models need to create an api key within Lemur.
|
||||
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.api_keys.schemas
|
||||
:platform: Unix
|
||||
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.api_keys.service
|
||||
:platform: Unix
|
||||
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.api_keys.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2017 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Eric Coan <kungfury@instructure.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.auth.ldap
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Ian Stahnke <ian.stahnke@myob.com>
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.auth.permissions
|
||||
:platform: Unix
|
||||
:synopsis: This module defines all the permission used within Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the authentication duties for
|
||||
lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.auth.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -14,6 +14,7 @@ from flask import Blueprint, current_app
|
||||
from flask_restful import reqparse, Resource, Api
|
||||
from flask_principal import Identity, identity_changed
|
||||
|
||||
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||
from lemur.extensions import metrics
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
@ -116,7 +117,6 @@ def retrieve_user(user_api_url, access_token):
|
||||
profile = r.json()
|
||||
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
return user, profile
|
||||
|
||||
|
||||
@ -267,7 +267,7 @@ class Login(Resource):
|
||||
identity_changed.send(current_app._get_current_object(),
|
||||
identity=Identity(user.id))
|
||||
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
|
||||
return dict(token=create_token(user))
|
||||
|
||||
# try ldap login
|
||||
@ -279,16 +279,16 @@ class Login(Resource):
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(),
|
||||
identity=Identity(user.id))
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
|
||||
return dict(token=create_token(user))
|
||||
except Exception as e:
|
||||
current_app.logger.error("ldap error: {0}".format(e))
|
||||
ldap_message = 'ldap error: %s' % e
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
return dict(message=ldap_message), 403
|
||||
|
||||
# if not valid user - no certificates for you
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
return dict(message='The supplied credentials are invalid'), 403
|
||||
|
||||
|
||||
@ -337,14 +337,14 @@ class Ping(Resource):
|
||||
roles = create_user_roles(profile)
|
||||
update_user(user, profile, roles)
|
||||
|
||||
if not user.active:
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
if not user or not user.active:
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
return dict(message='The supplied credentials are invalid'), 403
|
||||
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
|
||||
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
@ -387,12 +387,14 @@ class OAuth2(Resource):
|
||||
update_user(user, profile, roles)
|
||||
|
||||
if not user.active:
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
return dict(message='The supplied credentials are invalid'), 403
|
||||
|
||||
# Tell Flask-Principal the identity changed
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(user.id))
|
||||
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
|
||||
|
||||
return dict(token=create_token(user))
|
||||
|
||||
|
||||
@ -431,15 +433,15 @@ class Google(Resource):
|
||||
|
||||
user = user_service.get_by_email(profile['email'])
|
||||
|
||||
if not user.active:
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
if not (user and user.active):
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
return dict(message='The supplied credentials are invalid.'), 403
|
||||
|
||||
if user:
|
||||
metrics.send('successful_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
|
||||
return dict(token=create_token(user))
|
||||
|
||||
metrics.send('invalid_login', 'counter', 1)
|
||||
metrics.send('login', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
|
||||
|
||||
class Providers(Resource):
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.authorities.models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models need to create an authority within Lemur.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -32,6 +32,9 @@ class Authority(db.Model):
|
||||
authority_certificate = relationship("Certificate", backref='root_authority', uselist=False, foreign_keys='Certificate.root_authority_id')
|
||||
certificates = relationship("Certificate", backref='authority', foreign_keys='Certificate.authority_id')
|
||||
|
||||
authority_pending_certificate = relationship("PendingCertificate", backref='root_authority', uselist=False, foreign_keys='PendingCertificate.root_authority_id')
|
||||
pending_certificates = relationship('PendingCertificate', backref='authority', foreign_keys='PendingCertificate.authority_id')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.owner = kwargs['owner']
|
||||
self.roles = kwargs.get('roles', [])
|
||||
@ -39,6 +42,7 @@ class Authority(db.Model):
|
||||
self.description = kwargs.get('description')
|
||||
self.authority_certificate = kwargs['authority_certificate']
|
||||
self.plugin_name = kwargs['plugin']['slug']
|
||||
self.options = kwargs.get('options')
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.authorities.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -3,12 +3,16 @@
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer authorities in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from lemur import database
|
||||
from lemur.common.utils import truthiness
|
||||
from lemur.extensions import metrics
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.roles import service as role_service
|
||||
@ -106,6 +110,8 @@ def create(**kwargs):
|
||||
|
||||
cert = upload(**kwargs)
|
||||
kwargs['authority_certificate'] = cert
|
||||
if kwargs.get('plugin', {}).get('plugin_options', []):
|
||||
kwargs['options'] = json.dumps(kwargs['plugin']['plugin_options'])
|
||||
|
||||
authority = Authority(**kwargs)
|
||||
authority = database.create(authority)
|
||||
@ -170,8 +176,8 @@ def render(args):
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
if 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
query = query.filter(Authority.active == terms[1])
|
||||
if 'active' in filt:
|
||||
query = query.filter(Authority.active == truthiness(terms[1]))
|
||||
else:
|
||||
query = database.filter(query, Authority, terms)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.authorities.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
0
lemur/authorizations/__init__.py
Normal file
0
lemur/authorizations/__init__.py
Normal file
34
lemur/authorizations/models.py
Normal file
34
lemur/authorizations/models.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""
|
||||
.. module: lemur.authorizations.models
|
||||
:platform: unix
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Netflix Secops <secops@netflix.com>
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy_utils import JSONType
|
||||
from lemur.database import db
|
||||
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
class Authorization(db.Model):
|
||||
__tablename__ = 'pending_dns_authorizations'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
account_number = Column(String(128))
|
||||
domains = Column(JSONType)
|
||||
dns_provider_type = Column(String(128))
|
||||
options = Column(JSONType)
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
return plugins.get(self.plugin_name)
|
||||
|
||||
def __repr__(self):
|
||||
return "Authorization(id={id})".format(label=self.id)
|
||||
|
||||
def __init__(self, account_number, domains, dns_provider_type, options=None):
|
||||
self.account_number = account_number
|
||||
self.domains = domains
|
||||
self.dns_provider_type = dns_provider_type
|
||||
self.options = options
|
24
lemur/authorizations/service.py
Normal file
24
lemur/authorizations/service.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.service
|
||||
Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved.
|
||||
.. moduleauthor:: Secops <secops@netflix.com>
|
||||
"""
|
||||
from lemur import database
|
||||
|
||||
from lemur.authorizations.models import Authorization
|
||||
|
||||
|
||||
def get(authorization_id):
|
||||
"""
|
||||
Retrieve dns authorization by ID
|
||||
"""
|
||||
return database.get(Authorization, authorization_id)
|
||||
|
||||
|
||||
def create(account_number, domains, dns_provider_type, options=None):
|
||||
"""
|
||||
Creates a new dns authorization.
|
||||
"""
|
||||
|
||||
authorization = Authorization(account_number, domains, dns_provider_type, options)
|
||||
return database.create(authorization)
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.certificate.cli
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -20,6 +20,7 @@ from lemur import database
|
||||
from lemur.extensions import sentry
|
||||
from lemur.extensions import metrics
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||
from lemur.deployment import service as deployment_service
|
||||
from lemur.endpoints import service as endpoint_service
|
||||
from lemur.notifications.messaging import send_rotation_notification
|
||||
@ -106,16 +107,17 @@ def request_rotation(endpoint, certificate, message, commit):
|
||||
:param commit:
|
||||
:return:
|
||||
"""
|
||||
status = FAILURE_METRIC_STATUS
|
||||
if commit:
|
||||
try:
|
||||
deployment_service.rotate_certificate(endpoint, certificate)
|
||||
metrics.send('endpoint_rotation_success', 'counter', 1)
|
||||
|
||||
if message:
|
||||
send_rotation_notification(certificate)
|
||||
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
|
||||
except Exception as e:
|
||||
metrics.send('endpoint_rotation_failure', 'counter', 1)
|
||||
print(
|
||||
"[!] Failed to rotate endpoint {0} to certificate {1} reason: {2}".format(
|
||||
endpoint.name,
|
||||
@ -124,6 +126,8 @@ def request_rotation(endpoint, certificate, message, commit):
|
||||
)
|
||||
)
|
||||
|
||||
metrics.send('endpoint_rotation', 'counter', 1, metric_tags={'status': status})
|
||||
|
||||
|
||||
def request_reissue(certificate, commit):
|
||||
"""
|
||||
@ -132,16 +136,32 @@ def request_reissue(certificate, commit):
|
||||
:param commit:
|
||||
:return:
|
||||
"""
|
||||
# set the lemur identity for all cli commands
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
|
||||
status = FAILURE_METRIC_STATUS
|
||||
try:
|
||||
print("[+] {0} is eligible for re-issuance".format(certificate.name))
|
||||
|
||||
details = get_certificate_primitives(certificate)
|
||||
print_certificate_details(details)
|
||||
# set the lemur identity for all cli commands
|
||||
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
|
||||
|
||||
if commit:
|
||||
new_cert = reissue_certificate(certificate, replace=True)
|
||||
metrics.send('certificate_reissue_success', 'counter', 1)
|
||||
print("[+] New certificate named: {0}".format(new_cert.name))
|
||||
details = get_certificate_primitives(certificate)
|
||||
print_certificate_details(details)
|
||||
|
||||
if commit:
|
||||
new_cert = reissue_certificate(certificate, replace=True)
|
||||
print("[+] New certificate named: {0}".format(new_cert.name))
|
||||
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
current_app.logger.exception("Error reissuing certificate.", exc_info=True)
|
||||
print(
|
||||
"[!] Failed to reissue certificates. Reason: {}".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
|
||||
metrics.send('certificate_reissue', 'counter', 1, metric_tags={'status': status})
|
||||
|
||||
|
||||
@manager.option('-e', '--endpoint', dest='endpoint_name', help='Name of the endpoint you wish to rotate.')
|
||||
@ -159,6 +179,8 @@ def rotate(endpoint_name, new_certificate_name, old_certificate_name, message, c
|
||||
|
||||
print("[+] Starting endpoint rotation.")
|
||||
|
||||
status = FAILURE_METRIC_STATUS
|
||||
|
||||
try:
|
||||
old_cert = validate_certificate(old_certificate_name)
|
||||
new_cert = validate_certificate(new_certificate_name)
|
||||
@ -182,14 +204,19 @@ def rotate(endpoint_name, new_certificate_name, old_certificate_name, message, c
|
||||
print("[+] Rotating {0} to {1}".format(endpoint.name, endpoint.certificate.replaced[0].name))
|
||||
request_rotation(endpoint, endpoint.certificate.replaced[0], message, commit)
|
||||
else:
|
||||
metrics.send('endpoint_rotation_failure', 'counter', 1)
|
||||
metrics.send('endpoint_rotation', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
print("[!] Failed to rotate endpoint {0} reason: Multiple replacement certificates found.".format(
|
||||
endpoint.name
|
||||
))
|
||||
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
print("[+] Done!")
|
||||
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
|
||||
metrics.send('endpoint_rotation_job', 'counter', 1, metric_tags={'status': status})
|
||||
|
||||
|
||||
@manager.option('-o', '--old-certificate', dest='old_certificate_name', help='Name of the certificate you wish to reissue.')
|
||||
@manager.option('-c', '--commit', dest='commit', action='store_true', default=False, help='Persist changes.')
|
||||
@ -204,26 +231,30 @@ def reissue(old_certificate_name, commit):
|
||||
|
||||
print("[+] Starting certificate re-issuance.")
|
||||
|
||||
status = FAILURE_METRIC_STATUS
|
||||
|
||||
try:
|
||||
old_cert = validate_certificate(old_certificate_name)
|
||||
|
||||
if not old_cert:
|
||||
for certificate in get_all_pending_reissue():
|
||||
print("[+] {0} is eligible for re-issuance".format(certificate.name))
|
||||
request_reissue(certificate, commit)
|
||||
else:
|
||||
request_reissue(old_cert, commit)
|
||||
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
print("[+] Done!")
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
metrics.send('certificate_reissue_failure', 'counter', 1)
|
||||
current_app.logger.exception("Error reissuing certificate.", exc_info=True)
|
||||
print(
|
||||
"[!] Failed to reissue certificates. Reason: {}".format(
|
||||
e
|
||||
)
|
||||
)
|
||||
|
||||
metrics.send('certificate_reissue_job', 'counter', 1, metric_tags={'status': status})
|
||||
|
||||
|
||||
@manager.option('-f', '--fqdns', dest='fqdns', help='FQDNs to query. Multiple fqdns specified via comma.')
|
||||
@manager.option('-i', '--issuer', dest='issuer', help='Issuer to query for.')
|
||||
@ -275,9 +306,11 @@ def worker(data, commit, reason):
|
||||
if commit:
|
||||
plugin.revoke_certificate(cert, reason)
|
||||
|
||||
metrics.send('certificate_revoke', 'counter', 1, metric_tags={'status': SUCCESS_METRIC_STATUS})
|
||||
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
metrics.send('certificate_revoke_failure', 'counter', 1)
|
||||
metrics.send('certificate_revoke', 'counter', 1, metric_tags={'status': FAILURE_METRIC_STATUS})
|
||||
print(
|
||||
"[!] Failed to revoke certificates. Reason: {}".format(
|
||||
e
|
||||
|
@ -3,7 +3,7 @@ Debugging hooks for dumping imported or generated CSR and certificate details to
|
||||
|
||||
.. module: lemur.certificates.hooks
|
||||
:platform: Unix
|
||||
:copyright: (c) 2016-2017 by Marti Raudsepp, see AUTHORS for more
|
||||
:copyright: (c) 2018 by Marti Raudsepp, see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Marti Raudsepp <marti@juffo.org>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.certificates.models
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -33,10 +33,11 @@ from lemur.common import defaults
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
from lemur.extensions import metrics
|
||||
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||
|
||||
from lemur.models import certificate_associations, certificate_source_associations, \
|
||||
certificate_destination_associations, certificate_notification_associations, \
|
||||
certificate_replacement_associations, roles_certificates
|
||||
certificate_replacement_associations, roles_certificates, pending_cert_replacement_associations
|
||||
|
||||
from lemur.domains.models import Domain
|
||||
from lemur.policies.models import RotationPolicy
|
||||
@ -101,6 +102,7 @@ class Certificate(db.Model):
|
||||
serial = Column(String(128))
|
||||
cn = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True)
|
||||
|
||||
not_before = Column(ArrowType)
|
||||
not_after = Column(ArrowType)
|
||||
@ -128,6 +130,11 @@ class Certificate(db.Model):
|
||||
secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa
|
||||
backref='replaced')
|
||||
|
||||
replaced_by_pending = relationship('PendingCertificate',
|
||||
secondary=pending_cert_replacement_associations,
|
||||
backref='pending_replace',
|
||||
viewonly=True)
|
||||
|
||||
logs = relationship('Log', backref='certificate')
|
||||
endpoints = relationship('Endpoint', backref='certificate')
|
||||
rotation_policy = relationship("RotationPolicy")
|
||||
@ -171,6 +178,8 @@ class Certificate(db.Model):
|
||||
self.signing_algorithm = defaults.signing_algorithm(cert)
|
||||
self.bits = defaults.bitstrength(cert)
|
||||
self.external_id = kwargs.get('external_id')
|
||||
self.authority_id = kwargs.get('authority_id')
|
||||
self.dns_provider_id = kwargs.get('dns_provider_id')
|
||||
|
||||
for domain in defaults.domains(cert):
|
||||
self.domains.append(Domain(name=domain))
|
||||
@ -326,9 +335,8 @@ class Certificate(db.Model):
|
||||
|
||||
return_extensions['authority_key_identifier'] = aki
|
||||
|
||||
# TODO: Don't support CRLDistributionPoints yet https://github.com/Netflix/lemur/issues/662
|
||||
elif isinstance(value, x509.CRLDistributionPoints):
|
||||
current_app.logger.warning('CRLDistributionPoints not yet supported for clone operation.')
|
||||
return_extensions['crl_distribution_points'] = {'include_crl_dp': value}
|
||||
|
||||
# TODO: Not supporting custom OIDs yet. https://github.com/Netflix/lemur/issues/665
|
||||
else:
|
||||
@ -358,15 +366,16 @@ def update_destinations(target, value, initiator):
|
||||
:return:
|
||||
"""
|
||||
destination_plugin = plugins.get(value.plugin_name)
|
||||
|
||||
status = FAILURE_METRIC_STATUS
|
||||
try:
|
||||
if target.private_key:
|
||||
destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options)
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
current_app.logger.exception(e)
|
||||
metrics.send('destination_upload_failure', 'counter', 1,
|
||||
metric_tags={'certificate': target.name, 'destination': value.label})
|
||||
|
||||
metrics.send('destination_upload', 'counter', 1,
|
||||
metric_tags={'status': status, 'certificate': target.name, 'destination': value.label})
|
||||
|
||||
|
||||
@event.listens_for(Certificate.replaces, 'append')
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.certificates.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -9,31 +9,30 @@ from flask import current_app
|
||||
from marshmallow import fields, validate, validates_schema, post_load, pre_load
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.common import validators, missing
|
||||
from lemur.common.fields import ArrowDateTime, Hex
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.constants import CERTIFICATE_KEY_TYPES
|
||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||
from lemur.notifications import service as notification_service
|
||||
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
|
||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||
from lemur.schemas import (
|
||||
AssociatedAuthoritySchema,
|
||||
AssociatedDestinationSchema,
|
||||
AssociatedCertificateSchema,
|
||||
AssociatedNotificationSchema,
|
||||
AssociatedDnsProviderSchema,
|
||||
PluginInputSchema,
|
||||
ExtensionSchema,
|
||||
AssociatedRoleSchema,
|
||||
EndpointNestedOutputSchema,
|
||||
AssociatedRotationPolicySchema
|
||||
AssociatedRotationPolicySchema,
|
||||
)
|
||||
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
|
||||
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.common import validators, missing
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
from lemur.common.fields import ArrowDateTime, Hex
|
||||
|
||||
|
||||
class CertificateSchema(LemurInputSchema):
|
||||
@ -45,11 +44,14 @@ class CertificateCreationSchema(CertificateSchema):
|
||||
@post_load
|
||||
def default_notification(self, data):
|
||||
if not data['notifications']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(
|
||||
"DEFAULT_{0}".format(data['owner'].split('@')[0].upper()),
|
||||
[data['owner']],
|
||||
)
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(
|
||||
'DEFAULT_SECURITY',
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@ -67,9 +69,13 @@ class CertificateInputSchema(CertificateCreationSchema):
|
||||
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) # deprecated
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
dns_provider = fields.Nested(AssociatedDnsProviderSchema, missing=None, allow_none=True, required=False)
|
||||
|
||||
csr = fields.String(validate=validators.csr)
|
||||
key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048')
|
||||
|
||||
key_type = fields.String(
|
||||
validate=validate.OneOf(CERTIFICATE_KEY_TYPES),
|
||||
missing='RSA2048')
|
||||
|
||||
notify = fields.Boolean(default=True)
|
||||
rotation = fields.Boolean()
|
||||
@ -180,6 +186,7 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||
description = fields.String()
|
||||
issuer = fields.String()
|
||||
name = fields.String()
|
||||
dns_provider_id = fields.Integer(required=False, allow_none=True)
|
||||
|
||||
rotation = fields.Boolean()
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""
|
||||
.. module: lemur.certificate.service
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import arrow
|
||||
|
||||
from flask import current_app
|
||||
from sqlalchemy import func, or_, not_, cast, Boolean, Integer
|
||||
from sqlalchemy import func, or_, not_, cast, Integer
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
@ -17,7 +17,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
||||
from lemur import database
|
||||
from lemur.extensions import metrics, signals
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.common.utils import generate_private_key
|
||||
from lemur.common.utils import generate_private_key, truthiness
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.domains.models import Domain
|
||||
@ -25,6 +25,7 @@ from lemur.authorities.models import Authority
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.pending_certificates.models import PendingCertificate
|
||||
|
||||
from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema
|
||||
|
||||
@ -63,6 +64,9 @@ def get_by_serial(serial):
|
||||
:param serial:
|
||||
:return:
|
||||
"""
|
||||
if isinstance(serial, int):
|
||||
# although serial is a number, the DB column is String(128)
|
||||
serial = str(serial)
|
||||
return Certificate.query.filter(Certificate.serial == serial).all()
|
||||
|
||||
|
||||
@ -190,7 +194,7 @@ def mint(**kwargs):
|
||||
csr_imported.send(authority=authority, csr=csr)
|
||||
|
||||
cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs)
|
||||
return cert_body, private_key, cert_chain, external_id
|
||||
return cert_body, private_key, cert_chain, external_id, csr
|
||||
|
||||
|
||||
def import_certificate(**kwargs):
|
||||
@ -243,11 +247,12 @@ def create(**kwargs):
|
||||
"""
|
||||
Creates a new certificate.
|
||||
"""
|
||||
cert_body, private_key, cert_chain, external_id = mint(**kwargs)
|
||||
cert_body, private_key, cert_chain, external_id, csr = mint(**kwargs)
|
||||
kwargs['body'] = cert_body
|
||||
kwargs['private_key'] = private_key
|
||||
kwargs['chain'] = cert_chain
|
||||
kwargs['external_id'] = external_id
|
||||
kwargs['csr'] = csr
|
||||
|
||||
roles = create_certificate_roles(**kwargs)
|
||||
|
||||
@ -256,15 +261,20 @@ def create(**kwargs):
|
||||
else:
|
||||
kwargs['roles'] = roles
|
||||
|
||||
cert = Certificate(**kwargs)
|
||||
if cert_body:
|
||||
cert = Certificate(**kwargs)
|
||||
kwargs['creator'].certificates.append(cert)
|
||||
else:
|
||||
cert = PendingCertificate(**kwargs)
|
||||
kwargs['creator'].pending_certificates.append(cert)
|
||||
|
||||
kwargs['creator'].certificates.append(cert)
|
||||
cert.authority = kwargs['authority']
|
||||
certificate_issued.send(certificate=cert, authority=cert.authority)
|
||||
|
||||
database.commit()
|
||||
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
if isinstance(cert, Certificate):
|
||||
certificate_issued.send(certificate=cert, authority=cert.authority)
|
||||
metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer))
|
||||
return cert
|
||||
|
||||
|
||||
@ -288,16 +298,20 @@ def render(args):
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
term = '%{0}%'.format(terms[1])
|
||||
# Exact matches for quotes. Only applies to name, issuer, and cn
|
||||
if terms[1].startswith('"') and terms[1].endswith('"'):
|
||||
term = terms[1][1:-1]
|
||||
|
||||
if 'issuer' in terms:
|
||||
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||
sub_query = database.session_query(Authority.id)\
|
||||
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
|
||||
.filter(Authority.name.ilike(term))\
|
||||
.subquery()
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
Certificate.issuer.ilike('%{0}%'.format(terms[1])),
|
||||
Certificate.issuer.ilike(term),
|
||||
Certificate.authority_id.in_(sub_query)
|
||||
)
|
||||
)
|
||||
@ -305,18 +319,26 @@ def render(args):
|
||||
elif 'destination' in terms:
|
||||
query = query.filter(Certificate.destinations.any(Destination.id == terms[1]))
|
||||
elif 'notify' in filt:
|
||||
query = query.filter(Certificate.notify == cast(terms[1], Boolean))
|
||||
query = query.filter(Certificate.notify == truthiness(terms[1]))
|
||||
elif 'active' in filt:
|
||||
query = query.filter(Certificate.active == terms[1])
|
||||
query = query.filter(Certificate.active == truthiness(terms[1]))
|
||||
elif 'cn' in terms:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Certificate.cn.ilike('%{0}%'.format(terms[1])),
|
||||
Certificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
|
||||
Certificate.cn.ilike(term),
|
||||
Certificate.domains.any(Domain.name.ilike(term))
|
||||
)
|
||||
)
|
||||
elif 'id' in terms:
|
||||
query = query.filter(Certificate.id == cast(terms[1], Integer))
|
||||
elif 'name' in terms:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Certificate.name.ilike(term),
|
||||
Certificate.domains.any(Domain.name.ilike(term)),
|
||||
Certificate.cn.ilike(term),
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = database.filter(query, Certificate, terms)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.certificates.verify
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.certificates.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -18,6 +18,7 @@ from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
||||
|
||||
from lemur.certificates import service
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.certificates.schemas import (
|
||||
certificate_input_schema,
|
||||
@ -267,7 +268,9 @@ class CertificatesList(AuthenticatedResource):
|
||||
if authority_permission.can():
|
||||
data['creator'] = g.user
|
||||
cert = service.create(**data)
|
||||
log_service.create(g.user, 'create_cert', certificate=cert)
|
||||
if isinstance(cert, Certificate):
|
||||
# only log if created, not pending
|
||||
log_service.create(g.user, 'create_cert', certificate=cert)
|
||||
return cert
|
||||
|
||||
return dict(message="You are not authorized to use the authority: {0}".format(data['authority'].name)), 403
|
||||
@ -297,12 +300,14 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"owner": "joe@example.com",
|
||||
"publicCert": "-----BEGIN CERTIFICATE-----...",
|
||||
"intermediateCert": "-----BEGIN CERTIFICATE-----...",
|
||||
"body": "-----BEGIN CERTIFICATE-----...",
|
||||
"chain": "-----BEGIN CERTIFICATE-----...",
|
||||
"privateKey": "-----BEGIN RSA PRIVATE KEY-----..."
|
||||
"destinations": [],
|
||||
"notifications": [],
|
||||
"replacements": [],
|
||||
"roles": [],
|
||||
"notify": true,
|
||||
"name": "cert1"
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
|
||||
def text_to_slug(value):
|
||||
"""Normalize a string to a "slug" value, stripping character accents and removing non-alphanum characters."""
|
||||
|
||||
# Strip all character accents (ä => a): decompose Unicode characters and then drop combining chars.
|
||||
# Strip all character accents: decompose Unicode characters and then drop combining chars.
|
||||
value = ''.join(c for c in unicodedata.normalize('NFKD', value) if not unicodedata.combining(c))
|
||||
|
||||
# Replace all remaining non-alphanumeric characters with '-'. Multiple characters get collapsed into a single dash.
|
||||
@ -162,6 +162,9 @@ def domains(cert):
|
||||
entries = ext.value.get_values_for_type(x509.DNSName)
|
||||
for entry in entries:
|
||||
domains.append(entry)
|
||||
except x509.ExtensionNotFound:
|
||||
if current_app.config.get("LOG_SSL_SUBJ_ALT_NAME_ERRORS", True):
|
||||
sentry.captureException()
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
|
||||
@ -175,7 +178,7 @@ def serial(cert):
|
||||
:param cert:
|
||||
:return: serial number
|
||||
"""
|
||||
return cert.serial
|
||||
return cert.serial_number
|
||||
|
||||
|
||||
def san(cert):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.common.fields
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.common.health
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.common.managers
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.common.schema
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
@ -135,7 +135,6 @@ def unwrap_pagination(data, output_schema):
|
||||
marshaled_data = {'total': len(data)}
|
||||
marshaled_data['items'] = output_schema.dump(data, many=True).data
|
||||
return marshaled_data
|
||||
|
||||
return output_schema.dump(data).data
|
||||
|
||||
|
||||
|
@ -1,23 +1,22 @@
|
||||
"""
|
||||
.. module: lemur.common.utils
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import string
|
||||
import random
|
||||
import string
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import and_, func
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||
from flask_restful.reqparse import RequestParser
|
||||
from sqlalchemy import and_, func
|
||||
|
||||
from lemur.constants import CERTIFICATE_KEY_TYPES
|
||||
from lemur.exceptions import InvalidConfiguration
|
||||
|
||||
paginated_parser = RequestParser()
|
||||
@ -53,6 +52,19 @@ def parse_certificate(body):
|
||||
return x509.load_pem_x509_certificate(body, default_backend())
|
||||
|
||||
|
||||
def parse_csr(csr):
|
||||
"""
|
||||
Helper function that parses a CSR.
|
||||
|
||||
:param csr:
|
||||
:return:
|
||||
"""
|
||||
if isinstance(csr, str):
|
||||
csr = csr.encode('utf-8')
|
||||
|
||||
return x509.load_pem_x509_csr(csr, default_backend())
|
||||
|
||||
|
||||
def get_authority_key(body):
|
||||
"""Returns the authority key for a given certificate in hex format"""
|
||||
parsed_cert = parse_certificate(body)
|
||||
@ -65,17 +77,43 @@ def generate_private_key(key_type):
|
||||
"""
|
||||
Generates a new private key based on key_type.
|
||||
|
||||
Valid key types: RSA2048, RSA4096
|
||||
Valid key types: RSA2048, RSA4096', 'ECCPRIME192V1', 'ECCPRIME256V1', 'ECCSECP192R1',
|
||||
'ECCSECP224R1', 'ECCSECP256R1', 'ECCSECP384R1', 'ECCSECP521R1', 'ECCSECP256K1',
|
||||
'ECCSECT163K1', 'ECCSECT233K1', 'ECCSECT283K1', 'ECCSECT409K1', 'ECCSECT571K1',
|
||||
'ECCSECT163R2', 'ECCSECT233R1', 'ECCSECT283R1', 'ECCSECT409R1', 'ECCSECT571R2'
|
||||
|
||||
:param key_type:
|
||||
:return:
|
||||
"""
|
||||
valid_key_types = ['RSA2048', 'RSA4096']
|
||||
|
||||
if key_type not in valid_key_types:
|
||||
_CURVE_TYPES = {
|
||||
"ECCPRIME192V1": ec.SECP192R1(),
|
||||
"ECCPRIME256V1": ec.SECP256R1(),
|
||||
|
||||
"ECCSECP192R1": ec.SECP192R1(),
|
||||
"ECCSECP224R1": ec.SECP224R1(),
|
||||
"ECCSECP256R1": ec.SECP256R1(),
|
||||
"ECCSECP384R1": ec.SECP384R1(),
|
||||
"ECCSECP521R1": ec.SECP521R1(),
|
||||
"ECCSECP256K1": ec.SECP256K1(),
|
||||
|
||||
"ECCSECT163K1": ec.SECT163K1(),
|
||||
"ECCSECT233K1": ec.SECT233K1(),
|
||||
"ECCSECT283K1": ec.SECT283K1(),
|
||||
"ECCSECT409K1": ec.SECT409K1(),
|
||||
"ECCSECT571K1": ec.SECT571K1(),
|
||||
|
||||
"ECCSECT163R2": ec.SECT163R2(),
|
||||
"ECCSECT233R1": ec.SECT233R1(),
|
||||
"ECCSECT283R1": ec.SECT283R1(),
|
||||
"ECCSECT409R1": ec.SECT409R1(),
|
||||
"ECCSECT571R2": ec.SECT571R1(),
|
||||
}
|
||||
|
||||
if key_type not in CERTIFICATE_KEY_TYPES:
|
||||
raise Exception("Invalid key type: {key_type}. Supported key types: {choices}".format(
|
||||
key_type=key_type,
|
||||
choices=",".join(valid_key_types)
|
||||
choices=",".join(CERTIFICATE_KEY_TYPES)
|
||||
))
|
||||
|
||||
if 'RSA' in key_type:
|
||||
@ -85,6 +123,11 @@ def generate_private_key(key_type):
|
||||
key_size=key_size,
|
||||
backend=default_backend()
|
||||
)
|
||||
elif 'ECC' in key_type:
|
||||
return ec.generate_private_key(
|
||||
curve=_CURVE_TYPES[key_type],
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
|
||||
def is_weekend(date):
|
||||
@ -162,3 +205,9 @@ def windowed_query(q, column, windowsize):
|
||||
column, windowsize):
|
||||
for row in q.filter(whereclause).order_by(column):
|
||||
yield row
|
||||
|
||||
|
||||
def truthiness(s):
|
||||
"""If input string resembles something truthy then return True, else False."""
|
||||
|
||||
return s.lower() in ('true', 'yes', 'on', 't', '1')
|
||||
|
@ -1,8 +1,34 @@
|
||||
"""
|
||||
.. module: lemur.constants
|
||||
:copyright: (c) 2015 by Netflix Inc.
|
||||
:copyright: (c) 2018 by Netflix Inc.
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}"
|
||||
DEFAULT_NAMING_TEMPLATE = "{subject}-{issuer}-{not_before}-{not_after}"
|
||||
NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}"
|
||||
|
||||
SUCCESS_METRIC_STATUS = 'success'
|
||||
FAILURE_METRIC_STATUS = 'failure'
|
||||
|
||||
CERTIFICATE_KEY_TYPES = [
|
||||
'RSA2048',
|
||||
'RSA4096',
|
||||
'ECCPRIME192V1',
|
||||
'ECCPRIME256V1',
|
||||
'ECCSECP192R1',
|
||||
'ECCSECP224R1',
|
||||
'ECCSECP256R1',
|
||||
'ECCSECP384R1',
|
||||
'ECCSECP521R1',
|
||||
'ECCSECP256K1',
|
||||
'ECCSECT163K1',
|
||||
'ECCSECT233K1',
|
||||
'ECCSECT283K1',
|
||||
'ECCSECT409K1',
|
||||
'ECCSECT571K1',
|
||||
'ECCSECT163R2',
|
||||
'ECCSECT233R1',
|
||||
'ECCSECT283R1',
|
||||
'ECCSECT409R1',
|
||||
'ECCSECT571R2'
|
||||
]
|
||||
|
@ -4,13 +4,13 @@
|
||||
:synopsis: This module contains all of the database related methods
|
||||
needed for lemur to interact with a datastore
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from inflection import underscore
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy import exc, func
|
||||
from sqlalchemy.sql import and_, or_
|
||||
from sqlalchemy.orm import make_transient
|
||||
|
||||
@ -267,6 +267,17 @@ def clone(model):
|
||||
return model
|
||||
|
||||
|
||||
def get_count(q):
|
||||
"""
|
||||
Count the number of rows in a table. More efficient than count(*)
|
||||
:param q:
|
||||
:return:
|
||||
"""
|
||||
count_q = q.statement.with_only_columns([func.count()]).order_by(None)
|
||||
count = q.session.execute(count_q).scalar()
|
||||
return count
|
||||
|
||||
|
||||
def sort_and_page(query, model, args):
|
||||
"""
|
||||
Helper that allows us to combine sorting and paging
|
||||
@ -289,7 +300,7 @@ def sort_and_page(query, model, args):
|
||||
if sort_by and sort_dir:
|
||||
query = sort(query, model, sort_by, sort_dir)
|
||||
|
||||
total = query.count()
|
||||
total = get_count(query)
|
||||
|
||||
# offset calculated at zero
|
||||
page -= 1
|
||||
|
@ -1,56 +0,0 @@
|
||||
"""
|
||||
.. module: lemur.decorators
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from builtins import str
|
||||
|
||||
from datetime import timedelta
|
||||
from flask import make_response, request, current_app
|
||||
|
||||
from functools import update_wrapper
|
||||
|
||||
|
||||
# this is only used for dev
|
||||
def crossdomain(origin=None, methods=None, headers=None,
|
||||
max_age=21600, attach_to_all=True,
|
||||
automatic_options=True): # pragma: no cover
|
||||
if methods is not None:
|
||||
methods = ', '.join(sorted(x.upper() for x in methods))
|
||||
|
||||
if headers is not None and not isinstance(headers, str):
|
||||
headers = ', '.join(x.upper() for x in headers)
|
||||
|
||||
if not isinstance(origin, str):
|
||||
origin = ', '.join(origin)
|
||||
|
||||
if isinstance(max_age, timedelta):
|
||||
max_age = max_age.total_seconds()
|
||||
|
||||
def get_methods():
|
||||
if methods is not None:
|
||||
return methods
|
||||
|
||||
options_resp = current_app.make_default_options_response()
|
||||
return options_resp.headers['allow']
|
||||
|
||||
def decorator(f):
|
||||
def wrapped_function(*args, **kwargs):
|
||||
if automatic_options and request.method == 'OPTIONS':
|
||||
resp = current_app.make_default_options_response()
|
||||
else:
|
||||
resp = make_response(f(*args, **kwargs))
|
||||
if not attach_to_all and request.method != 'OPTIONS':
|
||||
return resp
|
||||
|
||||
h = resp.headers
|
||||
h['Access-Control-Allow-Origin'] = origin
|
||||
h['Access-Control-Allow-Methods'] = get_methods()
|
||||
h['Access-Control-Max-Age'] = str(max_age)
|
||||
h['Access-Control-Allow-Headers'] = "Origin, X-Requested-With, Content-Type, Accept, Authorization "
|
||||
h['Access-Control-Allow-Credentials'] = 'true'
|
||||
return resp
|
||||
|
||||
f.provide_automatic_options = False
|
||||
return update_wrapper(wrapped_function, f)
|
||||
return decorator
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.defaults.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
.. module: lemur.defaults.views
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app, Blueprint
|
||||
@ -50,7 +50,8 @@ class LemurDefaults(AuthenticatedResource):
|
||||
"state": "CA",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations"
|
||||
"organizationalUnit": "Operations",
|
||||
"dnsProviders": [{"name": "test", ...}, {...}],
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
@ -67,7 +68,7 @@ class LemurDefaults(AuthenticatedResource):
|
||||
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
organizational_unit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
|
||||
issuer_plugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN'),
|
||||
authority=default_authority
|
||||
authority=default_authority,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.destinations.models
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.destinations.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.destinations.service
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.destinations.views
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the accounts view code.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
37
lemur/dns_providers/models.py
Normal file
37
lemur/dns_providers/models.py
Normal file
@ -0,0 +1,37 @@
|
||||
from sqlalchemy import Column, Integer, String, text, Text
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
from sqlalchemy_utils import ArrowType
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.utils import Vault
|
||||
|
||||
|
||||
class DnsProvider(db.Model):
|
||||
__tablename__ = 'dns_providers'
|
||||
id = Column(
|
||||
Integer(),
|
||||
primary_key=True,
|
||||
)
|
||||
name = Column(String(length=256), unique=True, nullable=True)
|
||||
description = Column(Text(), nullable=True)
|
||||
provider_type = Column(String(length=256), nullable=True)
|
||||
credentials = Column(Vault, nullable=True)
|
||||
api_endpoint = Column(String(length=256), nullable=True)
|
||||
date_created = Column(ArrowType(), server_default=text('now()'), nullable=False)
|
||||
status = Column(String(length=128), nullable=True)
|
||||
options = Column(JSON, nullable=True)
|
||||
domains = Column(JSON, nullable=True)
|
||||
|
||||
def __init__(self, name, description, provider_type, credentials):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.provider_type = provider_type
|
||||
self.credentials = credentials
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
return plugins.get(self.plugin_name)
|
||||
|
||||
def __repr__(self):
|
||||
return "DnsProvider(name={name})".format(name=self.name)
|
27
lemur/dns_providers/schemas.py
Normal file
27
lemur/dns_providers/schemas.py
Normal file
@ -0,0 +1,27 @@
|
||||
from lemur.common.fields import ArrowDateTime
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
|
||||
from marshmallow import fields
|
||||
|
||||
|
||||
class DnsProvidersNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
providerType = fields.String()
|
||||
description = fields.String()
|
||||
credentials = fields.String()
|
||||
api_endpoint = fields.String()
|
||||
date_created = ArrowDateTime()
|
||||
|
||||
|
||||
class DnsProvidersNestedInputSchema(LemurInputSchema):
|
||||
__envelope__ = False
|
||||
name = fields.String()
|
||||
description = fields.String()
|
||||
provider_type = fields.Dict()
|
||||
|
||||
|
||||
dns_provider_output_schema = DnsProvidersNestedOutputSchema()
|
||||
|
||||
dns_provider_input_schema = DnsProvidersNestedInputSchema()
|
111
lemur/dns_providers/service.py
Normal file
111
lemur/dns_providers/service.py
Normal file
@ -0,0 +1,111 @@
|
||||
import json
|
||||
|
||||
from flask import current_app
|
||||
from lemur import database
|
||||
from lemur.dns_providers.models import DnsProvider
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper that helps us render the REST Api responses.
|
||||
:param args:
|
||||
:return:
|
||||
"""
|
||||
query = database.session_query(DnsProvider)
|
||||
|
||||
return database.sort_and_page(query, DnsProvider, args)
|
||||
|
||||
|
||||
def get(dns_provider_id):
|
||||
provider = database.get(DnsProvider, dns_provider_id)
|
||||
return provider
|
||||
|
||||
|
||||
def get_friendly(dns_provider_id):
|
||||
"""
|
||||
Retrieves a dns provider by its lemur assigned ID.
|
||||
|
||||
:param dns_provider_id: Lemur assigned ID
|
||||
:rtype : DnsProvider
|
||||
:return:
|
||||
"""
|
||||
dns_provider = get(dns_provider_id)
|
||||
dns_provider_friendly = {
|
||||
"name": dns_provider.name,
|
||||
"description": dns_provider.description,
|
||||
"providerType": dns_provider.provider_type,
|
||||
"options": dns_provider.options,
|
||||
"credentials": dns_provider.credentials,
|
||||
}
|
||||
|
||||
if dns_provider.provider_type == "route53":
|
||||
dns_provider_friendly["account_id"] = json.loads(dns_provider.credentials).get("account_id")
|
||||
return dns_provider_friendly
|
||||
|
||||
|
||||
def delete(dns_provider_id):
|
||||
"""
|
||||
Deletes a DNS provider.
|
||||
|
||||
:param dns_provider_id: Lemur assigned ID
|
||||
"""
|
||||
database.delete(get(dns_provider_id))
|
||||
|
||||
|
||||
def get_types():
|
||||
provider_config = current_app.config.get(
|
||||
'ACME_DNS_PROVIDER_TYPES',
|
||||
{"items": [
|
||||
{
|
||||
'name': 'route53',
|
||||
'requirements': [
|
||||
{
|
||||
'name': 'account_id',
|
||||
'type': 'int',
|
||||
'required': True,
|
||||
'helpMessage': 'AWS Account number'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'cloudflare',
|
||||
'requirements': [
|
||||
{
|
||||
'name': 'email',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'helpMessage': 'Cloudflare Email'
|
||||
},
|
||||
{
|
||||
'name': 'key',
|
||||
'type': 'str',
|
||||
'required': True,
|
||||
'helpMessage': 'Cloudflare Key'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'dyn',
|
||||
},
|
||||
]}
|
||||
)
|
||||
if not provider_config:
|
||||
raise Exception("No DNS Provider configuration specified.")
|
||||
provider_config["total"] = len(provider_config.get("items"))
|
||||
return provider_config
|
||||
|
||||
|
||||
def create(data):
|
||||
provider_name = data.get("name")
|
||||
|
||||
credentials = {}
|
||||
for item in data.get("provider_type", {}).get("requirements", []):
|
||||
credentials[item["name"]] = item["value"]
|
||||
dns_provider = DnsProvider(
|
||||
name=provider_name,
|
||||
description=data.get("description"),
|
||||
provider_type=data.get("provider_type").get("name"),
|
||||
credentials=json.dumps(credentials),
|
||||
)
|
||||
created = database.create(dns_provider)
|
||||
return created.id
|
170
lemur/dns_providers/views.py
Normal file
170
lemur/dns_providers/views.py
Normal file
@ -0,0 +1,170 @@
|
||||
"""
|
||||
.. module: lemur.dns)providers.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
|
||||
"""
|
||||
from flask import Blueprint, g
|
||||
from flask_restful import reqparse, Api
|
||||
|
||||
from lemur.auth.permissions import admin_permission
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
from lemur.dns_providers import service
|
||||
from lemur.dns_providers.schemas import dns_provider_output_schema, dns_provider_input_schema
|
||||
|
||||
mod = Blueprint('dns_providers', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class DnsProvidersList(AuthenticatedResource):
|
||||
""" Defines the 'dns_providers' endpoint """
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(DnsProvidersList, self).__init__()
|
||||
|
||||
@validate_schema(None, dns_provider_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /dns_providers
|
||||
|
||||
The current list of DNS Providers
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /dns_providers HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"items": [{
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"description": "test",
|
||||
"provider_type": "dyn",
|
||||
"status": "active",
|
||||
}],
|
||||
"total": 1
|
||||
}
|
||||
|
||||
:query sortBy: field to sort on
|
||||
:query sortDir: asc or desc
|
||||
:query page: int. default is 1
|
||||
:query filter: key value pair format is k;v
|
||||
:query count: count number. default is 10
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('dns_provider_id', type=int, location='args')
|
||||
parser.add_argument('name', type=str, location='args')
|
||||
parser.add_argument('type', type=str, location='args')
|
||||
|
||||
args = parser.parse_args()
|
||||
args['user'] = g.user
|
||||
return service.render(args)
|
||||
|
||||
@validate_schema(dns_provider_input_schema, None)
|
||||
@admin_permission.require(http_exception=403)
|
||||
def post(self, data=None):
|
||||
"""
|
||||
Creates a DNS Provider
|
||||
|
||||
**Example request**:
|
||||
{
|
||||
"providerType": {
|
||||
"name": "route53",
|
||||
"requirements": [
|
||||
{
|
||||
"name": "account_id",
|
||||
"type": "int",
|
||||
"required": true,
|
||||
"helpMessage": "AWS Account number",
|
||||
"value": 12345
|
||||
}
|
||||
],
|
||||
"route": "dns_provider_options",
|
||||
"reqParams": null,
|
||||
"restangularized": true,
|
||||
"fromServer": true,
|
||||
"parentResource": null,
|
||||
"restangularCollection": false
|
||||
},
|
||||
"name": "provider_name",
|
||||
"description": "provider_description"
|
||||
}
|
||||
|
||||
**Example request 2**
|
||||
{
|
||||
"providerType": {
|
||||
"name": "cloudflare",
|
||||
"requirements": [
|
||||
{
|
||||
"name": "email",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"helpMessage": "Cloudflare Email",
|
||||
"value": "test@example.com"
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"helpMessage": "Cloudflare Key",
|
||||
"value": "secretkey"
|
||||
}
|
||||
],
|
||||
"route": "dns_provider_options",
|
||||
"reqParams": null,
|
||||
"restangularized": true,
|
||||
"fromServer": true,
|
||||
"parentResource": null,
|
||||
"restangularCollection": false
|
||||
},
|
||||
"name": "provider_name",
|
||||
"description": "provider_description"
|
||||
}
|
||||
:return:
|
||||
"""
|
||||
return service.create(data)
|
||||
|
||||
|
||||
class DnsProviders(AuthenticatedResource):
|
||||
@validate_schema(None, dns_provider_output_schema)
|
||||
def get(self, dns_provider_id):
|
||||
return service.get_friendly(dns_provider_id)
|
||||
|
||||
@admin_permission.require(http_exception=403)
|
||||
def delete(self, dns_provider_id):
|
||||
service.delete(dns_provider_id)
|
||||
return {'result': True}
|
||||
|
||||
|
||||
class DnsProviderOptions(AuthenticatedResource):
|
||||
""" Defines the 'dns_provider_types' endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(DnsProviderOptions, self).__init__()
|
||||
|
||||
def get(self):
|
||||
return service.get_types()
|
||||
|
||||
|
||||
api.add_resource(DnsProvidersList, '/dns_providers', endpoint='dns_providers')
|
||||
api.add_resource(DnsProviders, '/dns_providers/<int:dns_provider_id>', endpoint='dns_provider')
|
||||
api.add_resource(DnsProviderOptions, '/dns_provider_options', endpoint='dns_provider_options')
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.domains.models
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.domains.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.domains.service
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.domains.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.cli
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.endpoints.models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models need to create an authority within Lemur.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -3,18 +3,17 @@
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer endpoints in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import arrow
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from sqlalchemy import func
|
||||
|
||||
from lemur import database
|
||||
from lemur.common.utils import truthiness
|
||||
from lemur.endpoints.models import Endpoint, Policy, Cipher
|
||||
from lemur.extensions import metrics
|
||||
|
||||
@ -132,19 +131,6 @@ def update(endpoint_id, **kwargs):
|
||||
return endpoint
|
||||
|
||||
|
||||
def rotate_certificate(endpoint, new_cert):
|
||||
"""Rotates a certificate on a given endpoint."""
|
||||
try:
|
||||
endpoint.source.plugin.update_endpoint(endpoint, new_cert)
|
||||
endpoint.certificate = new_cert
|
||||
database.update(endpoint)
|
||||
metrics.send('certificate_rotate_success', 'counter', 1, metric_tags={'endpoint': endpoint.name, 'source': endpoint.source.label})
|
||||
except Exception as e:
|
||||
metrics.send('certificate_rotate_failure', 'counter', 1, metric_tags={'endpoint': endpoint.name})
|
||||
current_app.logger.exception(e)
|
||||
raise e
|
||||
|
||||
|
||||
def render(args):
|
||||
"""
|
||||
Helper that helps us render the REST Api responses.
|
||||
@ -157,7 +143,7 @@ def render(args):
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
if 'active' in filt: # this is really weird but strcmp seems to not work here??
|
||||
query = query.filter(Endpoint.active == terms[1])
|
||||
query = query.filter(Endpoint.active == truthiness(terms[1]))
|
||||
elif 'port' in filt:
|
||||
if terms[1] != 'null': # ng-table adds 'null' if a number is removed
|
||||
query = query.filter(Endpoint.port == terms[1])
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.endpoints.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
.. module: lemur.exceptions
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app
|
||||
@ -34,3 +34,11 @@ class AttrNotFound(LemurException):
|
||||
|
||||
class InvalidConfiguration(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAuthority(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownProvider(Exception):
|
||||
pass
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
.. module: lemur.extensions
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
@ -26,3 +26,6 @@ sentry = Sentry()
|
||||
|
||||
from blinker import Namespace
|
||||
signals = Namespace()
|
||||
|
||||
from flask_cors import CORS
|
||||
cors = CORS()
|
||||
|
@ -4,7 +4,7 @@
|
||||
:synopsis: This module contains all the needed functions to allow
|
||||
the factory app creation.
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
@ -21,7 +21,7 @@ from flask import Flask
|
||||
|
||||
from lemur.certificates.hooks import activate_debug_dump
|
||||
from lemur.common.health import mod as health
|
||||
from lemur.extensions import db, migrate, principal, smtp_mail, metrics, sentry
|
||||
from lemur.extensions import db, migrate, principal, smtp_mail, metrics, sentry, cors
|
||||
|
||||
|
||||
DEFAULT_BLUEPRINTS = (
|
||||
@ -125,6 +125,10 @@ def configure_extensions(app):
|
||||
metrics.init_app(app)
|
||||
sentry.init_app(app)
|
||||
|
||||
if app.config['CORS']:
|
||||
app.config['CORS_HEADERS'] = 'Content-Type'
|
||||
cors.init_app(app, resources=r'/api/*', headers='Content-Type', origin='*', supports_credentials=True)
|
||||
|
||||
|
||||
def configure_blueprints(app, blueprints):
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.logs.models
|
||||
:platform: unix
|
||||
:synopsis: This module contains all of the models related private key audit log.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.logs.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the services level functions used to
|
||||
administer logs in Lemur
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.logs.views
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -21,6 +21,7 @@ from lemur.reporting.cli import manager as report_manager
|
||||
from lemur.endpoints.cli import manager as endpoint_manager
|
||||
from lemur.certificates.cli import manager as certificate_manager
|
||||
from lemur.notifications.cli import manager as notification_manager
|
||||
from lemur.pending_certificates.cli import manager as pending_certificate_manager
|
||||
|
||||
from lemur import database
|
||||
from lemur.users import service as user_service
|
||||
@ -44,6 +45,7 @@ from lemur.sources.models import Source # noqa
|
||||
from lemur.logs.models import Log # noqa
|
||||
from lemur.endpoints.models import Endpoint # noqa
|
||||
from lemur.policies.models import RotationPolicy # noqa
|
||||
from lemur.pending_certificates.models import PendingCertificate # noqa
|
||||
|
||||
|
||||
manager = Manager(create_app)
|
||||
@ -106,6 +108,9 @@ LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
|
||||
# Authentication Providers
|
||||
ACTIVE_PROVIDERS = []
|
||||
|
||||
# Metrics Providers
|
||||
METRIC_PROVIDERS = []
|
||||
|
||||
# Logging
|
||||
|
||||
LOG_LEVEL = "DEBUG"
|
||||
@ -203,16 +208,16 @@ class InitializeApp(Command):
|
||||
if operator_role:
|
||||
sys.stdout.write("[-] Operator role already created, skipping...!\n")
|
||||
else:
|
||||
# we create an admin role
|
||||
# we create an operator role
|
||||
operator_role = role_service.create('operator', description='This is the Lemur operator role.')
|
||||
sys.stdout.write("[+] Created 'operator' role\n")
|
||||
|
||||
read_only_role = role_service.get_by_name('read-only')
|
||||
|
||||
if read_only_role:
|
||||
sys.stdout.write("[-] Operator role already created, skipping...!\n")
|
||||
sys.stdout.write("[-] Read only role already created, skipping...!\n")
|
||||
else:
|
||||
# we create an admin role
|
||||
# we create an read only role
|
||||
read_only_role = role_service.create('read-only', description='This is the Lemur read only role.')
|
||||
sys.stdout.write("[+] Created 'read-only' role\n")
|
||||
|
||||
@ -232,9 +237,6 @@ class InitializeApp(Command):
|
||||
else:
|
||||
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
|
||||
|
||||
sys.stdout.write("[+] Creating expiration email notifications!\n")
|
||||
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
|
||||
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
|
||||
sys.stdout.write(
|
||||
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
|
||||
@ -244,14 +246,21 @@ class InitializeApp(Command):
|
||||
)
|
||||
|
||||
recipients = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
||||
sys.stdout.write("[+] Creating expiration email notifications!\n")
|
||||
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format(recipients))
|
||||
notification_service.create_default_expiration_notifications("DEFAULT_SECURITY", recipients=recipients)
|
||||
|
||||
days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 30)
|
||||
sys.stdout.write("[+] Creating default certificate rotation policy of {days} days before issuance.\n".format(
|
||||
days=days
|
||||
))
|
||||
_DEFAULT_ROTATION_INTERVAL = 'default'
|
||||
default_rotation_interval = policy_service.get_by_name(_DEFAULT_ROTATION_INTERVAL)
|
||||
|
||||
if default_rotation_interval:
|
||||
sys.stdout.write("[-] Default rotation interval policy already created, skipping...!\n")
|
||||
else:
|
||||
days = current_app.config.get("LEMUR_DEFAULT_ROTATION_INTERVAL", 30)
|
||||
sys.stdout.write("[+] Creating default certificate rotation policy of {days} days before issuance.\n".format(
|
||||
days=days))
|
||||
policy_service.create(days=days, name=_DEFAULT_ROTATION_INTERVAL)
|
||||
|
||||
policy_service.create(days=days, name='default')
|
||||
sys.stdout.write("[/] Done!\n")
|
||||
|
||||
|
||||
@ -513,19 +522,6 @@ def publish_verisign_units():
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
@manager.command
|
||||
def publish_unapproved_verisign_certificates():
|
||||
"""
|
||||
Query the Verisign for any certificates that need to be approved.
|
||||
:return:
|
||||
"""
|
||||
from lemur.plugins import plugins
|
||||
from lemur.extensions import metrics
|
||||
v = plugins.get('verisign-issuer')
|
||||
certs = v.get_pending_certificates()
|
||||
metrics.send('pending_certificates', 'gauge', certs)
|
||||
|
||||
|
||||
def main():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1', threaded=True))
|
||||
@ -542,6 +538,7 @@ def main():
|
||||
manager.add_command("endpoint", endpoint_manager)
|
||||
manager.add_command("report", report_manager)
|
||||
manager.add_command("policy", policy_manager)
|
||||
manager.add_command("pending_certs", pending_certificate_manager)
|
||||
manager.run()
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
.. module: lemur.metrics
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app
|
||||
|
105
lemur/migrations/versions/3adfdd6598df_.py
Normal file
105
lemur/migrations/versions/3adfdd6598df_.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Create tables and columns for the acme issuer.
|
||||
|
||||
Revision ID: 3adfdd6598df
|
||||
Revises: 556ceb3e3c3e
|
||||
Create Date: 2018-04-10 13:25:47.007556
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3adfdd6598df'
|
||||
down_revision = '556ceb3e3c3e'
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
from sqlalchemy_utils import ArrowType
|
||||
|
||||
from lemur.utils import Vault
|
||||
|
||||
|
||||
def upgrade():
|
||||
# create provider table
|
||||
print("Creating dns_providers table")
|
||||
op.create_table(
|
||||
'dns_providers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=256), nullable=True),
|
||||
sa.Column('description', sa.String(length=1024), nullable=True),
|
||||
sa.Column('provider_type', sa.String(length=256), nullable=True),
|
||||
sa.Column('credentials', Vault(), nullable=True),
|
||||
sa.Column('api_endpoint', sa.String(length=256), nullable=True),
|
||||
sa.Column('date_created', ArrowType(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('status', sa.String(length=128), nullable=True),
|
||||
sa.Column('options', JSON),
|
||||
sa.Column('domains', sa.JSON(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
print("Adding dns_provider_id column to certificates")
|
||||
op.add_column('certificates', sa.Column('dns_provider_id', sa.Integer(), nullable=True))
|
||||
print("Adding dns_provider_id column to pending_certs")
|
||||
op.add_column('pending_certs', sa.Column('dns_provider_id', sa.Integer(), nullable=True))
|
||||
print("Adding options column to pending_certs")
|
||||
op.add_column('pending_certs', sa.Column('options', JSON))
|
||||
|
||||
print("Creating pending_dns_authorizations table")
|
||||
op.create_table(
|
||||
'pending_dns_authorizations',
|
||||
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
|
||||
sa.Column('account_number', sa.String(length=128), nullable=True),
|
||||
sa.Column('domains', JSON, nullable=True),
|
||||
sa.Column('dns_provider_type', sa.String(length=128), nullable=True),
|
||||
sa.Column('options', JSON, nullable=True),
|
||||
)
|
||||
|
||||
print("Creating certificates_dns_providers_fk foreign key")
|
||||
op.create_foreign_key('certificates_dns_providers_fk', 'certificates', 'dns_providers', ['dns_provider_id'], ['id'],
|
||||
ondelete='cascade')
|
||||
|
||||
print("Altering column types in the api_keys table")
|
||||
op.alter_column('api_keys', 'issued_at',
|
||||
existing_type=sa.BIGINT(),
|
||||
nullable=True)
|
||||
op.alter_column('api_keys', 'revoked',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True)
|
||||
op.alter_column('api_keys', 'ttl',
|
||||
existing_type=sa.BIGINT(),
|
||||
nullable=True)
|
||||
op.alter_column('api_keys', 'user_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=True)
|
||||
|
||||
print("Creating dns_providers_id foreign key on pending_certs table")
|
||||
op.create_foreign_key(None, 'pending_certs', 'dns_providers', ['dns_provider_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
def downgrade():
|
||||
print("Removing dns_providers_id foreign key on pending_certs table")
|
||||
op.drop_constraint(None, 'pending_certs', type_='foreignkey')
|
||||
print("Reverting column types in the api_keys table")
|
||||
op.alter_column('api_keys', 'user_id',
|
||||
existing_type=sa.INTEGER(),
|
||||
nullable=False)
|
||||
op.alter_column('api_keys', 'ttl',
|
||||
existing_type=sa.BIGINT(),
|
||||
nullable=False)
|
||||
op.alter_column('api_keys', 'revoked',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False)
|
||||
op.alter_column('api_keys', 'issued_at',
|
||||
existing_type=sa.BIGINT(),
|
||||
nullable=False)
|
||||
print("Reverting certificates_dns_providers_fk foreign key")
|
||||
op.drop_constraint('certificates_dns_providers_fk', 'certificates', type_='foreignkey')
|
||||
|
||||
print("Dropping pending_dns_authorizations table")
|
||||
op.drop_table('pending_dns_authorizations')
|
||||
print("Undoing modifications to pending_certs table")
|
||||
op.drop_column('pending_certs', 'options')
|
||||
op.drop_column('pending_certs', 'dns_provider_id')
|
||||
print("Undoing modifications to certificates table")
|
||||
op.drop_column('certificates', 'dns_provider_id')
|
||||
|
||||
print("Deleting dns_providers table")
|
||||
op.drop_table('dns_providers')
|
28
lemur/migrations/versions/449c3d5c7299_.py
Normal file
28
lemur/migrations/versions/449c3d5c7299_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Add unique constraint to certificate_notification_associations table.
|
||||
|
||||
Revision ID: 449c3d5c7299
|
||||
Revises: 5770674184de
|
||||
Create Date: 2018-02-24 22:51:35.369229
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '449c3d5c7299'
|
||||
down_revision = '5770674184de'
|
||||
|
||||
from alembic import op
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
CONSTRAINT_NAME = "uq_dest_not_ids"
|
||||
TABLE = "certificate_notification_associations"
|
||||
COLUMNS = ["notification_id", "certificate_id"]
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_unique_constraint(CONSTRAINT_NAME, TABLE, COLUMNS)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_constraint(CONSTRAINT_NAME, TABLE)
|
99
lemur/migrations/versions/556ceb3e3c3e_.py
Normal file
99
lemur/migrations/versions/556ceb3e3c3e_.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""Add Pending Certificates models and relations
|
||||
|
||||
Revision ID: 556ceb3e3c3e
|
||||
Revises: 47baffaae1a7
|
||||
Create Date: 2018-01-05 01:18:45.571595
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '556ceb3e3c3e'
|
||||
down_revision = '449c3d5c7299'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from lemur.utils import Vault
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy_utils import ArrowType
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('pending_certs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('external_id', sa.String(length=128), nullable=True),
|
||||
sa.Column('owner', sa.String(length=128), nullable=False),
|
||||
sa.Column('name', sa.String(length=256), nullable=True),
|
||||
sa.Column('description', sa.String(length=1024), nullable=True),
|
||||
sa.Column('notify', sa.Boolean(), nullable=True),
|
||||
sa.Column('number_attempts', sa.Integer(), nullable=True),
|
||||
sa.Column('rename', sa.Boolean(), nullable=True),
|
||||
sa.Column('cn', sa.String(length=128), nullable=True),
|
||||
sa.Column('csr', sa.Text(), nullable=False),
|
||||
sa.Column('chain', sa.Text(), nullable=True),
|
||||
sa.Column('private_key', Vault(), nullable=True),
|
||||
sa.Column('date_created', ArrowType(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('status', sa.String(length=128), nullable=True),
|
||||
sa.Column('rotation', sa.Boolean(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('authority_id', sa.Integer(), nullable=True),
|
||||
sa.Column('root_authority_id', sa.Integer(), nullable=True),
|
||||
sa.Column('rotation_policy_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['authority_id'], ['authorities.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['root_authority_id'], ['authorities.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['rotation_policy_id'], ['rotation_policies.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('pending_cert_destination_associations',
|
||||
sa.Column('destination_id', sa.Integer(), nullable=True),
|
||||
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['destination_id'], ['destinations.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
|
||||
)
|
||||
op.create_index('pending_cert_destination_associations_ix', 'pending_cert_destination_associations', ['destination_id', 'pending_cert_id'], unique=False)
|
||||
op.create_table('pending_cert_notification_associations',
|
||||
sa.Column('notification_id', sa.Integer(), nullable=True),
|
||||
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade')
|
||||
)
|
||||
op.create_index('pending_cert_notification_associations_ix', 'pending_cert_notification_associations', ['notification_id', 'pending_cert_id'], unique=False)
|
||||
op.create_table('pending_cert_replacement_associations',
|
||||
sa.Column('replaced_certificate_id', sa.Integer(), nullable=True),
|
||||
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['replaced_certificate_id'], ['certificates.id'], ondelete='cascade')
|
||||
)
|
||||
op.create_index('pending_cert_replacement_associations_ix', 'pending_cert_replacement_associations', ['replaced_certificate_id', 'pending_cert_id'], unique=False)
|
||||
op.create_table('pending_cert_role_associations',
|
||||
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||
)
|
||||
op.create_index('pending_cert_role_associations_ix', 'pending_cert_role_associations', ['pending_cert_id', 'role_id'], unique=False)
|
||||
op.create_table('pending_cert_source_associations',
|
||||
sa.Column('source_id', sa.Integer(), nullable=True),
|
||||
sa.Column('pending_cert_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['pending_cert_id'], ['pending_certs.id'], ondelete='cascade'),
|
||||
sa.ForeignKeyConstraint(['source_id'], ['sources.id'], ondelete='cascade')
|
||||
)
|
||||
op.create_index('pending_cert_source_associations_ix', 'pending_cert_source_associations', ['source_id', 'pending_cert_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('pending_cert_source_associations_ix', table_name='pending_cert_source_associations')
|
||||
op.drop_table('pending_cert_source_associations')
|
||||
op.drop_index('pending_cert_role_associations_ix', table_name='pending_cert_role_associations')
|
||||
op.drop_table('pending_cert_role_associations')
|
||||
op.drop_index('pending_cert_replacement_associations_ix', table_name='pending_cert_replacement_associations')
|
||||
op.drop_table('pending_cert_replacement_associations')
|
||||
op.drop_index('pending_cert_notification_associations_ix', table_name='pending_cert_notification_associations')
|
||||
op.drop_table('pending_cert_notification_associations')
|
||||
op.drop_index('pending_cert_destination_associations_ix', table_name='pending_cert_destination_associations')
|
||||
op.drop_table('pending_cert_destination_associations')
|
||||
op.drop_table('pending_certs')
|
||||
# ### end Alembic commands ###
|
44
lemur/migrations/versions/5770674184de_.py
Normal file
44
lemur/migrations/versions/5770674184de_.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Remove duplicates from certificate_notification_associations.
|
||||
|
||||
Revision ID: 5770674184de
|
||||
Revises: ce547319f7be
|
||||
Create Date: 2018-02-23 15:27:30.335435
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5770674184de'
|
||||
down_revision = 'ce547319f7be'
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from lemur.models import certificate_notification_associations
|
||||
|
||||
db = SQLAlchemy()
|
||||
session = db.session()
|
||||
|
||||
|
||||
def upgrade():
|
||||
print("Querying for all entries in certificate_notification_associations.")
|
||||
# Query for all entries in table
|
||||
results = session.query(certificate_notification_associations).with_entities(
|
||||
certificate_notification_associations.c.certificate_id,
|
||||
certificate_notification_associations.c.notification_id,
|
||||
certificate_notification_associations.c.id,
|
||||
)
|
||||
|
||||
seen = {}
|
||||
# Iterate through all entries and mark as seen for each certificate_id and notification_id pair
|
||||
for x in results:
|
||||
# If we've seen a pair already, delete the duplicates
|
||||
if seen.get("{}-{}".format(x.certificate_id, x.notification_id)):
|
||||
print("Deleting duplicate: {}".format(x))
|
||||
d = session.query(certificate_notification_associations).filter(certificate_notification_associations.c.id==x.id)
|
||||
d.delete(synchronize_session=False)
|
||||
seen["{}-{}".format(x.certificate_id, x.notification_id)] = True
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
|
||||
|
||||
def downgrade():
|
||||
# No way to downgrade this
|
||||
pass
|
36
lemur/migrations/versions/ce547319f7be_.py
Normal file
36
lemur/migrations/versions/ce547319f7be_.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Add id column to certificate_notification_associations.
|
||||
|
||||
Revision ID: ce547319f7be
|
||||
Revises: 5bc47fa7cac4
|
||||
Create Date: 2018-02-23 11:00:02.150561
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ce547319f7be'
|
||||
down_revision = '5bc47fa7cac4'
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
TABLE = "certificate_notification_associations"
|
||||
|
||||
|
||||
def upgrade():
|
||||
print("Adding id column")
|
||||
op.add_column(
|
||||
TABLE,
|
||||
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True)
|
||||
)
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
|
||||
def downgrade():
|
||||
op.drop_column(TABLE, "id")
|
||||
db.session.commit()
|
||||
db.session.flush()
|
@ -4,11 +4,11 @@
|
||||
:synopsis: This module contains all of the associative tables
|
||||
that help define the many to many relationships established in Lemur
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Index
|
||||
from sqlalchemy import Column, Integer, ForeignKey, Index, UniqueConstraint
|
||||
|
||||
from lemur.database import db
|
||||
|
||||
@ -41,7 +41,9 @@ certificate_notification_associations = db.Table('certificate_notification_assoc
|
||||
Column('notification_id', Integer,
|
||||
ForeignKey('notifications.id', ondelete='cascade')),
|
||||
Column('certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
ForeignKey('certificates.id', ondelete='cascade')),
|
||||
Column('id', Integer, primary_key=True, autoincrement=True),
|
||||
UniqueConstraint('notification_id', 'certificate_id', name='uq_dest_not_ids')
|
||||
)
|
||||
|
||||
Index('certificate_notification_associations_ix', certificate_notification_associations.c.notification_id, certificate_notification_associations.c.certificate_id)
|
||||
@ -53,7 +55,7 @@ certificate_replacement_associations = db.Table('certificate_replacement_associa
|
||||
ForeignKey('certificates.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.replaced_certificate_id, certificate_replacement_associations.c.certificate_id)
|
||||
Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.replaced_certificate_id, certificate_replacement_associations.c.certificate_id, unique=True)
|
||||
|
||||
roles_authorities = db.Table('roles_authorities',
|
||||
Column('authority_id', Integer, ForeignKey('authorities.id')),
|
||||
@ -83,3 +85,48 @@ policies_ciphers = db.Table('policies_ciphers',
|
||||
Column('policy_id', Integer, ForeignKey('policy.id')))
|
||||
|
||||
Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id)
|
||||
|
||||
|
||||
pending_cert_destination_associations = db.Table('pending_cert_destination_associations',
|
||||
Column('destination_id', Integer,
|
||||
ForeignKey('destinations.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_destination_associations_ix', pending_cert_destination_associations.c.destination_id, pending_cert_destination_associations.c.pending_cert_id)
|
||||
|
||||
|
||||
pending_cert_notification_associations = db.Table('pending_cert_notification_associations',
|
||||
Column('notification_id', Integer,
|
||||
ForeignKey('notifications.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_notification_associations_ix', pending_cert_notification_associations.c.notification_id, pending_cert_notification_associations.c.pending_cert_id)
|
||||
|
||||
pending_cert_source_associations = db.Table('pending_cert_source_associations',
|
||||
Column('source_id', Integer,
|
||||
ForeignKey('sources.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_source_associations_ix', pending_cert_source_associations.c.source_id, pending_cert_source_associations.c.pending_cert_id)
|
||||
|
||||
pending_cert_replacement_associations = db.Table('pending_cert_replacement_associations',
|
||||
Column('replaced_certificate_id', Integer,
|
||||
ForeignKey('certificates.id', ondelete='cascade')),
|
||||
Column('pending_cert_id', Integer,
|
||||
ForeignKey('pending_certs.id', ondelete='cascade'))
|
||||
)
|
||||
|
||||
Index('pending_cert_replacement_associations_ix', pending_cert_replacement_associations.c.replaced_certificate_id, pending_cert_replacement_associations.c.pending_cert_id)
|
||||
|
||||
pending_cert_role_associations = db.Table('pending_cert_role_associations',
|
||||
Column('pending_cert_id', Integer, ForeignKey('pending_certs.id')),
|
||||
Column('role_id', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
Index('pending_cert_role_associations_ix', pending_cert_role_associations.c.pending_cert_id, pending_cert_role_associations.c.role_id)
|
||||
|
@ -1,12 +1,14 @@
|
||||
"""
|
||||
.. module: lemur.notifications.cli
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask_script import Manager
|
||||
|
||||
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||
from lemur.extensions import sentry, metrics
|
||||
from lemur.notifications.messaging import send_expiration_notifications
|
||||
|
||||
manager = Manager(usage="Handles notification related tasks.")
|
||||
@ -25,11 +27,18 @@ def expirations(exclude):
|
||||
|
||||
:return:
|
||||
"""
|
||||
print("Starting to notify subscribers about expiring certificates!")
|
||||
success, failed = send_expiration_notifications(exclude)
|
||||
print(
|
||||
"Finished notifying subscribers about expiring certificates! Sent: {success} Failed: {failed}".format(
|
||||
success=success,
|
||||
failed=failed
|
||||
status = FAILURE_METRIC_STATUS
|
||||
try:
|
||||
print("Starting to notify subscribers about expiring certificates!")
|
||||
success, failed = send_expiration_notifications(exclude)
|
||||
print(
|
||||
"Finished notifying subscribers about expiring certificates! Sent: {success} Failed: {failed}".format(
|
||||
success=success,
|
||||
failed=failed
|
||||
)
|
||||
)
|
||||
)
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
|
||||
metrics.send('expiration_notification_job', 'counter', 1, metric_tags={'status': status})
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.notifications.messaging
|
||||
:platform: Unix
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
@ -17,8 +17,9 @@ from flask import current_app
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
from lemur import database, metrics
|
||||
from lemur.extensions import sentry
|
||||
from lemur import database
|
||||
from lemur.constants import FAILURE_METRIC_STATUS, SUCCESS_METRIC_STATUS
|
||||
from lemur.extensions import metrics, sentry
|
||||
from lemur.common.utils import windowed_query
|
||||
|
||||
from lemur.certificates.schemas import certificate_notification_output_schema
|
||||
@ -94,14 +95,17 @@ def send_notification(event_type, data, targets, notification):
|
||||
:param notification:
|
||||
:return:
|
||||
"""
|
||||
status = FAILURE_METRIC_STATUS
|
||||
try:
|
||||
notification.plugin.send(event_type, data, targets, notification.options)
|
||||
metrics.send('{0}_notification_sent'.format(event_type), 'counter', 1)
|
||||
return True
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
metrics.send('{0}_notification_failure'.format(event_type), 'counter', 1)
|
||||
current_app.logger.exception(e)
|
||||
|
||||
metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': event_type})
|
||||
|
||||
if status == SUCCESS_METRIC_STATUS:
|
||||
return True
|
||||
|
||||
|
||||
def send_expiration_notifications(exclude):
|
||||
@ -147,8 +151,10 @@ def send_rotation_notification(certificate, notification_plugin=None):
|
||||
rotated.
|
||||
|
||||
:param certificate:
|
||||
:param notification_plugin:
|
||||
:return:
|
||||
"""
|
||||
status = FAILURE_METRIC_STATUS
|
||||
if not notification_plugin:
|
||||
notification_plugin = plugins.get(current_app.config.get('LEMUR_DEFAULT_NOTIFICATION_PLUGIN'))
|
||||
|
||||
@ -156,12 +162,14 @@ def send_rotation_notification(certificate, notification_plugin=None):
|
||||
|
||||
try:
|
||||
notification_plugin.send('rotation', data, [data['owner']])
|
||||
metrics.send('rotation_notification_sent', 'counter', 1)
|
||||
return True
|
||||
status = SUCCESS_METRIC_STATUS
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
metrics.send('rotation_notification_failure', 'counter', 1)
|
||||
current_app.logger.exception(e)
|
||||
|
||||
metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': 'rotation'})
|
||||
|
||||
if status == SUCCESS_METRIC_STATUS:
|
||||
return True
|
||||
|
||||
|
||||
def needs_notification(certificate):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.notifications.models
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
@ -11,7 +11,8 @@ from sqlalchemy_utils import JSONType
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.models import certificate_notification_associations
|
||||
from lemur.models import certificate_notification_associations, \
|
||||
pending_cert_notification_associations
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
@ -29,6 +30,13 @@ class Notification(db.Model):
|
||||
backref="notification",
|
||||
cascade='all,delete'
|
||||
)
|
||||
pending_certificates = relationship(
|
||||
"PendingCertificate",
|
||||
secondary=pending_cert_notification_associations,
|
||||
passive_deletes=True,
|
||||
backref="notification",
|
||||
cascade='all,delete'
|
||||
)
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.notifications.schemas
|
||||
:platform: unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.notifications.service
|
||||
:platform: Unix
|
||||
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
@ -12,6 +12,7 @@ from flask import current_app
|
||||
|
||||
from lemur import database
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.common.utils import truthiness
|
||||
from lemur.notifications.models import Notification
|
||||
|
||||
|
||||
@ -169,10 +170,8 @@ def render(args):
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
if terms[0] == 'active' and terms[1] == 'false':
|
||||
query = query.filter(Notification.active == False) # noqa
|
||||
elif terms[0] == 'active' and terms[1] == 'true':
|
||||
query = query.filter(Notification.active == True) # noqa
|
||||
if terms[0] == 'active':
|
||||
query = query.filter(Notification.active == truthiness(terms[1]))
|
||||
else:
|
||||
query = database.filter(query, Notification, terms)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
.. module: lemur.notifications.views
|
||||
:platform: Unix
|
||||
:synopsis: This module contains all of the accounts view code.
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
|
0
lemur/pending_certificates/__init__.py
Normal file
0
lemur/pending_certificates/__init__.py
Normal file
98
lemur/pending_certificates/cli.py
Normal file
98
lemur/pending_certificates/cli.py
Normal file
@ -0,0 +1,98 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.cli
|
||||
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
|
||||
"""
|
||||
from flask_script import Manager
|
||||
|
||||
from lemur.authorities.service import get as get_authority
|
||||
from lemur.pending_certificates import service as pending_certificate_service
|
||||
from lemur.plugins.base import plugins
|
||||
from lemur.users import service as user_service
|
||||
|
||||
manager = Manager(usage="Handles pending certificate related tasks.")
|
||||
|
||||
|
||||
@manager.option('-i', dest='ids', action='append', help='IDs of pending certificates to fetch')
|
||||
def fetch(ids):
|
||||
"""
|
||||
Attempt to get full certificates for each pending certificate listed.
|
||||
|
||||
Args:
|
||||
ids: a list of ids of PendingCertificates (passed in by manager options when run as CLI)
|
||||
`python manager.py pending_certs fetch -i 123 321 all`
|
||||
"""
|
||||
pending_certs = pending_certificate_service.get_pending_certs(ids)
|
||||
user = user_service.get_by_username('lemur')
|
||||
new = 0
|
||||
failed = 0
|
||||
|
||||
for cert in pending_certs:
|
||||
authority = plugins.get(cert.authority.plugin_name)
|
||||
real_cert = authority.get_ordered_certificate(cert)
|
||||
if real_cert:
|
||||
# If a real certificate was returned from issuer, then create it in Lemur and delete
|
||||
# the pending certificate
|
||||
pending_certificate_service.create_certificate(cert, real_cert, user)
|
||||
pending_certificate_service.delete(cert)
|
||||
# add metrics to metrics extension
|
||||
new += 1
|
||||
else:
|
||||
pending_certificate_service.increment_attempt(cert)
|
||||
failed += 1
|
||||
print(
|
||||
"[+] Certificates: New: {new} Failed: {failed}".format(
|
||||
new=new,
|
||||
failed=failed,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@manager.command
|
||||
def fetch_all_acme():
|
||||
"""
|
||||
Attempt to get full certificates for each pending certificate listed with the acme-issuer. This is more efficient
|
||||
for acme-issued certificates because it will configure all of the DNS challenges prior to resolving any
|
||||
certificates.
|
||||
"""
|
||||
pending_certs = pending_certificate_service.get_pending_certs('all')
|
||||
user = user_service.get_by_username('lemur')
|
||||
new = 0
|
||||
failed = 0
|
||||
wrong_issuer = 0
|
||||
acme_certs = []
|
||||
|
||||
# We only care about certs using the acme-issuer plugin
|
||||
for cert in pending_certs:
|
||||
cert_authority = get_authority(cert.authority_id)
|
||||
if cert_authority.plugin_name == 'acme-issuer':
|
||||
acme_certs.append(cert)
|
||||
else:
|
||||
wrong_issuer += 1
|
||||
|
||||
authority = plugins.get("acme-issuer")
|
||||
resolved_certs = authority.get_ordered_certificates(acme_certs)
|
||||
|
||||
for cert in resolved_certs:
|
||||
real_cert = cert.get("cert")
|
||||
# It's necessary to reload the pending cert due to detached instance: http://sqlalche.me/e/bhk3
|
||||
pending_cert = pending_certificate_service.get(cert.get("pending_cert").id)
|
||||
|
||||
if real_cert:
|
||||
# If a real certificate was returned from issuer, then create it in Lemur and delete
|
||||
# the pending certificate
|
||||
pending_certificate_service.create_certificate(pending_cert, real_cert, user)
|
||||
pending_certificate_service.delete_by_id(pending_cert.id)
|
||||
# add metrics to metrics extension
|
||||
new += 1
|
||||
else:
|
||||
pending_certificate_service.increment_attempt(pending_cert)
|
||||
failed += 1
|
||||
print(
|
||||
"[+] Certificates: New: {new} Failed: {failed} Not using ACME: {wrong_issuer}".format(
|
||||
new=new,
|
||||
failed=failed,
|
||||
wrong_issuer=wrong_issuer
|
||||
)
|
||||
)
|
102
lemur/pending_certificates/models.py
Normal file
102
lemur/pending_certificates/models.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.models
|
||||
Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved.
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
from datetime import datetime as dt
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
|
||||
from sqlalchemy_utils.types.arrow import ArrowType
|
||||
from sqlalchemy_utils import JSONType
|
||||
|
||||
import lemur.common.utils
|
||||
from lemur.certificates.models import get_or_increase_name
|
||||
from lemur.common import defaults
|
||||
from lemur.database import db
|
||||
from lemur.utils import Vault
|
||||
|
||||
from lemur.models import pending_cert_source_associations, \
|
||||
pending_cert_destination_associations, pending_cert_notification_associations, \
|
||||
pending_cert_replacement_associations, pending_cert_role_associations
|
||||
|
||||
|
||||
class PendingCertificate(db.Model):
|
||||
__tablename__ = 'pending_certs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
external_id = Column(String(128))
|
||||
owner = Column(String(128), nullable=False)
|
||||
name = Column(String(256), unique=True)
|
||||
description = Column(String(1024))
|
||||
notify = Column(Boolean, default=True)
|
||||
number_attempts = Column(Integer)
|
||||
rename = Column(Boolean, default=True)
|
||||
|
||||
cn = Column(String(128))
|
||||
csr = Column(Text(), nullable=False)
|
||||
chain = Column(Text())
|
||||
private_key = Column(Vault, nullable=True)
|
||||
|
||||
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)
|
||||
dns_provider_id = Column(Integer, ForeignKey('dns_providers.id', ondelete="CASCADE"))
|
||||
|
||||
status = Column(String(128))
|
||||
|
||||
rotation = Column(Boolean, default=False)
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE"))
|
||||
rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id'))
|
||||
|
||||
notifications = relationship('Notification', secondary=pending_cert_notification_associations, backref='pending_cert', passive_deletes=True)
|
||||
destinations = relationship('Destination', secondary=pending_cert_destination_associations, backref='pending_cert', passive_deletes=True)
|
||||
sources = relationship('Source', secondary=pending_cert_source_associations, backref='pending_cert', passive_deletes=True)
|
||||
roles = relationship('Role', secondary=pending_cert_role_associations, backref='pending_cert', passive_deletes=True)
|
||||
replaces = relationship('Certificate',
|
||||
secondary=pending_cert_replacement_associations,
|
||||
backref='pending_cert',
|
||||
passive_deletes=True)
|
||||
options = Column(JSONType)
|
||||
|
||||
rotation_policy = relationship("RotationPolicy")
|
||||
|
||||
sensitive_fields = ('private_key',)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.csr = kwargs.get('csr')
|
||||
self.private_key = kwargs.get('private_key', "")
|
||||
if self.private_key:
|
||||
# If the request does not send private key, the key exists but the value is None
|
||||
self.private_key = self.private_key.strip()
|
||||
self.external_id = kwargs.get('external_id')
|
||||
|
||||
# when destinations are appended they require a valid name.
|
||||
if kwargs.get('name'):
|
||||
self.name = get_or_increase_name(defaults.text_to_slug(kwargs['name']), 0)
|
||||
self.rename = False
|
||||
else:
|
||||
# TODO: Fix auto-generated name, it should be renamed on creation
|
||||
self.name = get_or_increase_name(
|
||||
defaults.certificate_name(kwargs['common_name'], kwargs['authority'].name,
|
||||
dt.now(), dt.now(), False), self.external_id)
|
||||
self.rename = True
|
||||
|
||||
self.cn = defaults.common_name(lemur.common.utils.parse_csr(self.csr))
|
||||
self.owner = kwargs['owner']
|
||||
self.number_attempts = 0
|
||||
|
||||
if kwargs.get('chain'):
|
||||
self.chain = kwargs['chain'].strip()
|
||||
|
||||
self.notify = kwargs.get('notify', True)
|
||||
self.destinations = kwargs.get('destinations', [])
|
||||
self.notifications = kwargs.get('notifications', [])
|
||||
self.description = kwargs.get('description')
|
||||
self.roles = list(set(kwargs.get('roles', [])))
|
||||
self.replaces = kwargs.get('replaces', [])
|
||||
self.rotation = kwargs.get('rotation')
|
||||
self.rotation_policy = kwargs.get('rotation_policy')
|
||||
try:
|
||||
self.dns_provider_id = kwargs.get('dns_provider').id
|
||||
except (AttributeError, KeyError, TypeError, Exception):
|
||||
pass
|
101
lemur/pending_certificates/schemas.py
Normal file
101
lemur/pending_certificates/schemas.py
Normal file
@ -0,0 +1,101 @@
|
||||
from marshmallow import fields, post_load
|
||||
|
||||
from lemur.schemas import (
|
||||
AssociatedCertificateSchema,
|
||||
AssociatedDestinationSchema,
|
||||
AssociatedNotificationSchema,
|
||||
AssociatedRoleSchema,
|
||||
EndpointNestedOutputSchema,
|
||||
ExtensionSchema
|
||||
)
|
||||
|
||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||
from lemur.certificates.schemas import CertificateNestedOutputSchema
|
||||
from lemur.destinations.schemas import DestinationNestedOutputSchema
|
||||
from lemur.domains.schemas import DomainNestedOutputSchema
|
||||
from lemur.notifications.schemas import NotificationNestedOutputSchema
|
||||
from lemur.roles.schemas import RoleNestedOutputSchema
|
||||
from lemur.policies.schemas import RotationPolicyNestedOutputSchema
|
||||
|
||||
from lemur.notifications import service as notification_service
|
||||
|
||||
|
||||
class PendingCertificateSchema(LemurInputSchema):
|
||||
owner = fields.Email(required=True)
|
||||
description = fields.String(missing='', allow_none=True)
|
||||
|
||||
|
||||
class PendingCertificateOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
external_id = fields.String()
|
||||
csr = fields.String()
|
||||
chain = fields.String()
|
||||
deleted = fields.Boolean(default=False)
|
||||
description = fields.String()
|
||||
issuer = fields.String()
|
||||
name = fields.String()
|
||||
number_attempts = fields.Integer()
|
||||
date_created = fields.Date()
|
||||
|
||||
rotation = fields.Boolean()
|
||||
|
||||
# Note aliasing is the first step in deprecating these fields.
|
||||
notify = fields.Boolean()
|
||||
active = fields.Boolean(attribute='notify')
|
||||
|
||||
cn = fields.String()
|
||||
common_name = fields.String(attribute='cn')
|
||||
|
||||
owner = fields.Email()
|
||||
|
||||
status = fields.String()
|
||||
user = fields.Nested(UserNestedOutputSchema)
|
||||
|
||||
extensions = fields.Nested(ExtensionSchema)
|
||||
|
||||
# associated objects
|
||||
domains = fields.Nested(DomainNestedOutputSchema, many=True)
|
||||
destinations = fields.Nested(DestinationNestedOutputSchema, many=True)
|
||||
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')
|
||||
rotation_policy = fields.Nested(RotationPolicyNestedOutputSchema)
|
||||
|
||||
|
||||
class PendingCertificateEditInputSchema(PendingCertificateSchema):
|
||||
owner = fields.String()
|
||||
|
||||
notify = fields.Boolean()
|
||||
rotation = fields.Boolean()
|
||||
|
||||
destinations = fields.Nested(AssociatedDestinationSchema, missing=[], many=True)
|
||||
notifications = fields.Nested(AssociatedNotificationSchema, missing=[], many=True)
|
||||
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
|
||||
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
|
||||
|
||||
@post_load
|
||||
def enforce_notifications(self, data):
|
||||
"""
|
||||
Ensures that when an owner changes, default notifications are added for the new owner.
|
||||
Old owner notifications are retained unless explicitly removed.
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
if data['owner']:
|
||||
notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper())
|
||||
data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']])
|
||||
return data
|
||||
|
||||
|
||||
class PendingCertificateCancelSchema(LemurInputSchema):
|
||||
note = fields.String()
|
||||
|
||||
|
||||
pending_certificate_output_schema = PendingCertificateOutputSchema()
|
||||
pending_certificate_edit_input_schema = PendingCertificateEditInputSchema()
|
||||
pending_certificate_cancel_schema = PendingCertificateCancelSchema()
|
224
lemur/pending_certificates/service.py
Normal file
224
lemur/pending_certificates/service.py
Normal file
@ -0,0 +1,224 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.service
|
||||
Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved.
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
import arrow
|
||||
|
||||
from sqlalchemy import or_, cast, Integer
|
||||
|
||||
from lemur import database
|
||||
from lemur.common.utils import truthiness
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
from lemur.roles.models import Role
|
||||
from lemur.domains.models import Domain
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.pending_certificates.models import PendingCertificate
|
||||
|
||||
from lemur.certificates import service as certificate_service
|
||||
from lemur.users import service as user_service
|
||||
|
||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||
|
||||
|
||||
def get(pending_cert_id):
|
||||
"""
|
||||
Retrieve pending certificate by ID
|
||||
"""
|
||||
return database.get(PendingCertificate, pending_cert_id)
|
||||
|
||||
|
||||
def get_by_external_id(issuer, external_id):
|
||||
"""
|
||||
Retrieves a pending certificate by its issuer and external_id
|
||||
Since external_id is not necessarily unique between CAs
|
||||
|
||||
:param issuer:
|
||||
:param external_id:
|
||||
:return: PendingCertificate or None
|
||||
"""
|
||||
if isinstance(external_id, int):
|
||||
external_id = str(external_id)
|
||||
return PendingCertificate.query \
|
||||
.filter(PendingCertificate.authority_id == issuer.id) \
|
||||
.filter(PendingCertificate.external_id == external_id) \
|
||||
.one_or_none()
|
||||
|
||||
|
||||
def get_by_name(pending_cert_name):
|
||||
"""
|
||||
Retrieve pending certificate by name
|
||||
"""
|
||||
return database.get(PendingCertificate, pending_cert_name, field='name')
|
||||
|
||||
|
||||
def delete(pending_certificate):
|
||||
database.delete(pending_certificate)
|
||||
|
||||
|
||||
def delete_by_id(id):
|
||||
database.delete(get(id))
|
||||
|
||||
|
||||
def get_pending_certs(pending_ids):
|
||||
"""
|
||||
Retrieve a list of pending certs given a list of ids
|
||||
Filters out non-existing pending certs
|
||||
"""
|
||||
pending_certs = []
|
||||
if 'all' in pending_ids:
|
||||
query = database.session_query(PendingCertificate)
|
||||
return database.find_all(query, PendingCertificate, {}).all()
|
||||
else:
|
||||
for pending_id in pending_ids:
|
||||
pending_cert = get(pending_id)
|
||||
if pending_cert:
|
||||
pending_certs.append(pending_cert)
|
||||
return pending_certs
|
||||
|
||||
|
||||
def create_certificate(pending_certificate, certificate, user):
|
||||
"""
|
||||
Create and store a certificate with pending certificate's info
|
||||
Args:
|
||||
pending_certificate: PendingCertificate which will populate the certificate
|
||||
certificate: dict from Authority, which contains the body, chain and external id
|
||||
user: User that called this function, used as 'creator' of the certificate if it does
|
||||
not have an owner
|
||||
"""
|
||||
certificate['owner'] = pending_certificate.owner
|
||||
data, errors = CertificateUploadInputSchema().load(certificate)
|
||||
if errors:
|
||||
raise Exception("Unable to create certificate: {reasons}".format(reasons=errors))
|
||||
|
||||
data.update(vars(pending_certificate))
|
||||
# Copy relationships, vars doesn't copy this without explicit fields
|
||||
data['notifications'] = list(pending_certificate.notifications)
|
||||
data['destinations'] = list(pending_certificate.destinations)
|
||||
data['sources'] = list(pending_certificate.sources)
|
||||
data['roles'] = list(pending_certificate.roles)
|
||||
data['replaces'] = list(pending_certificate.replaces)
|
||||
data['rotation_policy'] = pending_certificate.rotation_policy
|
||||
|
||||
# Replace external id and chain with the one fetched from source
|
||||
data['external_id'] = certificate['external_id']
|
||||
data['chain'] = certificate['chain']
|
||||
creator = user_service.get_by_email(pending_certificate.owner)
|
||||
if not creator:
|
||||
# Owner of the pending certificate is not the creator, so use the current user who called
|
||||
# this as the creator (usually lemur)
|
||||
creator = user
|
||||
|
||||
if pending_certificate.rename:
|
||||
# If generating name from certificate, remove the one from pending certificate
|
||||
del data['name']
|
||||
data['creator'] = creator
|
||||
cert = certificate_service.import_certificate(**data)
|
||||
database.update(cert)
|
||||
return cert
|
||||
|
||||
|
||||
def increment_attempt(pending_certificate):
|
||||
"""
|
||||
Increments pending certificate attempt counter and updates it in the database.
|
||||
"""
|
||||
pending_certificate.number_attempts += 1
|
||||
database.update(pending_certificate)
|
||||
return pending_certificate.number_attempts
|
||||
|
||||
|
||||
def update(pending_cert_id, **kwargs):
|
||||
"""
|
||||
Updates a pending certificate. The allowed fields are validated by
|
||||
PendingCertificateEditInputSchema.
|
||||
"""
|
||||
pending_cert = get(pending_cert_id)
|
||||
for key, value in kwargs.items():
|
||||
setattr(pending_cert, key, value)
|
||||
return database.update(pending_cert)
|
||||
|
||||
|
||||
def cancel(pending_certificate, **kwargs):
|
||||
"""
|
||||
Cancel a pending certificate. A check should be done prior to this function to decide to
|
||||
revoke the certificate or just abort cancelling.
|
||||
Args:
|
||||
pending_certificate: PendingCertificate to be cancelled
|
||||
Returns: the pending certificate if successful, raises Exception if there was an issue
|
||||
"""
|
||||
plugin = plugins.get(pending_certificate.authority.plugin_name)
|
||||
plugin.cancel_ordered_certificate(pending_certificate, **kwargs)
|
||||
pending_certificate.status = 'Cancelled'
|
||||
database.update(pending_certificate)
|
||||
return pending_certificate
|
||||
|
||||
|
||||
def render(args):
|
||||
query = database.session_query(PendingCertificate)
|
||||
time_range = args.pop('time_range')
|
||||
destination_id = args.pop('destination_id')
|
||||
notification_id = args.pop('notification_id', None)
|
||||
show = args.pop('show')
|
||||
# owner = args.pop('owner')
|
||||
# creator = args.pop('creator') # TODO we should enabling filtering by owner
|
||||
|
||||
filt = args.pop('filter')
|
||||
|
||||
if filt:
|
||||
terms = filt.split(';')
|
||||
|
||||
if 'issuer' in terms:
|
||||
# we can't rely on issuer being correct in the cert directly so we combine queries
|
||||
sub_query = database.session_query(Authority.id)\
|
||||
.filter(Authority.name.ilike('%{0}%'.format(terms[1])))\
|
||||
.subquery()
|
||||
|
||||
query = query.filter(
|
||||
or_(
|
||||
PendingCertificate.issuer.ilike('%{0}%'.format(terms[1])),
|
||||
PendingCertificate.authority_id.in_(sub_query)
|
||||
)
|
||||
)
|
||||
|
||||
elif 'destination' in terms:
|
||||
query = query.filter(PendingCertificate.destinations.any(Destination.id == terms[1]))
|
||||
elif 'notify' in filt:
|
||||
query = query.filter(PendingCertificate.notify == truthiness(terms[1]))
|
||||
elif 'active' in filt:
|
||||
query = query.filter(PendingCertificate.active == truthiness(terms[1]))
|
||||
elif 'cn' in terms:
|
||||
query = query.filter(
|
||||
or_(
|
||||
PendingCertificate.cn.ilike('%{0}%'.format(terms[1])),
|
||||
PendingCertificate.domains.any(Domain.name.ilike('%{0}%'.format(terms[1])))
|
||||
)
|
||||
)
|
||||
elif 'id' in terms:
|
||||
query = query.filter(PendingCertificate.id == cast(terms[1], Integer))
|
||||
else:
|
||||
query = database.filter(query, PendingCertificate, terms)
|
||||
|
||||
if show:
|
||||
sub_query = database.session_query(Role.name).filter(Role.user_id == args['user'].id).subquery()
|
||||
query = query.filter(
|
||||
or_(
|
||||
PendingCertificate.user_id == args['user'].id,
|
||||
PendingCertificate.owner.in_(sub_query)
|
||||
)
|
||||
)
|
||||
|
||||
if destination_id:
|
||||
query = query.filter(PendingCertificate.destinations.any(Destination.id == destination_id))
|
||||
|
||||
if notification_id:
|
||||
query = query.filter(PendingCertificate.notifications.any(Notification.id == notification_id))
|
||||
|
||||
if time_range:
|
||||
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
|
||||
now = arrow.now().format('YYYY-MM-DD')
|
||||
query = query.filter(PendingCertificate.not_after <= to).filter(PendingCertificate.not_after >= now)
|
||||
|
||||
return database.sort_and_page(query, PendingCertificate, args)
|
424
lemur/pending_certificates/views.py
Normal file
424
lemur/pending_certificates/views.py
Normal file
@ -0,0 +1,424 @@
|
||||
"""
|
||||
.. module: lemur.pending_certificates.views
|
||||
:platform: Unix
|
||||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: James Chuong <jchuong@instartlogic.com>
|
||||
"""
|
||||
from flask import Blueprint, g, make_response, jsonify
|
||||
from flask_restful import Api, reqparse
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
from lemur.auth.permissions import CertificatePermission
|
||||
|
||||
from lemur.common.schema import validate_schema
|
||||
from lemur.common.utils import paginated_parser
|
||||
|
||||
from lemur.pending_certificates import service
|
||||
from lemur.roles import service as role_service
|
||||
|
||||
from lemur.pending_certificates.schemas import (
|
||||
pending_certificate_output_schema,
|
||||
pending_certificate_edit_input_schema,
|
||||
pending_certificate_cancel_schema,
|
||||
)
|
||||
|
||||
mod = Blueprint('pending_certificates', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class PendingCertificatesList(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(PendingCertificatesList, self).__init__()
|
||||
|
||||
@validate_schema(None, pending_certificate_output_schema)
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /pending_certificates
|
||||
|
||||
List of pending certificates
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pending_certificates HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}],
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"rotation": true,
|
||||
"rotationPolicy": {"name": "default"},
|
||||
"replaces": [],
|
||||
"replaced": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
parser = paginated_parser.copy()
|
||||
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
|
||||
parser.add_argument('owner', type=bool, location='args')
|
||||
parser.add_argument('id', type=str, location='args')
|
||||
parser.add_argument('active', type=bool, location='args')
|
||||
parser.add_argument('destinationId', type=int, dest="destination_id", location='args')
|
||||
parser.add_argument('creator', type=str, location='args')
|
||||
parser.add_argument('show', type=str, location='args')
|
||||
args = parser.parse_args()
|
||||
args['user'] = g.user
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class PendingCertificates(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
super(PendingCertificates, self).__init__()
|
||||
|
||||
@validate_schema(None, pending_certificate_output_schema)
|
||||
def get(self, pending_certificate_id):
|
||||
"""
|
||||
.. http:get:: /pending_certificates/1
|
||||
|
||||
One pending certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pending_certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 1,
|
||||
"issuer": "SymantecCorporation",
|
||||
"notBefore": "2016-06-03T00:00:00+00:00",
|
||||
"notAfter": "2018-01-12T23:59:59+00:00",
|
||||
"destinations": [],
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}],
|
||||
"signingAlgorithm": "sha256",
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"rotation": true,
|
||||
"rotationPolicy": {"name": "default"},
|
||||
"replaces": [],
|
||||
"replaced": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"san": null
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
return service.get(pending_certificate_id)
|
||||
|
||||
@validate_schema(pending_certificate_edit_input_schema, pending_certificate_output_schema)
|
||||
def put(self, pending_certificate_id, data=None):
|
||||
"""
|
||||
.. http:put:: /pending_certificates/1
|
||||
|
||||
Update a pending certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /pending certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"owner": "jimbob@example.com",
|
||||
"active": false
|
||||
"notifications": [],
|
||||
"destinations": [],
|
||||
"replacements": []
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"status": null,
|
||||
"cn": "*.test.example.net",
|
||||
"chain": "",
|
||||
"authority": {
|
||||
"active": true,
|
||||
"owner": "secure@example.com",
|
||||
"id": 1,
|
||||
"description": "verisign test authority",
|
||||
"name": "verisign"
|
||||
},
|
||||
"owner": "joe@example.com",
|
||||
"serial": "82311058732025924142789179368889309156",
|
||||
"id": 2288,
|
||||
"issuer": "SymantecCorporation",
|
||||
"destinations": [],
|
||||
"description": null,
|
||||
"deleted": null,
|
||||
"notifications": [{
|
||||
"id": 1
|
||||
}]
|
||||
"user": {
|
||||
"username": "jane",
|
||||
"active": true,
|
||||
"email": "jane@example.com",
|
||||
"id": 2
|
||||
},
|
||||
"active": true,
|
||||
"number_attempts": 1,
|
||||
"csr": "-----BEGIN CERTIFICATE REQUEST-----...",
|
||||
"external_id": 12345,
|
||||
"domains": [{
|
||||
"sensitive": false,
|
||||
"id": 1090,
|
||||
"name": "*.test.example.net"
|
||||
}],
|
||||
"replaces": [],
|
||||
"name": "WILDCARD.test.example.net-SymantecCorporation-20160603-20180112",
|
||||
"roles": [{
|
||||
"id": 464,
|
||||
"description": "This is a google group based role created by Lemur",
|
||||
"name": "joe@example.com"
|
||||
}],
|
||||
"rotation": true,
|
||||
"rotationPolicy": {"name": "default"},
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
|
||||
"""
|
||||
pending_cert = service.get(pending_certificate_id)
|
||||
|
||||
if not pending_cert:
|
||||
return dict(message="Cannot find specified pending certificate"), 404
|
||||
|
||||
# allow creators
|
||||
if g.current_user != pending_cert.user:
|
||||
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
|
||||
for destination in data['destinations']:
|
||||
if destination.plugin.requires_key:
|
||||
if not pending_cert.private_key:
|
||||
return dict(
|
||||
message='Unable to add destination: {0}. Certificate does not have required private key.'.format(
|
||||
destination.label
|
||||
)
|
||||
), 400
|
||||
|
||||
pending_cert = service.update(pending_certificate_id, **data)
|
||||
return pending_cert
|
||||
|
||||
@validate_schema(pending_certificate_cancel_schema, None)
|
||||
def delete(self, pending_certificate_id, data=None):
|
||||
"""
|
||||
.. http:delete:: /pending_certificates/1
|
||||
|
||||
Cancel and delete a pending certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /pending certificates/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
{
|
||||
"note": "Why I am cancelling this order"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 204: no error
|
||||
:statuscode 401: unauthenticated
|
||||
:statuscode 403: unauthorized
|
||||
:statuscode 404: pending certificate id not found
|
||||
:statuscode 500: internal error
|
||||
"""
|
||||
pending_cert = service.get(pending_certificate_id)
|
||||
|
||||
if not pending_cert:
|
||||
return dict(message="Cannot find specified pending certificate"), 404
|
||||
|
||||
# allow creators
|
||||
if g.current_user != pending_cert.user:
|
||||
owner_role = role_service.get_by_name(pending_cert.owner)
|
||||
permission = CertificatePermission(owner_role, [x.name for x in pending_cert.roles])
|
||||
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to update this certificate'), 403
|
||||
|
||||
if service.cancel(pending_cert, **data):
|
||||
service.delete(pending_cert)
|
||||
return('', 204)
|
||||
else:
|
||||
# service.cancel raises exception if there was an issue, but this will ensure something
|
||||
# is relayed to user in case of something unexpected (unsuccessful update somehow).
|
||||
return dict(message="Unexpected error occurred while trying to cancel this certificate"), 500
|
||||
|
||||
|
||||
class PendingCertificatePrivateKey(AuthenticatedResource):
|
||||
def __init__(self):
|
||||
super(PendingCertificatePrivateKey, self).__init__()
|
||||
|
||||
def get(self, pending_certificate_id):
|
||||
"""
|
||||
.. http:get:: /pending_certificates/1/key
|
||||
|
||||
Retrieves the private key for a given pneding certificate
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /pending_certificates/1/key HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"key": "-----BEGIN ..."
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
cert = service.get(pending_certificate_id)
|
||||
if not cert:
|
||||
return dict(message="Cannot find specified pending certificate"), 404
|
||||
|
||||
# allow creators
|
||||
if g.current_user != cert.user:
|
||||
owner_role = role_service.get_by_name(cert.owner)
|
||||
permission = CertificatePermission(owner_role, [x.name for x in cert.roles])
|
||||
|
||||
if not permission.can():
|
||||
return dict(message='You are not authorized to view this key'), 403
|
||||
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
response.headers['cache-control'] = 'private, max-age=0, no-cache, no-store'
|
||||
response.headers['pragma'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
api.add_resource(PendingCertificatesList, '/pending_certificates', endpoint='pending_certificates')
|
||||
api.add_resource(PendingCertificates, '/pending_certificates/<int:pending_certificate_id>', endpoint='pending_certificate')
|
||||
api.add_resource(PendingCertificatePrivateKey, '/pending_certificates/<int:pending_certificate_id>/key', endpoint='privateKeyPendingCertificates')
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.base
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
.. module: lemur.plugins.base.manager
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson (kglisson@netflix.com)
|
||||
@ -34,6 +34,10 @@ class PluginManager(InstanceManager):
|
||||
for plugin in self.all(version=2):
|
||||
if plugin.slug == slug:
|
||||
return plugin
|
||||
current_app.logger.error(
|
||||
"Unable to find slug: {} in self.all version 1: {} or version 2: {}".format(
|
||||
slug, self.all(version=1), self.all(version=2))
|
||||
)
|
||||
raise KeyError(slug)
|
||||
|
||||
def first(self, func_name, *args, **kwargs):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.base.v1
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.bases.destination
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.bases.export
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.bases.issuer
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
@ -24,3 +24,9 @@ class IssuerPlugin(Plugin):
|
||||
|
||||
def revoke_certificate(self, certificate, comments):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ordered_certificate(self, certificate):
|
||||
raise NotImplementedError
|
||||
|
||||
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.bases.metric
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.bases.notification
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
.. module: lemur.plugins.bases.source
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user