diff --git a/AUTHORS b/AUTHORS index f186b025..d15393b1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ -- Kevin Glisson (kglisson@netflix.com) \ No newline at end of file +- Kevin Glisson (kglisson@netflix.com) +- Jeremy Heffner diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..ec344cc4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include setup.py package.json bower.json gulpfile.js README.rst MANIFEST.in LICENSE AUTHORS +recursive-include lemur/plugins/lemur_email/templates * +recursive-include lemur/static * +global-exclude *~ diff --git a/Makefile b/Makefile index dd2c2695..03369342 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,6 @@ develop: update-submodules setup-git npm install pip install "setuptools>=0.9.8" # order matters here, base package must install first - # this is temporary until the version we need is released - pip install -e 'git+https://git@github.com/pyca/cryptography.git#egg=cryptography-1.0.dev1' pip install -e . pip install "file://`pwd`#egg=lemur[dev]" pip install "file://`pwd`#egg=lemur[tests]" diff --git a/lemur/__init__.py b/lemur/__init__.py index 24cb6ef1..79b45241 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -17,7 +17,7 @@ from lemur.domains.views import mod as domains_bp from lemur.destinations.views import mod as destinations_bp from lemur.authorities.views import mod as authorities_bp from lemur.certificates.views import mod as certificates_bp -from lemur.status.views import mod as status_bp +from lemur.defaults.views import mod as defaults_bp from lemur.plugins.views import mod as plugins_bp from lemur.notifications.views import mod as notifications_bp from lemur.sources.views import mod as sources_bp @@ -31,7 +31,7 @@ LEMUR_BLUEPRINTS = ( destinations_bp, authorities_bp, certificates_bp, - status_bp, + defaults_bp, plugins_bp, notifications_bp, sources_bp diff --git a/lemur/auth/permissions.py b/lemur/auth/permissions.py index 13d8f6e1..6cc04cac 100644 --- a/lemur/auth/permissions.py +++ b/lemur/auth/permissions.py @@ -37,7 +37,7 @@ ViewRoleCredentialsNeed = partial(RoleUser, 'roleView') class ViewRoleCredentialsPermission(Permission): def __init__(self, role_id): - need = ViewRoleCredentialsNeed(str(role_id)) + need = ViewRoleCredentialsNeed(role_id) super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin')) diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index 23961ede..5a1f4341 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -22,7 +22,7 @@ from lemur.certificates.models import Certificate from lemur.plugins.base import plugins -def update(authority_id, active=None, roles=None): +def update(authority_id, description=None, owner=None, active=None, roles=None): """ Update a an authority with new values. @@ -37,6 +37,9 @@ def update(authority_id, active=None, roles=None): if active: authority.active = active + + authority.description = description + authority.owner = owner return database.update(authority) diff --git a/lemur/authorities/views.py b/lemur/authorities/views.py index f449a837..43a65fe5 100644 --- a/lemur/authorities/views.py +++ b/lemur/authorities/views.py @@ -20,6 +20,7 @@ from lemur.common.utils import paginated_parser, marshal_items FIELDS = { 'name': fields.String, + 'owner': fields.String, 'description': fields.String, 'options': fields.Raw, 'pluginName': fields.String, @@ -264,7 +265,9 @@ class Authorities(AuthenticatedResource): { "roles": [], - "active": false + "active": false, + "owner": "bob@example.com", + "description": "this is authority1" } **Example response**: @@ -279,12 +282,12 @@ class Authorities(AuthenticatedResource): "id": 1, "name": "authority1", "description": "this is authority1", - "pluginname": null, + "pluginName": null, "chain": "-----begin ...", "body": "-----begin ...", "active": false, - "notbefore": "2015-06-05t17:09:39", - "notafter": "2015-06-10t17:09:39" + "notBefore": "2015-06-05t17:09:39", + "notAfter": "2015-06-10t17:09:39" "options": null } @@ -292,8 +295,10 @@ class Authorities(AuthenticatedResource): :statuscode 200: no error :statuscode 403: unauthenticated """ - self.reqparse.add_argument('roles', type=list, location='json') - self.reqparse.add_argument('active', type=str, location='json') + self.reqparse.add_argument('roles', type=list, default=[], location='json') + self.reqparse.add_argument('active', type=str, location='json', required=True) + self.reqparse.add_argument('owner', type=str, location='json', required=True) + self.reqparse.add_argument('description', type=str, location='json', required=True) args = self.reqparse.parse_args() authority = service.get(authority_id) @@ -315,7 +320,13 @@ class Authorities(AuthenticatedResource): return dict(message="You are not allowed to associate a role which you are not a member of"), 400 if permission.can(): - return service.update(authority_id, active=args['active'], roles=args['roles']) + return service.update( + authority_id, + owner=args['owner'], + description=args['description'], + active=args['active'], + roles=args['roles'] + ) return dict(message="You are not authorized to update this authority"), 403 diff --git a/lemur/certificates/verify.py b/lemur/certificates/verify.py index 1e0febec..79afdf50 100644 --- a/lemur/certificates/verify.py +++ b/lemur/certificates/verify.py @@ -6,14 +6,28 @@ .. moduleauthor:: Kevin Glisson """ import os -import re -import hashlib import requests import subprocess from OpenSSL import crypto +from cryptography import x509 +from cryptography.hazmat.backends import default_backend from flask import current_app +from contextlib import contextmanager +from tempfile import NamedTemporaryFile + + +@contextmanager +def mktempfile(): + with NamedTemporaryFile(delete=False) as f: + name = f.name + + try: + yield name + finally: + os.unlink(name) + def ocsp_verify(cert_path, issuer_chain_path): """ @@ -53,27 +67,18 @@ def crl_verify(cert_path): :return: True if certificate is valid, False otherwise :raise Exception: If certificate does not have CRL """ - s = "(http(s)?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}/\S*?$)" - regex = re.compile(s, re.MULTILINE) + with open(cert_path, 'rt') as c: + cert = x509.load_pem_x509_certificate(c.read(), default_backend()) - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_path, 'rt').read()) - for x in range(x509.get_extension_count()): - ext = x509.get_extension(x) - if ext.get_short_name() == 'crlDistributionPoints': - r = regex.search(ext.get_data()) - points = r.groups() - break - else: - raise Exception("Certificate does not have a CRL distribution point") - - for point in points: - if point: - response = requests.get(point) - crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) - revoked = crl.get_revoked() - for r in revoked: - if x509.get_serial_number() == r.get_serial(): - return + distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value + for p in distribution_points: + point = p.full_name[0].value + response = requests.get(point) + crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) # TODO this should be switched to cryptography when support exists + revoked = crl.get_revoked() + for r in revoked: + if cert.serial == r.get_serial(): + return return True @@ -99,22 +104,6 @@ def verify(cert_path, issuer_chain_path): raise Exception("Failed to verify") -def make_tmp_file(string): - """ - Creates a temporary file for a given string - - :param string: - :return: Full file path to created file - """ - m = hashlib.md5() - m.update(string) - hexdigest = m.hexdigest() - path = os.path.join(os.path.dirname(os.path.abspath(__file__)), hexdigest) - with open(path, 'w') as f: - f.write(string) - return path - - def verify_string(cert_string, issuer_string): """ Verify a certificate given only it's string value @@ -123,13 +112,11 @@ def verify_string(cert_string, issuer_string): :param issuer_string: :return: True if valid, False otherwise """ - cert_path = make_tmp_file(cert_string) - issuer_path = make_tmp_file(issuer_string) - status = verify(cert_path, issuer_path) - remove_tmp_file(cert_path) - remove_tmp_file(issuer_path) + with mktempfile() as cert_tmp: + with open(cert_tmp, 'w') as f: + f.write(cert_string) + with mktempfile() as issuer_tmp: + with open(issuer_tmp, 'w') as f: + f.write(issuer_string) + status = verify(cert_tmp, issuer_tmp) return status - - -def remove_tmp_file(file_path): - os.remove(file_path) diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 834fdff7..112c86ad 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -7,7 +7,7 @@ """ from builtins import str -from flask import Blueprint, current_app, make_response, jsonify +from flask import Blueprint, make_response, jsonify from flask.ext.restful import reqparse, Api, fields from cryptography import x509 @@ -668,58 +668,9 @@ class NotificationCertificatesList(AuthenticatedResource): return service.render(args) -class CertificatesDefaults(AuthenticatedResource): - """ Defineds the 'certificates' defaults endpoint """ - def __init__(self): - super(CertificatesDefaults) - - def get(self): - """ - .. http:get:: /certificates/defaults - - Returns defaults needed to generate CSRs - - **Example request**: - - .. sourcecode:: http - - GET /certificates/defaults HTTP/1.1 - Host: example.com - Accept: application/json, text/javascript - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: text/javascript - - { - "country": "US", - "state": "CA", - "location": "Los Gatos", - "organization": "Netflix", - "organizationalUnit": "Operations" - } - - :reqheader Authorization: OAuth token to authenticate - :statuscode 200: no error - :statuscode 403: unauthenticated - """ - return dict( - country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'), - state=current_app.config.get('LEMUR_DEFAULT_STATE'), - location=current_app.config.get('LEMUR_DEFAULT_LOCATION'), - organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'), - organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT') - ) - - api.add_resource(CertificatesList, '/certificates', endpoint='certificates') api.add_resource(Certificates, '/certificates/', endpoint='certificate') api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload') api.add_resource(CertificatePrivateKey, '/certificates//key', endpoint='privateKeyCertificates') api.add_resource(NotificationCertificatesList, '/notifications//certificates', endpoint='notificationCertificates') -api.add_resource(CertificatesDefaults, '/certificates/defaults', endpoint='certificatesDefault') diff --git a/lemur/status/__init__.py b/lemur/defaults/__init__.py similarity index 100% rename from lemur/status/__init__.py rename to lemur/defaults/__init__.py diff --git a/lemur/defaults/views.py b/lemur/defaults/views.py new file mode 100644 index 00000000..d053aef7 --- /dev/null +++ b/lemur/defaults/views.py @@ -0,0 +1,63 @@ +""" +.. module: lemur.status.views + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from flask import current_app, Blueprint +from flask.ext.restful import Api + +from lemur.auth.service import AuthenticatedResource + + +mod = Blueprint('default', __name__) +api = Api(mod) + + +class LemurDefaults(AuthenticatedResource): + """ Defines the 'defaults' endpoint """ + def __init__(self): + super(LemurDefaults) + + def get(self): + """ + .. http:get:: /defaults + + Returns defaults needed to generate CSRs + + **Example request**: + + .. sourcecode:: http + + GET /defaults HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "country": "US", + "state": "CA", + "location": "Los Gatos", + "organization": "Netflix", + "organizationalUnit": "Operations" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return dict( + country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'), + state=current_app.config.get('LEMUR_DEFAULT_STATE'), + location=current_app.config.get('LEMUR_DEFAULT_LOCATION'), + organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'), + organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT') + ) + +api.add_resource(LemurDefaults, '/defaults', endpoint='default') diff --git a/lemur/manage.py b/lemur/manage.py index 4b7ff6fb..5f9418ad 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -4,6 +4,8 @@ import os import sys import base64 import time +import requests +import json from gunicorn.config import make_settings from cryptography.fernet import Fernet @@ -146,12 +148,15 @@ def check_revoked(): as `unknown`. """ for cert in cert_service.get_all_certs(): - if cert.chain: - status = verify_string(cert.body, cert.chain) - else: - status = verify_string(cert.body, "") + try: + if cert.chain: + status = verify_string(cert.body, cert.chain) + else: + status = verify_string(cert.body, "") - cert.status = 'valid' if status else "invalid" + cert.status = 'valid' if status else 'invalid' + except Exception as e: + cert.status = 'unknown' database.update(cert) @@ -181,7 +186,7 @@ def generate_settings(): return output -@manager.option('-s', '--sources', dest='labels', default='', required=False) +@manager.option('-s', '--sources', dest='labels') def sync_sources(labels): """ Attempts to run several methods Certificate discovery. This is @@ -207,13 +212,14 @@ def sync_sources(labels): try: sync_lock.acquire(timeout=10) # wait up to 10 seconds - if labels: - sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels)) - labels = labels.split(",") - else: - sys.stdout.write("[+] Starting to sync ALL sources!\n") + sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels)) + labels = labels.split(",") + + if labels[0] == 'all': + sync() + else: + sync(labels=labels) - sync(labels=labels) sys.stdout.write( "[+] Finished syncing sources. Run Time: {time}\n".format( time=(time.time() - start_time) @@ -563,11 +569,11 @@ class ProvisionELB(Command): 'authority': authority, 'owner': owner, # defaults: - 'organization': u'Netflix, Inc.', - 'organizationalUnit': u'Operations', - 'country': u'US', - 'state': u'California', - 'location': u'Los Gatos' + 'organization': current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'), + 'organizationalUnit': current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'), + 'country': current_app.config.get('LEMUR_DEFAULT_COUNTRY'), + 'state': current_app.config.get('LEMUR_DEFAULT_STATE'), + 'location': current_app.config.get('LEMUR_DEFAULT_LOCATION') } return options @@ -680,6 +686,38 @@ class ProvisionELB(Command): done = True +@manager.command +def publish_verisign_units(): + """ + Simple function that queries verisign for API units and posts the mertics to + Atlas API for other teams to consume. + :return: + """ + from lemur.plugins import plugins + v = plugins.get('verisign-issuer') + units = v.get_available_units() + + metrics = {} + for item in units: + if item['@type'] in metrics.keys(): + metrics[item['@type']] += int(item['@remaining']) + else: + metrics.update({item['@type']: int(item['@remaining'])}) + + for name, value in metrics.items(): + metric = [ + { + "timestamp": 1321351651, + "type": "GAUGE", + "name": "Symantec {0} Unit Count".format(name), + "tags": {}, + "value": value + } + ] + + requests.post('http://localhost:8078/metrics', data=json.dumps(metric)) + + def main(): manager.add_command("start", LemurServer()) manager.add_command("runserver", Server(host='127.0.0.1')) diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index 3c944580..df34254d 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -9,7 +9,6 @@ """ import ssl -import socket import arrow @@ -114,8 +113,9 @@ def _get_domain_certificate(name): try: pub_key = ssl.get_server_certificate((name, 443)) return cert_service.find_duplicates(pub_key.strip()) - except socket.gaierror as e: + except Exception as e: current_app.logger.info(str(e)) + return [] def _find_superseded(cert): diff --git a/lemur/plugins/lemur_email/templates/expiration.html b/lemur/plugins/lemur_email/templates/expiration.html index f3584bd2..32d6883a 100644 --- a/lemur/plugins/lemur_email/templates/expiration.html +++ b/lemur/plugins/lemur_email/templates/expiration.html @@ -53,7 +53,7 @@

- Lemur, Netflix's SSL management portal has noticed that the following certificates are expiring soon, if you rely on these certificates + Lemur, has noticed that the following certificates are expiring soon, if you rely on these certificates you should create new certificates to replace the certificates that are expiring.

diff --git a/lemur/plugins/lemur_verisign/plugin.py b/lemur/plugins/lemur_verisign/plugin.py index 2eee6e3c..74e8f7f7 100644 --- a/lemur/plugins/lemur_verisign/plugin.py +++ b/lemur/plugins/lemur_verisign/plugin.py @@ -146,7 +146,7 @@ class VerisignIssuerPlugin(IssuerPlugin): :param issuer_options: :return: :raise Exception: """ - url = current_app.config.get('VERISIGN_URL') + 'rest/services/enroll' + url = current_app.config.get("VERISIGN_URL") + '/rest/services/enroll' data = process_options(issuer_options) data['csr'] = csr @@ -176,7 +176,7 @@ class VerisignIssuerPlugin(IssuerPlugin): :return: """ - url = current_app.config.get("VERISIGN_URL") + 'rest/services/getTokens' + url = current_app.config.get("VERISIGN_URL") + '/rest/services/getTokens' response = self.session.post(url, headers={'content-type': 'application/x-www-form-urlencoded'}) return handle_response(response.content)['Response']['Order'] diff --git a/lemur/static/app/angular/app.js b/lemur/static/app/angular/app.js index fd620ffe..da9ce330 100644 --- a/lemur/static/app/angular/app.js +++ b/lemur/static/app/angular/app.js @@ -60,6 +60,15 @@ lemur.controller('datePickerController', function ($scope, $timeout){ }; }); +lemur.service('DefaultService', function (LemurRestangular) { + var DefaultService = this; + DefaultService.get = function () { + return LemurRestangular.all('defaults').customGET().then(function (defaults) { + return defaults; + }); + }; +}); + lemur.factory('LemurRestangular', function (Restangular, $location, $auth) { return Restangular.withConfig(function (RestangularConfigurer) { RestangularConfigurer.setBaseUrl('http://localhost:5000/api/1'); diff --git a/lemur/static/app/angular/authorities/authority/authority.js b/lemur/static/app/angular/authorities/authority/authority.js index 0fcd54b3..be8c4066 100644 --- a/lemur/static/app/angular/authorities/authority/authority.js +++ b/lemur/static/app/angular/authorities/authority/authority.js @@ -30,6 +30,9 @@ angular.module('lemur') .controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler) { $scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities'); + // set the defaults + AuthorityService.getDefaults($scope.authority); + $scope.loading = false; $scope.create = function (authority) { WizardHandler.wizard().context.loading = true; diff --git a/lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html b/lemur/static/app/angular/authorities/authority/edit.tpl.html similarity index 59% rename from lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html rename to lemur/static/app/angular/authorities/authority/edit.tpl.html index f100ba1b..a11e9177 100644 --- a/lemur/static/app/angular/authorities/authority/authorityEdit.tpl.html +++ b/lemur/static/app/angular/authorities/authority/edit.tpl.html @@ -1,9 +1,32 @@