diff --git a/docs/developer/plugins/index.rst b/docs/developer/plugins/index.rst index b7cdcd2a..3168c21e 100644 --- a/docs/developer/plugins/index.rst +++ b/docs/developer/plugins/index.rst @@ -1,11 +1,11 @@ Writing a Plugin ================ -**The plugin interface is a work in progress.** - Several interfaces exist for extending Lemur: -* Issuers (lemur.issuers) +* Issuer (lemur.plugins.base.issuer) +* Destination (lemur.plugins.base.destination) +* Source (lemur.plugins.base.source) Structure --------- @@ -29,9 +29,9 @@ if you want to pull the version using pkg_resources (which is what we recommend) Inside of ``plugin.py``, you'll declare your Plugin class:: import lemur_pluginname - from lemur.common.services.issuers.plugins import Issuer + from lemur.plugins.base.issuer import IssuerPlugin - class PluginName(Plugin): + class PluginName(IssuerPlugin): title = 'Plugin Name' slug = 'pluginname' description = 'My awesome plugin!' @@ -55,27 +55,43 @@ And you'll register it via ``entry_points`` in your ``setup.py``:: ) -That's it! Users will be able to install your plugin via ``pip install `` and configure it -via the web interface based on the hooks you enabled. +That's it! Users will be able to install your plugin via ``pip install ``. + +Interfaces +========== + +Lemur has several different plugin interfaces that are used to extend Lemur, each of them require +that you subclass and override their functions in order for your plugin to function. -Permissions -=========== +Issuer +------ -As described in the plugin interface, Lemur provides a suite of permissions. +Issuer plugins are to be used when you want to allow Lemur to use external services to create certificates. +In the simple case this means that you have one Certificate Authority and you ask it for certificates given a +few parameters. In a more advanced case this could mean that this third party not only allows you to create certifcates +but also allows you to create Certificate Authorities and Sub Certificate Authorities. -In most cases, a admin (that is, if User.is_admin is ``True``), will be granted implicit permissions -on everything. +The `IssuerPlugin` interface only required that you implement one function:: -This page attempts to describe those permissions, and the contextual objects along with them. + def create_certificate(self, options): + # requests.get('a third party') -.. data:: add_project - Controls whether a user can create a new project. +Lemur will pass a dictionary of all possible options for certificate creation. - :: +Optionally the `IssuerPlugin` exposes another function for authority create:: - >>> has_perm('add_project', user) + def create_authority(self, options): + # request.get('a third party') + + +If implemented this function will be used to allow users to create external Certificate Authorities. From this function +you are expected to return the ROOT certificate authority, any intermediates that Authority might provide and any roles +you wish to be associated with this authority. + +.. Note:: You do not need to associate roles to the authority at creation time as they can always be associated after the +fact. Testing @@ -149,3 +165,5 @@ Running tests follows the py.test standard. As long as your test files and metho =========================== 1 passed in 0.35 seconds ============================ +.. SeeAlso:: Lemur bundles several plugins that use the same interfaces mentioned above. View the source: #TODO + diff --git a/gulp/server.js b/gulp/server.js index 1f00c599..7ee20381 100644 --- a/gulp/server.js +++ b/gulp/server.js @@ -38,7 +38,7 @@ function browserSyncInit(baseDir, files, browser) { gulp.task('serve', ['watch'], function () { browserSyncInit([ '.tmp', - 'app' + 'lemur/static/app' ], [ '.tmp/*.html', '.tmp/styles/**/*.css', diff --git a/gulp/watch.js b/gulp/watch.js index 9d6f894c..460a935b 100644 --- a/gulp/watch.js +++ b/gulp/watch.js @@ -3,7 +3,7 @@ var gulp = require('gulp'); -gulp.task('watch', ['dev:styles', 'dev:scripts', 'dev:inject'] ,function () { +gulp.task('watch', ['dev:styles', 'dev:scripts', 'dev:inject', 'dev:fonts'] ,function () { gulp.watch('app/styles/**/*.less', ['dev:styles']); gulp.watch('app/styles/**/*.css', ['dev:styles']); gulp.watch('app/**/*.js', ['dev:scripts']); diff --git a/lemur/__init__.py b/lemur/__init__.py index 39432438..aada8abe 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -8,8 +8,6 @@ """ -from flask import jsonify - from lemur import factory from lemur.users.views import mod as users_bp diff --git a/lemur/auth/service.py b/lemur/auth/service.py index 5fd20f42..6386f6d0 100644 --- a/lemur/auth/service.py +++ b/lemur/auth/service.py @@ -72,13 +72,13 @@ def create_token(user): :param user: :return: """ - expiration_delta = timedelta(days=int(current_app.config.get('TOKEN_EXPIRATION', 1))) + expiration_delta = timedelta(days=int(current_app.config.get('LEMUR_TOKEN_EXPIRATION', 1))) payload = { 'sub': user.id, 'iat': datetime.now(), 'exp': datetime.now() + expiration_delta } - token = jwt.encode(payload, current_app.config['TOKEN_SECRET']) + token = jwt.encode(payload, current_app.config['LEMUR_TOKEN_SECRET']) return token.decode('unicode_escape') @@ -102,7 +102,7 @@ def login_required(f): return dict(message='Token is invalid'), 403 try: - payload = jwt.decode(token, current_app.config['TOKEN_SECRET']) + payload = jwt.decode(token, current_app.config['LEMUR_TOKEN_SECRET']) except jwt.DecodeError: return dict(message='Token is invalid'), 403 except jwt.ExpiredSignatureError: diff --git a/lemur/auth/views.py b/lemur/auth/views.py index 06e77f77..fd7255ca 100644 --- a/lemur/auth/views.py +++ b/lemur/auth/views.py @@ -14,8 +14,6 @@ from flask import g, Blueprint, current_app, abort from flask.ext.restful import reqparse, Resource, Api from flask.ext.principal import Identity, identity_changed -from lemur.common.crypto import unlock - from lemur.auth.permissions import admin_permission from lemur.users import service as user_service from lemur.roles import service as role_service @@ -234,24 +232,7 @@ class Ping(Resource): return dict(token=create_token(user)) -class Unlock(AuthenticatedResource): - def __init__(self): - self.reqparse = reqparse.RequestParser() - super(Unlock, self).__init__() - - @admin_permission.require(http_exception=403) - def post(self): - self.reqparse.add_argument('password', type=str, required=True, location='json') - args = self.reqparse.parse_args() - unlock(args['password']) - return { - "message": "You have successfully unlocked this Lemur instance", - "type": "success" - } - - api.add_resource(Login, '/auth/login', endpoint='login') api.add_resource(Ping, '/auth/ping', endpoint='ping') -api.add_resource(Unlock, '/auth/unlock', endpoint='unlock') diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index 84861665..f414d6a8 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -17,7 +17,7 @@ from lemur.roles import service as role_service from lemur.roles.models import Role import lemur.certificates.service as cert_service -from lemur.common.services.issuers.manager import get_plugin_by_name +from lemur.plugins.base import plugins def update(authority_id, active=None, roles=None): """ @@ -49,12 +49,12 @@ def create(kwargs): :return: """ - issuer = get_plugin_by_name(kwargs.get('pluginName')) + issuer = plugins.get(kwargs.get('pluginName')) kwargs['creator'] = g.current_user.email cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs) - cert = cert_service.save_cert(cert_body, None, intermediate, None, None, None) + cert = cert_service.save_cert(cert_body, None, intermediate, None) cert.user = g.current_user # we create and attach any roles that the issuer gives us @@ -65,9 +65,11 @@ def create(kwargs): password=r['password'], description="{0} auto generated role".format(kwargs.get('pluginName')), username=r['username']) + # the user creating the authority should be able to administer it if role.username == 'admin': g.current_user.roles.append(role) + role_objs.append(role) authority = Authority( @@ -80,7 +82,6 @@ def create(kwargs): roles=role_objs ) - # do this last encase we need to roll back/abort database.update(cert) authority = database.create(authority) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 47af84e5..5a46a015 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -20,13 +20,12 @@ from sqlalchemy_utils import EncryptedType from lemur.database import db from lemur.domains.models import Domain -from lemur.users import service as user_service from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE, NONSTANDARD_NAMING_TEMPLATE from lemur.models import certificate_associations, certificate_account_associations -def create_name(issuer, not_before, not_after, common_name, san): +def create_name(issuer, not_before, not_after, subject, san): """ Create a name for our certificate. A naming standard is based on a series of templates. The name includes @@ -36,11 +35,6 @@ def create_name(issuer, not_before, not_after, common_name, san): :rtype : str :return: """ - delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum()) - # aws doesn't allow special chars - subject = common_name.replace('*', "WILDCARD") - issuer = issuer.translate(None, delchars) - if san: t = SAN_NAMING_TEMPLATE else: @@ -53,7 +47,14 @@ def create_name(issuer, not_before, not_after, common_name, san): not_after=not_after.strftime('%Y%m%d') ) - return temp + # NOTE we may want to give more control over naming + # aws doesn't allow special chars except '-' + disallowed_chars = ''.join(c for c in map(chr, range(256)) if not c.isalnum()) + disallowed_chars = disallowed_chars.replace("-", "") + temp = temp.replace('*', "WILDCARD") + temp = temp.translate(None, disallowed_chars) + # white space is silly too + return temp.replace(" ", "-") def cert_get_cn(cert): @@ -85,11 +86,6 @@ def cert_get_domains(cert): domains.append(entry) except Exception as e: current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e)) - - # do a simple check to make sure it's a real domain - common_name = cert_get_cn(cert) - if '.' in common_name: - domains.append(common_name) return domains @@ -111,11 +107,8 @@ def cert_is_san(cert): :param cert: :return: Bool """ - domains = cert_get_domains(cert) - if len(domains) > 1: + if len(cert_get_domains(cert)) > 1: return True - return False - def cert_is_wildcard(cert): """ @@ -127,7 +120,6 @@ def cert_is_wildcard(cert): domains = cert_get_domains(cert) if len(domains) == 1 and domains[0][0:1] == "*": return True - return False def cert_get_bitstrength(cert): @@ -205,8 +197,8 @@ class Certificate(db.Model): owner = Column(String(128)) body = Column(Text()) private_key = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) - challenge = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) - csr_config = Column(Text()) + challenge = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) # TODO deprecate + csr_config = Column(Text()) # TODO deprecate status = Column(String(128)) deleted = Column(Boolean, index=True) name = Column(String(128)) @@ -227,13 +219,11 @@ class Certificate(db.Model): domains = relationship("Domain", secondary=certificate_associations, backref="certificate") elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate') - def __init__(self, body, private_key=None, challenge=None, chain=None, csr_config=None): + def __init__(self, body, private_key=None, chain=None): self.body = body # We encrypt the private_key on creation self.private_key = private_key self.chain = chain - self.csr_config = csr_config - self.challenge = challenge cert = x509.load_pem_x509_certificate(str(self.body), default_backend()) self.bits = cert_get_bitstrength(cert) self.issuer = cert_get_issuer(cert) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 354c9868..3b3b2089 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -5,24 +5,17 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -import os import arrow import string import random -import hashlib -import datetime -import subprocess from sqlalchemy import func, or_ from flask import g, current_app from lemur import database from lemur.common.services.aws import iam -from lemur.common.services.issuers.manager import get_plugin_by_name - +from lemur.plugins.base import plugins from lemur.certificates.models import Certificate -from lemur.certificates.exceptions import UnableToCreateCSR, \ - UnableToCreatePrivateKey, MissingFiles from lemur.accounts.models import Account from lemur.accounts import service as account_service @@ -30,6 +23,12 @@ from lemur.authorities.models import Authority from lemur.roles.models import Role +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa + + def get(cert_id): """ @@ -127,24 +126,18 @@ def mint(issuer_options): """ authority = issuer_options['authority'] - issuer = get_plugin_by_name(authority.plugin_name) - # NOTE if we wanted to support more issuers it might make sense to - # push CSR creation down to the plugin - path = create_csr(issuer.get_csr_config(issuer_options)) - challenge, csr, csr_config, private_key = load_ssl_pack(path) + issuer = plugins.get(authority.plugin_name) - issuer_options['challenge'] = challenge + csr, private_key = create_csr(issuer_options) + + issuer_options['challenge'] = create_challenge() issuer_options['creator'] = g.user.email cert_body, cert_chain = issuer.create_certificate(csr, issuer_options) - cert = save_cert(cert_body, private_key, cert_chain, challenge, csr_config, issuer_options.get('accounts')) + cert = save_cert(cert_body, private_key, cert_chain, issuer_options.get('accounts')) cert.user = g.user cert.authority = authority database.update(cert) - - # securely delete pack after saving it to RDS and IAM (if applicable) - delete_ssl_pack(path) - return cert, private_key, cert_chain, @@ -152,7 +145,7 @@ def import_certificate(**kwargs): """ Uploads already minted certificates and pulls the required information into Lemur. - This is to be used for certificates that are reated outside of Lemur but + This is to be used for certificates that are created outside of Lemur but should still be tracked. Internally this is used to bootstrap Lemur with external @@ -180,7 +173,7 @@ def import_certificate(**kwargs): return cert -def save_cert(cert_body, private_key, cert_chain, challenge, csr_config, accounts): +def save_cert(cert_body, private_key, cert_chain, accounts): """ Determines if the certificate needs to be uploaded to AWS or other services. @@ -189,9 +182,9 @@ def save_cert(cert_body, private_key, cert_chain, challenge, csr_config, account :param cert_chain: :param challenge: :param csr_config: - :param account_ids: + :param accounts: """ - cert = Certificate(cert_body, private_key, challenge, cert_chain, csr_config) + cert = Certificate(cert_body, private_key, cert_chain) # if we have an AWS accounts lets upload them if accounts: for account in accounts: @@ -211,8 +204,6 @@ def upload(**kwargs): kwargs.get('public_cert'), kwargs.get('private_key'), kwargs.get('intermediate_cert'), - None, - None, kwargs.get('accounts') ) @@ -230,6 +221,7 @@ def create(**kwargs): cert.owner = kwargs['owner'] database.create(cert) + cert.description = kwargs['description'] g.user.certificates.append(cert) database.update(g.user) return cert @@ -302,94 +294,92 @@ def create_csr(csr_config): :param csr_config: """ + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() + ) - # we create a no colliding file name - path = create_path(hashlib.md5(csr_config).hexdigest()) + # TODO When we figure out a better way to validate these options they should be parsed as unicode + builder = x509.CertificateSigningRequestBuilder() + builder = builder.subject_name(x509.Name([ + x509.NameAttribute(x509.OID_COMMON_NAME, unicode(csr_config['commonName'])), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, unicode(csr_config['organization'])), + x509.NameAttribute(x509.OID_ORGANIZATIONAL_UNIT_NAME, unicode(csr_config['organizationalUnit'])), + x509.NameAttribute(x509.OID_COUNTRY_NAME, unicode(csr_config['country'])), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, unicode(csr_config['state'])), + x509.NameAttribute(x509.OID_LOCALITY_NAME, unicode(csr_config['location'])), + ])) - challenge = create_challenge() - challenge_path = os.path.join(path, 'challenge.txt') + builder = builder.add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True, + ) - with open(challenge_path, 'w') as c: - c.write(challenge) + #for k, v in csr_config.get('extensions', {}).items(): + # if k == 'subAltNames': + # builder = builder.add_extension( + # x509.SubjectAlternativeName([x509.DNSName(n) for n in v]), critical=True, + # ) - csr_path = os.path.join(path, 'csr_config.txt') + # TODO support more CSR options, none of the authorities support these atm + # builder.add_extension( + # x509.KeyUsage( + # digital_signature=digital_signature, + # content_commitment=content_commitment, + # key_encipherment=key_enipherment, + # data_encipherment=data_encipherment, + # key_agreement=key_agreement, + # key_cert_sign=key_cert_sign, + # crl_sign=crl_sign, + # encipher_only=enchipher_only, + # decipher_only=decipher_only + # ), critical=True + # ) + # + # # we must maintain our own list of OIDs here + # builder.add_extension( + # x509.ExtendedKeyUsage( + # server_authentication=server_authentication, + # email= + # ) + # ) + # + # builder.add_extension( + # x509.AuthorityInformationAccess() + # ) + # + # builder.add_extension( + # x509.AuthorityKeyIdentifier() + # ) + # + # builder.add_extension( + # x509.SubjectKeyIdentifier() + # ) + # + # builder.add_extension( + # x509.CRLDistributionPoints() + # ) + # + # builder.add_extension( + # x509.ObjectIdentifier(oid) + # ) - with open(csr_path, 'w') as f: - f.write(csr_config) + request = builder.sign( + private_key, hashes.SHA256(), default_backend() + ) - #TODO use cloudCA to seed a -rand file for each call - #TODO replace openssl shell calls with cryptograph - with open('/dev/null', 'w') as devnull: - code = subprocess.call(['openssl', 'genrsa', - '-out', os.path.join(path, 'private.key'), '2048'], - stdout=devnull, stderr=devnull) + # serialize our private key and CSR + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, # would like to use PKCS8 but AWS ELBs don't like it + encryption_algorithm=serialization.NoEncryption() + ) - if code != 0: - raise UnableToCreatePrivateKey(code) - - with open('/dev/null', 'w') as devnull: - code = subprocess.call(['openssl', 'req', '-new', '-sha256', '-nodes', - '-config', csr_path, "-key", os.path.join(path, 'private.key'), - "-out", os.path.join(path, 'request.csr')], stdout=devnull, stderr=devnull) - - if code != 0: - raise UnableToCreateCSR(code) - - return path - - -def create_path(domain_hash): - """ - - :param domain_hash: - :return: - """ - path = os.path.join('/tmp', domain_hash) - - try: - os.mkdir(path) - except OSError as e: - now = datetime.datetime.now() - path = os.path.join('/tmp', "{}.{}".format(domain_hash, now.strftime('%s'))) - os.mkdir(path) - current_app.logger.warning(e) - - current_app.logger.debug("Writing ssl files to: {}".format(path)) - return path - - -def load_ssl_pack(path): - """ - Loads the information created by openssl to be used by other functions. - - :param path: - """ - if len(os.listdir(path)) != 4: - raise MissingFiles(path) - - with open(os.path.join(path, 'challenge.txt')) as c: - challenge = c.read() - - with open(os.path.join(path, 'request.csr')) as r: - csr = r.read() - - with open(os.path.join(path, 'csr_config.txt')) as config: - csr_config = config.read() - - with open(os.path.join(path, 'private.key')) as key: - private_key = key.read() - - return (challenge, csr, csr_config, private_key,) - - -def delete_ssl_pack(path): - """ - Removes the temporary files associated with CSR creation. - - :param path: - """ - subprocess.check_call(['srm', '-r', path]) + csr = request.public_bytes( + encoding=serialization.Encoding.PEM + ) + return csr, pem def create_challenge(): """ diff --git a/lemur/certificates/sync.py b/lemur/certificates/sync.py index 3de59982..92438aa4 100644 --- a/lemur/certificates/sync.py +++ b/lemur/certificates/sync.py @@ -30,8 +30,7 @@ from lemur.certificates.models import Certificate, get_name_from_arn from lemur.common.services.aws.iam import get_all_server_certs from lemur.common.services.aws.iam import get_cert_from_arn -from lemur.common.services.issuers.manager import get_plugin_by_name - +from lemur.plugins.base import plugins def aws(): """ @@ -101,7 +100,7 @@ def cloudca(): """ user = user_service.get_by_email('lemur@nobody') # sync all new certificates/authorities not created through lemur - issuer = get_plugin_by_name('cloudca') + issuer = plugins.get('cloudca') authorities = issuer.get_authorities() total = 0 new = 1 diff --git a/lemur/common/crypto.py b/lemur/common/crypto.py deleted file mode 100644 index 80b03e42..00000000 --- a/lemur/common/crypto.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -.. module: lemur.common.crypto - :platform: Unix - :synopsis: This module contains all cryptographic function's in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. -.. moduleauthor:: Kevin Glisson - -""" -import os -import ssl -import StringIO -import functools -from Crypto import Random -from Crypto.Cipher import AES -from hashlib import sha512 - -from flask import current_app - -from lemur.factory import create_app - - -old_init = ssl.SSLSocket.__init__ - -@functools.wraps(old_init) -def ssl_bug(self, *args, **kwargs): - kwargs['ssl_version'] = ssl.PROTOCOL_TLSv1 - old_init(self, *args, **kwargs) - -ssl.SSLSocket.__init__ = ssl_bug - - -def derive_key_and_iv(password, salt, key_length, iv_length): - """ - Derives the key and iv from the password and salt. - - :param password: - :param salt: - :param key_length: - :param iv_length: - :return: key, iv - """ - d = d_i = '' - - while len(d) < key_length + iv_length: - d_i = sha512(d_i + password + salt).digest() - d += d_i - - return d[:key_length], d[key_length:key_length+iv_length] - - -def encrypt(in_file, out_file, password, key_length=32): - """ - Encrypts a file. - - :param in_file: - :param out_file: - :param password: - :param key_length: - """ - bs = AES.block_size - salt = Random.new().read(bs - len('Salted__')) - key, iv = derive_key_and_iv(password, salt, key_length, bs) - cipher = AES.new(key, AES.MODE_CBC, iv) - out_file.write('Salted__' + salt) - finished = False - while not finished: - chunk = in_file.read(1024 * bs) - if len(chunk) == 0 or len(chunk) % bs != 0: - padding_length = bs - (len(chunk) % bs) - chunk += padding_length * chr(padding_length) - finished = True - out_file.write(cipher.encrypt(chunk)) - - -def decrypt(in_file, out_file, password, key_length=32): - """ - Decrypts a file. - - :param in_file: - :param out_file: - :param password: - :param key_length: - :raise ValueError: - """ - bs = AES.block_size - salt = in_file.read(bs)[len('Salted__'):] - key, iv = derive_key_and_iv(password, salt, key_length, bs) - cipher = AES.new(key, AES.MODE_CBC, iv) - next_chunk = '' - finished = False - while not finished: - chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs)) - if len(next_chunk) == 0: - padding_length = ord(chunk[-1]) - if padding_length < 1 or padding_length > bs: - raise ValueError("bad decrypt pad (%d)" % padding_length) - # all the pad-bytes must be the same - if chunk[-padding_length:] != (padding_length * chr(padding_length)): - # this is similar to the bad decrypt:evp_enc.c from openssl program - raise ValueError("bad decrypt") - chunk = chunk[:-padding_length] - finished = True - out_file.write(chunk) - - -def encrypt_string(string, password): - """ - Encrypts a string. - - :param string: - :param password: - :return: - """ - in_file = StringIO.StringIO(string) - enc_file = StringIO.StringIO() - encrypt(in_file, enc_file, password) - enc_file.seek(0) - return enc_file.read() - - -def decrypt_string(string, password): - """ - Decrypts a string. - - :param string: - :param password: - :return: - """ - in_file = StringIO.StringIO(string) - out_file = StringIO.StringIO() - decrypt(in_file, out_file, password) - out_file.seek(0) - return out_file.read() - - -def lock(password): - """ - Encrypts Lemur's KEY_PATH. This directory can be used to store secrets needed for normal - Lemur operation. This is especially useful for storing secrets needed for communication - with third parties (e.g. external certificate authorities). - - Lemur does not assume anything about the contents of the directory and will attempt to - encrypt all files contained within. Currently this has only been tested against plain - text files. - - :param password: - """ - dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted") - - if not os.path.exists(dest_dir): - current_app.logger.debug("Creating encryption directory: {0}".format(dest_dir)) - os.makedirs(dest_dir) - - for root, dirs, files in os.walk(os.path.join(current_app.config.get("KEY_PATH"), 'decrypted')): - for f in files: - source = os.path.join(root, f) - dest = os.path.join(dest_dir, f + ".enc") - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - encrypt(in_file, out_file, password) - - -def unlock(password): - """ - Decrypts Lemur's KEY_PATH, allowing lemur to use the secrets within. - - This reverses the :func:`lock` function. - - :param password: - """ - dest_dir = os.path.join(current_app.config.get("KEY_PATH"), "decrypted") - source_dir = os.path.join(current_app.config.get("KEY_PATH"), "encrypted") - - if not os.path.exists(dest_dir): - current_app.logger.debug("Creating decryption directory: {0}".format(dest_dir)) - os.makedirs(dest_dir) - - for root, dirs, files in os.walk(source_dir): - for f in files: - source = os.path.join(source_dir, f) - dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1])) - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - current_app.logger.debug("Writing file: {0} Source: {1}".format(dest, source)) - decrypt(in_file, out_file, password) - diff --git a/lemur/common/managers.py b/lemur/common/managers.py new file mode 100644 index 00000000..43079f46 --- /dev/null +++ b/lemur/common/managers.py @@ -0,0 +1,64 @@ +""" +.. module: lemur.common.managers + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from flask import current_app + +# inspired by https://github.com/getsentry/sentry +class InstanceManager(object): + def __init__(self, class_list=None, instances=True): + if class_list is None: + class_list = [] + self.instances = instances + self.update(class_list) + + def get_class_list(self): + return self.class_list + + def add(self, class_path): + self.cache = None + self.class_list.append(class_path) + + def remove(self, class_path): + self.cache = None + self.class_list.remove(class_path) + + def update(self, class_list): + """ + Updates the class list and wipes the cache. + """ + self.cache = None + self.class_list = class_list + + def all(self): + """ + Returns a list of cached instances. + """ + class_list = list(self.get_class_list()) + if not class_list: + self.cache = [] + return [] + + if self.cache is not None: + return self.cache + + results = [] + for cls_path in class_list: + module_name, class_name = cls_path.rsplit('.', 1) + try: + module = __import__(module_name, {}, {}, class_name) + cls = getattr(module, class_name) + if self.instances: + results.append(cls()) + else: + results.append(cls) + except Exception: + current_app.logger.exception('Unable to import %s', cls_path) + continue + self.cache = results + + return results \ No newline at end of file diff --git a/lemur/common/services/issuers/manager.py b/lemur/common/services/issuers/manager.py deleted file mode 100644 index 4a5982c3..00000000 --- a/lemur/common/services/issuers/manager.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -.. module: lemur.common.services.issuers.manager - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson (kglisson@netflix.com) -""" -import pkgutil -from importlib import import_module - -from flask import current_app - -from lemur.common.services.issuers import plugins - -# TODO make the plugin dir configurable -def get_plugin_by_name(plugin_name): - """ - Fetches a given plugin by it's name. We use a known location for issuer plugins and attempt - to load it such that it can be used for issuing certificates. - - :param plugin_name: - :return: a plugin `class` :raise Exception: Generic error whenever the plugin specified can not be found. - """ - for importer, modname, ispkg in pkgutil.iter_modules(plugins.__path__): - try: - issuer = import_module('lemur.common.services.issuers.plugins.{0}.{0}'.format(modname)) - if issuer.__name__ == plugin_name: - # we shouldn't return bad issuers - issuer_obj = issuer.init() - return issuer_obj - except Exception as e: - current_app.logger.warn("Issuer {0} was unable to be imported: {1}".format(modname, e)) - - else: - raise Exception("Could not find the specified plugin: {0}".format(plugin_name)) - - diff --git a/lemur/common/services/issuers/plugins/cloudca/constants.py b/lemur/common/services/issuers/plugins/cloudca/constants.py deleted file mode 100644 index 229910bf..00000000 --- a/lemur/common/services/issuers/plugins/cloudca/constants.py +++ /dev/null @@ -1,27 +0,0 @@ -CSR_CONFIG = """ - # Configuration for standard CSR generation for Netflix - # Used for procuring CloudCA certificates - # Author: kglisson - # Contact: secops@netflix.com - - [ req ] - # Use a 2048 bit private key - default_bits = 2048 - default_keyfile = key.pem - prompt = no - encrypt_key = no - - # base request - distinguished_name = req_distinguished_name - - # distinguished_name - [ req_distinguished_name ] - countryName = "{country}" # C= - stateOrProvinceName = "{state}" # ST= - localityName = "{location}" # L= - organizationName = "{organization}" # O= - organizationalUnitName = "{organizationalUnit}" # OU= - # This is the hostname/subject name on the certificate - commonName = "{commonName}" # CN= - """ - diff --git a/lemur/common/services/issuers/plugins/verisign/constants.py b/lemur/common/services/issuers/plugins/verisign/constants.py deleted file mode 100644 index e5d84c49..00000000 --- a/lemur/common/services/issuers/plugins/verisign/constants.py +++ /dev/null @@ -1,159 +0,0 @@ -CSR_CONFIG = """ - # Configuration for standard CSR generation for Netflix - # Used for procuring VeriSign certificates - # Author: jachan - # Contact: cloudsecurity@netflix.com - - [ req ] - # Use a 2048 bit private key - default_bits = 2048 - default_keyfile = key.pem - prompt = no - encrypt_key = no - - # base request - distinguished_name = req_distinguished_name - - # extensions - # Uncomment the following line if you are requesting a SAN cert - {is_san_comment}req_extensions = req_ext - - # distinguished_name - [ req_distinguished_name ] - countryName = "US" # C= - stateOrProvinceName = "CALIFORNIA" # ST= - localityName = "Los Gatos" # L= - organizationName = "Netflix, Inc." # O= - organizationalUnitName = "{OU}" # OU= - # This is the hostname/subject name on the certificate - commonName = "{DNS[0]}" # CN= - - [ req_ext ] - # Uncomment the following line if you are requesting a SAN cert - {is_san_comment}subjectAltName = @alt_names - - [alt_names] - # Put your SANs here - {DNS_LINES} - """ - -VERISIGN_INTERMEDIATE = """ ------BEGIN CERTIFICATE----- -MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW -ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5IC0gRzMwHhcNMTMxMDMxMDAwMDAwWhcNMjMxMDMwMjM1OTU5WjB+MQsw -CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV -BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVjIENs -YXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEAstgFyhx0LbUXVjnFSlIJluhL2AzxaJ+aQihiw6UwU35VEYJb -A3oNL+F5BMm0lncZgQGUWfm893qZJ4Itt4PdWid/sgN6nFMl6UgfRk/InSn4vnlW -9vf92Tpo2otLgjNBEsPIPMzWlnqEIRoiBAMnF4scaGGTDw5RgDMdtLXO637QYqzu -s3sBdO9pNevK1T2p7peYyo2qRA4lmUoVlqTObQJUHypqJuIGOmNIrLRM0XWTUP8T -L9ba4cYY9Z/JJV3zADreJk20KQnNDz0jbxZKgRb78oMQw7jW2FUyPfG9D72MUpVK -Fpd6UiFjdS8W+cRmvvW1Cdj/JwDNRHxvSz+w9wIDAQABo4IBQDCCATwwHQYDVR0O -BBYEFF9gz2GQVd+EQxSKYCqy9Xr0QxjvMBIGA1UdEwEB/wQIMAYBAf8CAQAwawYD -VR0gBGQwYjBgBgpghkgBhvhFAQc2MFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cu -c3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0 -aC5jb20vcnBhMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9zLnN5bWNiLmNvbS9w -Y2EzLWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwKQYDVR0RBCIwIKQeMBwxGjAYBgNV -BAMTEVN5bWFudGVjUEtJLTEtNTM0MC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcw -AYYSaHR0cDovL3Muc3ltY2QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBbF1K+1lZ7 -9Pc0CUuWysf2IdBpgO/nmhnoJOJ/2S9h3RPrWmXk4WqQy04q6YoW51KN9kMbRwUN -gKOomv4p07wdKNWlStRxPA91xQtzPwBIZXkNq2oeJQzAAt5mrL1LBmuaV4oqgX5n -m7pSYHPEFfe7wVDJCKW6V0o6GxBzHOF7tpQDS65RsIJAOloknO4NWF2uuil6yjOe -soHCL47BJ89A8AShP/U3wsr8rFNtqVNpT+F2ZAwlgak3A/I5czTSwXx4GByoaxbn -5+CdKa/Y5Gk5eZVpuXtcXQGc1PfzSEUTZJXXCm5y2kMiJG8+WnDcwJLgLeVX+OQr -J+71/xuzAYN6 ------END CERTIFICATE----- -""" - - -VERISIGN_ROOT = """ ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl -cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu -LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT -aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD -VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT -aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ -bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu -IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b -N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t -KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu -kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm -CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ -Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu -imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te -2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe -DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC -/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p -F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt -TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== ------END CERTIFICATE----- -""" - -OLD_VERISIGN_INTERMEDIATE = """ ------BEGIN CERTIFICATE----- -MIIFlTCCBH2gAwIBAgIQLP62CQ7ireLp/CI3JPG2vzANBgkqhkiG9w0BAQUFADCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW -ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5IC0gRzMwHhcNMTAwMjA4MDAwMDAwWhcNMjAwMjA3MjM1OTU5WjCBtTEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2UgYXQg -aHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEvMC0GA1UEAxMmVmVy -aVNpZ24gQ2xhc3MgMyBTZWN1cmUgU2VydmVyIENBIC0gRzMwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCxh4QfwgxF9byrJZenraI+nLr2wTm4i8rCrFbG -5btljkRPTc5v7QlK1K9OEJxoiy6Ve4mbE8riNDTB81vzSXtig0iBdNGIeGwCU/m8 -f0MmV1gzgzszChew0E6RJK2GfWQS3HRKNKEdCuqWHQsV/KNLO85jiND4LQyUhhDK -tpo9yus3nABINYYpUHjoRWPNGUFP9ZXse5jUxHGzUL4os4+guVOc9cosI6n9FAbo -GLSa6Dxugf3kzTU2s1HTaewSulZub5tXxYsU5w7HnO1KVGrJTcW/EbGuHGeBy0RV -M5l/JJs/U0V/hhrzPPptf4H1uErT9YU3HLWm0AnkGHs4TvoPAgMBAAGjggGIMIIB -hDASBgNVHRMBAf8ECDAGAQH/AgEAMHAGA1UdIARpMGcwZQYLYIZIAYb4RQEHFwMw -VjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL2NwczAqBggr -BgEFBQcCAjAeGhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vcnBhMA4GA1UdDwEB -/wQEAwIBBjBtBggrBgEFBQcBDARhMF+hXaBbMFkwVzBVFglpbWFnZS9naWYwITAf -MAcGBSsOAwIaBBSP5dMahqyNjmvDz4Bq1EgYLHsZLjAlFiNodHRwOi8vbG9nby52 -ZXJpc2lnbi5jb20vdnNsb2dvLmdpZjAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQ -VmVyaVNpZ25NUEtJLTItNjAdBgNVHQ4EFgQUDURcFlNEwYJ+HSCrJfQBY9i+eaUw -NAYDVR0fBC0wKzApoCegJYYjaHR0cDovL2NybC52ZXJpc2lnbi5jb20vcGNhMy1n -My5jcmwwDQYJKoZIhvcNAQEFBQADggEBAHREFQzFWA4YY+3z8CjDeuuSSG/ghSBJ -olwwlpIX4IjoeYuzT864Hzk2tTeEeODf4YFIVsSxah8nUsGdpgVTUGPPoUJOMXvn -8wJeBSlUDXBwv3td5XbPIPXHy6vmIS6phYRetZUgq1CDTI/pvtWZKXTGM/eYXlLF -6QDvXevUHQjfb3cqQvfLljws85xLxbNFmz7cy9YmiLOd5n+gFC6X5hzSDO7+DDMi -o//+4Q/nk/UId1UCsobqYWVmqs017AmyiAPO/v3sGncYYQY2BMYgla74dZfeDNu4 -MXA68Mb6ZdlkhGEmZYVBcOmkaKs+P+SggTofsK27BlpugAtNWjEy5JY= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEOzCCA6SgAwIBAgIQSsnqCI7m94zHpfn6OaSTljANBgkqhkiG9w0BAQUFADBf -MQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xNzA1BgNVBAsT -LkNsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw -HhcNMTEwNjA5MDAwMDAwWhcNMjExMTA3MjM1OTU5WjCByjELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJpU2lnbiwgSW5jLiAtIEZv -ciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzMwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLupxS/HgfGh5vGzdzvfjJa5QS -ME/wNkf10JEK9RfIpWHBFkBN+4phkOV2IMERBn2rLG6m9RFBjvotrSphWaRnJkzQ -6LxSW3AgBFjResmkabyDF2StBYu80FjOjYz16/BCSQudlydnMm7hrpMVHHC8IE0v -GN6SiOhshVcRGul+4yYRVKJFllWDyjCJ6NzYo+0qgD9/eWVXPhUgZggvlZO/qkcv -qEaX8BLi/sIKK1Hmdua3RrfiDabMqMNMWVWJ5uhTXBzqnfBiFgunyV8M8N7Cds6v -92ry+kGmojMUyeV6Y9OeYjfVhWWeDuZTJHQbXh0SU1vHLOeDSTsVropouVeXAgMB -AAGjggEGMIIBAjAPBgNVHRMBAf8EBTADAQH/MD0GA1UdIAQ2MDQwMgYEVR0gADAq -MCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vY3BzMDEGA1Ud -HwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwudmVyaXNpZ24uY29tL3BjYTMuY3JsMA4G -A1UdDwEB/wQEAwIBBjBtBggrBgEFBQcBDARhMF+hXaBbMFkwVzBVFglpbWFnZS9n -aWYwITAfMAcGBSsOAwIaBBSP5dMahqyNjmvDz4Bq1EgYLHsZLjAlFiNodHRwOi8v -bG9nby52ZXJpc2lnbi5jb20vdnNsb2dvLmdpZjANBgkqhkiG9w0BAQUFAAOBgQBl -2Sr58sJgybnqQQfKNrcYL2iu/gMk5mdU7nTDLNn1M8Fetw6Tz3iejrImFBFT0cjC -EiG0PXsq2BzUS2TsiU+/lYeH3pVk9HPGF9+9GZCX6GmBEmlmStMkQA5ZdRWwRHQX -op4GYNOwg7jdL+afe2dcFqFH284ueQXZ8fT4PuJKoQ== ------END CERTIFICATE----- -""" diff --git a/lemur/factory.py b/lemur/factory.py index 63d4f428..a606700a 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -12,6 +12,7 @@ import os import imp import errno +import pkg_resources from logging import Formatter from logging.handlers import RotatingFileHandler @@ -51,6 +52,7 @@ def create_app(app_name=None, blueprints=None, config=None): configure_blueprints(app, blueprints) configure_extensions(app) configure_logging(app) + install_plugins(app) @app.teardown_appcontext def teardown(exception=None): @@ -141,3 +143,26 @@ def configure_logging(app): app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG')) app.logger.addHandler(handler) + +def install_plugins(app): + """ + Installs new issuers that are not currently bundled with Lemur. + + :param settings: + :return: + """ + from lemur.plugins.base import register + # entry_points={ + # 'lemur.plugins': [ + # 'verisign = lemur_verisign.plugin:VerisignPlugin' + # ], + # }, + for ep in pkg_resources.iter_entry_points('lemur.plugins'): + try: + plugin = ep.load() + except Exception: + import sys + import traceback + app.logger.error("Failed to load plugin %r:\n%s\n" % (ep.name, traceback.format_exc())) + else: + register(plugin) diff --git a/lemur/manage.py b/lemur/manage.py index 1680ed2f..1ff0d9f5 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -1,9 +1,10 @@ -#!/usr/bin/env python import os import sys import base64 from gunicorn.config import make_settings +from cryptography.fernet import Fernet + from flask import current_app from flask.ext.script import Manager, Command, Option, Group, prompt_pass from flask.ext.migrate import Migrate, MigrateCommand, stamp @@ -20,7 +21,6 @@ from lemur.certificates import sync from lemur.elbs.sync import sync_all_elbs from lemur import create_app -from lemur.common.crypto import encrypt, decrypt, lock, unlock # Needed to be imported so that SQLAlchemy create_all can find our models from lemur.users.models import User @@ -133,78 +133,6 @@ def create(): stamp(revision='head') -@manager.command -def lock(): - """ - Encrypts all of the files in the `keys` directory with the password - given. This is a useful function to ensure that you do no check in - your key files into source code in clear text. - - :return: - """ - password = prompt_pass("Please enter the encryption password") - lock(password) - sys.stdout.write("[+] Lemur keys have been encrypted!\n") - - -@manager.command -def unlock(): - """ - Decrypts all of the files in the `keys` directory with the password - given. This is most commonly used during the startup sequence of Lemur - allowing it to go from source code to something that can communicate - with external services. - - :return: - """ - password = prompt_pass("Please enter the encryption password") - unlock(password) - sys.stdout.write("[+] Lemur keys have been unencrypted!\n") - - -@manager.command -def encrypt_file(source): - """ - Utility to encrypt sensitive files, Lemur will decrypt these - files when admin enters the correct password. - - Uses AES-256-CBC encryption - """ - dest = source + ".encrypted" - password = prompt_pass("Please enter the encryption password") - password1 = prompt_pass("Please confirm the encryption password") - if password != password1: - sys.stdout.write("[!] Encryption passwords do not match!\n") - return - - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - encrypt(in_file, out_file, password) - - sys.stdout.write("[+] Writing encryption files... {0}!\n".format(dest)) - - -@manager.command -def decrypt_file(source): - """ - Utility to decrypt, Lemur will decrypt these - files when admin enters the correct password. - - Assumes AES-256-CBC encryption - """ - # cleanup extensions a bit - if ".encrypted" in source: - dest = ".".join(source.split(".")[:-1]) + ".decrypted" - else: - dest = source + ".decrypted" - - password = prompt_pass("Please enter the encryption password") - - with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: - decrypt(in_file, out_file, password) - - sys.stdout.write("[+] Writing decrypted files... {0}!\n".format(dest)) - - @manager.command def check_revoked(): """ @@ -346,45 +274,6 @@ class InitializeApp(Command): sys.stdout.write("[/] Done!\n") - -#def install_issuers(settings): -# """ -# Installs new issuers that are not currently bundled with Lemur. -# -# :param settings: -# :return: -# """ -# from lemur.issuers import register -# # entry_points={ -# # 'lemur.issuers': [ -# # 'verisign = lemur_issuers.issuers:VerisignPlugin' -# # ], -# # }, -# installed_apps = list(settings.INSTALLED_APPS) -# for ep in pkg_resources.iter_entry_points('lemur.apps'): -# try: -# issuer = ep.load() -# except Exception: -# import sys -# import traceback -# -# sys.stderr.write("Failed to load app %r:\n%s\n" % (ep.name, traceback.format_exc())) -# else: -# installed_apps.append(ep.module_name) -# settings.INSTALLED_APPS = tuple(installed_apps) -# -# for ep in pkg_resources.iter_entry_points('lemur.issuers'): -# try: -# issuer = ep.load() -# except Exception: -# import sys -# import traceback -# -# sys.stderr.write("Failed to load issuer %r:\n%s\n" % (ep.name, traceback.format_exc())) -# else: -# register(issuer) - - class CreateUser(Command): """ This command allows for the creation of a new user within Lemur @@ -492,7 +381,84 @@ def create_config(config_path=None): with open(config_path, 'w') as f: f.write(config) - sys.stdout.write("Created a new configuration file {0}\n".format(config_path)) + sys.stdout.write("[+] Created a new configuration file {0}\n".format(config_path)) + + +@manager.command +def lock(path=None): + """ + Encrypts a given path. This directory can be used to store secrets needed for normal + Lemur operation. This is especially useful for storing secrets needed for communication + with third parties (e.g. external certificate authorities). + + Lemur does not assume anything about the contents of the directory and will attempt to + encrypt all files contained within. Currently this has only been tested against plain + text files. + + Path defaults ~/.lemur/keys + + :param: path + """ + if not path: + path = os.path.expanduser('~/.lemur/keys') + + dest_dir = os.path.join(path, "encrypted") + sys.stdout.write("[!] Generating a new key...\n") + + key = Fernet.generate_key() + + if not os.path.exists(dest_dir): + sys.stdout.write("[+] Creating encryption directory: {0}\n".format(dest_dir)) + os.makedirs(dest_dir) + + for root, dirs, files in os.walk(os.path.join(path, 'decrypted')): + for f in files: + source = os.path.join(root, f) + dest = os.path.join(dest_dir, f + ".enc") + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + f = Fernet(key) + data = f.encrypt(in_file.read()) + out_file.write(data) + sys.stdout.write("[+] Writing file: {0} Source: {1}\n".format(dest, source)) + + sys.stdout.write("[+] Keys have been encrypted with key {0}\n".format(key)) + + +@manager.command +def unlock(path=None): + """ + Decrypts all of the files in a given directory with provided password. + This is most commonly used during the startup sequence of Lemur + allowing it to go from source code to something that can communicate + with external services. + + Path defaults ~/.lemur/keys + + :param: path + """ + key = prompt_pass("[!] Please enter the encryption password") + + if not path: + path = os.path.expanduser('~/.lemur/keys') + + dest_dir = os.path.join(path, "decrypted") + source_dir = os.path.join(path, "encrypted") + + if not os.path.exists(dest_dir): + sys.stdout.write("[+] Creating decryption directory: {0}\n".format(dest_dir)) + os.makedirs(dest_dir) + + for root, dirs, files in os.walk(source_dir): + for f in files: + source = os.path.join(source_dir, f) + dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1])) + with open(source, 'rb') as in_file, open(dest, 'wb') as out_file: + f = Fernet(key) + data = f.decrypt(in_file.read()) + out_file.write(data) + sys.stdout.write("[+] Writing file: {0} Source: {1}\n".format(dest, source)) + + sys.stdout.write("[+] Keys have been unencrypted!\n") def main(): diff --git a/lemur/plugins/__init__.py b/lemur/plugins/__init__.py new file mode 100644 index 00000000..d2656990 --- /dev/null +++ b/lemur/plugins/__init__.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import + +from lemur.plugins.base import * # NOQA +from lemur.plugins.bases import * # NOQA diff --git a/lemur/plugins/base/__init__.py b/lemur/plugins/base/__init__.py new file mode 100644 index 00000000..7091b27b --- /dev/null +++ b/lemur/plugins/base/__init__.py @@ -0,0 +1,16 @@ +""" +.. module: lemur.plugins.base + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from __future__ import absolute_import, print_function + +from lemur.plugins.base.manager import PluginManager +from lemur.plugins.base.v1 import * # NOQA + +plugins = PluginManager() +register = plugins.register +unregister = plugins.unregister diff --git a/lemur/plugins/base/manager.py b/lemur/plugins/base/manager.py new file mode 100644 index 00000000..32234cdc --- /dev/null +++ b/lemur/plugins/base/manager.py @@ -0,0 +1,59 @@ +""" +.. module: lemur.plugins.base.manager + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson (kglisson@netflix.com) +""" +from flask import current_app +from lemur.common.managers import InstanceManager + + +# inspired by https://github.com/getsentry/sentry +class PluginManager(InstanceManager): + def __iter__(self): + return iter(self.all()) + + def __len__(self): + return sum(1 for i in self.all()) + + def all(self, version=1): + for plugin in sorted(super(PluginManager, self).all(), key=lambda x: x.get_title()): + if not plugin.is_enabled(): + continue + if version is not None and plugin.__version__ != version: + continue + yield plugin + + def get(self, slug): + for plugin in self.all(version=1): + if plugin.slug == slug: + return plugin + for plugin in self.all(version=2): + if plugin.slug == slug: + return plugin + raise KeyError(slug) + + def first(self, func_name, *args, **kwargs): + version = kwargs.pop('version', 1) + for plugin in self.all(version=version): + try: + result = getattr(plugin, func_name)(*args, **kwargs) + except Exception as e: + current_app.logger.error('Error processing %s() on %r: %s', func_name, plugin.__class__, e, extra={ + 'func_arg': args, + 'func_kwargs': kwargs, + }, exc_info=True) + continue + + if result is not None: + return result + + def register(self, cls): + self.add('%s.%s' % (cls.__module__, cls.__name__)) + return cls + + def unregister(self, cls): + self.remove('%s.%s' % (cls.__module__, cls.__name__)) + return cls + diff --git a/lemur/plugins/base/v1.py b/lemur/plugins/base/v1.py new file mode 100644 index 00000000..448e6d95 --- /dev/null +++ b/lemur/plugins/base/v1.py @@ -0,0 +1,117 @@ +""" +.. module: lemur.plugins.base.v1 + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from threading import local + +# stolen from https://github.com/getsentry/sentry/ +class PluginMount(type): + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + if IPlugin in bases: + return new_cls + if new_cls.title is None: + new_cls.title = new_cls.__name__ + if not new_cls.slug: + new_cls.slug = new_cls.title.replace(' ', '-').lower() + return new_cls + + +class IPlugin(local): + """ + Plugin interface. Should not be inherited from directly. + A plugin should be treated as if it were a singleton. The owner does not + control when or how the plugin gets instantiated, nor is it guaranteed that + it will happen, or happen more than once. + >>> from lemur.plugins import Plugin + >>> + >>> class MyPlugin(Plugin): + >>> def get_title(self): + >>> return 'My Plugin' + As a general rule all inherited methods should allow ``**kwargs`` to ensure + ease of future compatibility. + """ + # Generic plugin information + title = None + slug = None + description = None + version = None + author = None + author_url = None + resource_links = () + + # Configuration specifics + conf_key = None + conf_title = None + + # Global enabled state + enabled = True + can_disable = True + + def is_enabled(self, project=None): + """ + Returns a boolean representing if this plugin is enabled. + If ``project`` is passed, it will limit the scope to that project. + >>> plugin.is_enabled() + """ + if not self.enabled: + return False + if not self.can_disable: + return True + + return True + + def get_conf_key(self): + """ + Returns a string representing the configuration keyspace prefix for this plugin. + """ + if not self.conf_key: + self.conf_key = self.get_conf_title().lower().replace(' ', '_') + return self.conf_key + + def get_conf_title(self): + """ + Returns a string representing the title to be shown on the configuration page. + """ + return self.conf_title or self.get_title() + + def get_title(self): + """ + Returns the general title for this plugin. + >>> plugin.get_title() + """ + return self.title + + def get_description(self): + """ + Returns the description for this plugin. This is shown on the plugin configuration + page. + >>> plugin.get_description() + """ + return self.description + + def get_resource_links(self): + """ + Returns a list of tuples pointing to various resources for this plugin. + >>> def get_resource_links(self): + >>> return [ + >>> ('Documentation', 'http://sentry.readthedocs.org'), + >>> ('Bug Tracker', 'https://github.com/getsentry/sentry/issues'), + >>> ('Source', 'https://github.com/getsentry/sentry'), + >>> ] + """ + return self.resource_links + + +class Plugin(IPlugin): + """ + A plugin should be treated as if it were a singleton. The owner does not + control when or how the plugin gets instantiated, nor is it guaranteed that + it will happen, or happen more than once. + """ + __version__ = 1 + __metaclass__ = PluginMount diff --git a/lemur/plugins/bases/__init__.py b/lemur/plugins/bases/__init__.py new file mode 100644 index 00000000..d43aa85e --- /dev/null +++ b/lemur/plugins/bases/__init__.py @@ -0,0 +1,2 @@ +from .destination import DestinationPlugin # NOQA +from .issuer import IssuerPlugin # NOQA \ No newline at end of file diff --git a/lemur/plugins/bases/destination.py b/lemur/plugins/bases/destination.py new file mode 100644 index 00000000..b95d2c7e --- /dev/null +++ b/lemur/plugins/bases/destination.py @@ -0,0 +1,13 @@ +""" +.. module: lemur.bases.destination + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from lemur.plugins.base import Plugin + +class DestinationPlugin(Plugin): + pass + diff --git a/lemur/common/services/issuers/issuer.py b/lemur/plugins/bases/issuer.py similarity index 55% rename from lemur/common/services/issuers/issuer.py rename to lemur/plugins/bases/issuer.py index 4950a9b9..b2e5c964 100644 --- a/lemur/common/services/issuers/issuer.py +++ b/lemur/plugins/bases/issuer.py @@ -1,32 +1,21 @@ """ -.. module: authority +.. module: lemur.bases.issuer :platform: Unix :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -from flask import current_app +from lemur.plugins.base import Plugin - -class Issuer(object): +class IssuerPlugin(Plugin): """ This is the base class from which all of the supported issuers will inherit from. """ - - def __init__(self): - self.dry_run = current_app.config.get('DRY_RUN') - def create_certificate(self): raise NotImplementedError def create_authority(self): - raise NotImplementedError - - def get_authorities(self): - raise NotImplementedError - - def get_csr_config(self): - raise NotImplementedError + raise NotImplemented diff --git a/lemur/common/services/issuers/__init__.py b/lemur/plugins/bases/source.py similarity index 100% rename from lemur/common/services/issuers/__init__.py rename to lemur/plugins/bases/source.py diff --git a/lemur/common/services/issuers/plugins/__init__.py b/lemur/plugins/lemur_aws/__init__.py similarity index 100% rename from lemur/common/services/issuers/plugins/__init__.py rename to lemur/plugins/lemur_aws/__init__.py diff --git a/lemur/plugins/lemur_aws/aws.py b/lemur/plugins/lemur_aws/aws.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/common/services/issuers/plugins/cloudca/__init__.py b/lemur/plugins/lemur_cloudca/__init__.py similarity index 100% rename from lemur/common/services/issuers/plugins/cloudca/__init__.py rename to lemur/plugins/lemur_cloudca/__init__.py diff --git a/lemur/common/services/issuers/plugins/cloudca/cloudca.py b/lemur/plugins/lemur_cloudca/plugin.py similarity index 94% rename from lemur/common/services/issuers/plugins/cloudca/cloudca.py rename to lemur/plugins/lemur_cloudca/plugin.py index d6612b4e..8d42cb3b 100644 --- a/lemur/common/services/issuers/plugins/cloudca/cloudca.py +++ b/lemur/plugins/lemur_cloudca/plugin.py @@ -18,10 +18,8 @@ from requests.adapters import HTTPAdapter from flask import current_app from lemur.exceptions import LemurException -from lemur.common.services.issuers.issuer import Issuer - -from lemur.common.services.issuers.plugins import cloudca - +from lemur.plugins.bases import IssuerPlugin +from lemur.plugins import lemur_cloudca as cloudca from lemur.authorities import service as authority_service @@ -144,7 +142,7 @@ def get_auth_data(ca_name): raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name)) -class CloudCA(Issuer): +class CloudCAPlugin(IssuerPlugin): title = 'CloudCA' slug = 'cloudca' description = 'Enables the creation of certificates from the cloudca API.' @@ -164,7 +162,7 @@ class CloudCA(Issuer): else: current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA") - super(CloudCA, self).__init__(*args, **kwargs) + super(CloudCAPlugin, self).__init__(*args, **kwargs) def create_authority(self, options): """ @@ -261,15 +259,6 @@ class CloudCA(Issuer): return cert, "".join(intermediates), - def get_csr_config(self, issuer_options): - """ - Get a valid CSR for use with CloudCA - - :param issuer_options: - :return: - """ - return cloudca.constants.CSR_CONFIG.format(**issuer_options) - def random(self, length=10): """ Uses CloudCA as a decent source of randomness. @@ -317,9 +306,6 @@ class CloudCA(Issuer): :param data: :return: """ - if self.dry_run: - endpoint += '?dry_run=1' - data = dumps(dict(data.items() + get_auth_data(data['caName']).items())) # we set a low timeout, if cloudca is down it shouldn't bring down @@ -334,13 +320,6 @@ class CloudCA(Issuer): :param endpoint: :return: """ - if self.dry_run: - endpoint += '?dry_run=1' - response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle) return process_response(response) - -def init(): - return CloudCA() - diff --git a/lemur/common/services/issuers/plugins/verisign/__init__.py b/lemur/plugins/lemur_verisign/__init__.py similarity index 100% rename from lemur/common/services/issuers/plugins/verisign/__init__.py rename to lemur/plugins/lemur_verisign/__init__.py diff --git a/lemur/plugins/lemur_verisign/constants.py b/lemur/plugins/lemur_verisign/constants.py new file mode 100644 index 00000000..0f90ed98 --- /dev/null +++ b/lemur/plugins/lemur_verisign/constants.py @@ -0,0 +1,58 @@ +VERISIGN_INTERMEDIATE = """-----BEGIN CERTIFICATE----- +MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp +U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW +ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5IC0gRzMwHhcNMTMxMDMxMDAwMDAwWhcNMjMxMDMwMjM1OTU5WjB+MQsw +CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV +BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVjIENs +YXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAstgFyhx0LbUXVjnFSlIJluhL2AzxaJ+aQihiw6UwU35VEYJb +A3oNL+F5BMm0lncZgQGUWfm893qZJ4Itt4PdWid/sgN6nFMl6UgfRk/InSn4vnlW +9vf92Tpo2otLgjNBEsPIPMzWlnqEIRoiBAMnF4scaGGTDw5RgDMdtLXO637QYqzu +s3sBdO9pNevK1T2p7peYyo2qRA4lmUoVlqTObQJUHypqJuIGOmNIrLRM0XWTUP8T +L9ba4cYY9Z/JJV3zADreJk20KQnNDz0jbxZKgRb78oMQw7jW2FUyPfG9D72MUpVK +Fpd6UiFjdS8W+cRmvvW1Cdj/JwDNRHxvSz+w9wIDAQABo4IBQDCCATwwHQYDVR0O +BBYEFF9gz2GQVd+EQxSKYCqy9Xr0QxjvMBIGA1UdEwEB/wQIMAYBAf8CAQAwawYD +VR0gBGQwYjBgBgpghkgBhvhFAQc2MFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cu +c3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0 +aC5jb20vcnBhMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9zLnN5bWNiLmNvbS9w +Y2EzLWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwKQYDVR0RBCIwIKQeMBwxGjAYBgNV +BAMTEVN5bWFudGVjUEtJLTEtNTM0MC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcw +AYYSaHR0cDovL3Muc3ltY2QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBbF1K+1lZ7 +9Pc0CUuWysf2IdBpgO/nmhnoJOJ/2S9h3RPrWmXk4WqQy04q6YoW51KN9kMbRwUN +gKOomv4p07wdKNWlStRxPA91xQtzPwBIZXkNq2oeJQzAAt5mrL1LBmuaV4oqgX5n +m7pSYHPEFfe7wVDJCKW6V0o6GxBzHOF7tpQDS65RsIJAOloknO4NWF2uuil6yjOe +soHCL47BJ89A8AShP/U3wsr8rFNtqVNpT+F2ZAwlgak3A/I5czTSwXx4GByoaxbn +5+CdKa/Y5Gk5eZVpuXtcXQGc1PfzSEUTZJXXCm5y2kMiJG8+WnDcwJLgLeVX+OQr +J+71/xuzAYN6 +-----END CERTIFICATE----- +""" + +VERISIGN_ROOT = """-----BEGIN CERTIFICATE----- +MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl +cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu +LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT +aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD +VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT +aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ +bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu +IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg +LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b +N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t +KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu +kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm +CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ +Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu +imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te +2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe +DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC +/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p +F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt +TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== +-----END CERTIFICATE----- +""" + diff --git a/lemur/common/services/issuers/plugins/verisign/verisign.py b/lemur/plugins/lemur_verisign/plugin.py similarity index 75% rename from lemur/common/services/issuers/plugins/verisign/verisign.py rename to lemur/plugins/lemur_verisign/plugin.py index 2b3ca1cd..59adaeaa 100644 --- a/lemur/common/services/issuers/plugins/verisign/verisign.py +++ b/lemur/plugins/lemur_verisign/plugin.py @@ -1,5 +1,5 @@ """ -.. module: lemur.common.services.issuers.plugins.verisign.verisign +.. module: lemur.plugins.lemur_verisign.verisign :platform: Unix :synopsis: This module is responsible for communicating with the VeriSign VICE 2.0 API. :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more @@ -13,10 +13,9 @@ import xmltodict from flask import current_app -from lemur.common.services.issuers.issuer import Issuer -from lemur.common.services.issuers.plugins import verisign - -from lemur.certificates.exceptions import InsufficientDomains +from lemur.plugins.bases import IssuerPlugin +from lemur.plugins import lemur_verisign as verisign +from lemur.plugins.lemur_verisign import constants # https://support.venafi.com/entries/66445046-Info-VeriSign-Error-Codes @@ -54,11 +53,29 @@ VERISIGN_ERRORS = { "0x6013": "only supports DSA keys with (2048, 256) as the bit lengths of the prime parameter pair (p, q), other DSA key sizes will get this error", "0x600d": "RSA key size < 2A048", "0x4828": "Verisign certificates can be at most two years in length", - "0x3043": "Certificates must have a validity of at least 1 day" + "0x3043": "Certificates must have a validity of at least 1 day", + "0x950b": "CSR: Invalid State", } -class Verisign(Issuer): +def handle_response(content): + """ + Helper function that helps with parsing responses from the Verisign API. + :param content: + :return: :raise Exception: + """ + d = xmltodict.parse(content) + global VERISIGN_ERRORS + if d.get('Error'): + status_code = d['Error']['StatusCode'] + elif d.get('Response'): + status_code = d['Response']['StatusCode'] + if status_code in VERISIGN_ERRORS.keys(): + raise Exception(VERISIGN_ERRORS[status_code]) + return d + + +class VerisignPlugin(IssuerPlugin): title = 'VeriSign' slug = 'verisign' description = 'Enables the creation of certificates by the VICE2.0 verisign API.' @@ -70,24 +87,7 @@ class Verisign(Issuer): def __init__(self, *args, **kwargs): self.session = requests.Session() self.session.cert = current_app.config.get('VERISIGN_PEM_PATH') - super(Verisign, self).__init__(*args, **kwargs) - - @staticmethod - def handle_response(content): - """ - Helper function that helps with parsing responses from the Verisign API. - :param content: - :return: :raise Exception: - """ - d = xmltodict.parse(content) - global VERISIGN_ERRORS - if d.get('Error'): - status_code = d['Error']['StatusCode'] - elif d.get('Response'): - status_code = d['Response']['StatusCode'] - if status_code in VERISIGN_ERRORS.keys(): - raise Exception(VERISIGN_ERRORS[status_code]) - return d + super(VerisignPlugin, self).__init__(*args, **kwargs) def create_certificate(self, csr, issuer_options): """ @@ -126,42 +126,8 @@ class Verisign(Issuer): current_app.logger.info("Requesting a new verisign certificate: {0}".format(data)) response = self.session.post(url, data=data) - cert = self.handle_response(response.content)['Response']['Certificate'] - return cert, verisign.constants.VERISIGN_INTERMEDIATE, - - def get_csr_config(self, issuer_options): - """ - Used to generate a valid CSR for the given Certificate Authority. - - :param issuer_options: - :return: :raise InsufficientDomains: - """ - domains = [] - - if issuer_options.get('commonName'): - domains.append(issuer_options.get('commonName')) - - if issuer_options.get('extensions'): - for n in issuer_options['extensions']['subAltNames']['names']: - if n['value']: - domains.append(n['value']) - - is_san_comment = "#" - - dns_lines = [] - if len(domains) < 1: - raise InsufficientDomains - - elif len(domains) > 1: - is_san_comment = "" - for domain_line in list(set(domains)): - dns_lines.append("DNS.{} = {}".format(len(dns_lines) + 1, domain_line)) - - return verisign.constants.CSR_CONFIG.format( - is_san_comment=is_san_comment, - OU=issuer_options.get('organizationalUnit', 'Operations'), - DNS=domains, - DNS_LINES="\n".join(dns_lines)) + cert = handle_response(response.content)['Response']['Certificate'] + return cert, constants.VERISIGN_INTERMEDIATE, @staticmethod def create_authority(options): @@ -173,7 +139,7 @@ class Verisign(Issuer): :return: """ role = {'username': '', 'password': '', 'name': 'verisign'} - return verisign.constants.VERISIGN_ROOT, "", [role] + return constants.VERISIGN_ROOT, "", [role] def get_available_units(self): """ @@ -184,11 +150,5 @@ class Verisign(Issuer): """ url = current_app.config.get("VERISIGN_URL") + '/getTokens' response = self.session.post(url, headers={'content-type': 'application/x-www-form-urlencoded'}) - return self.handle_response(response.content)['Response']['Order'] + return handle_response(response.content)['Response']['Order'] - def get_authorities(self): - pass - - -def init(): - return Verisign() diff --git a/lemur/pytest.py b/lemur/pytest.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/static/app/angular/authorities/view/view.tpl.html b/lemur/static/app/angular/authorities/view/view.tpl.html index 35dfb4d4..9bbbaeb7 100644 --- a/lemur/static/app/angular/authorities/view/view.tpl.html +++ b/lemur/static/app/angular/authorities/view/view.tpl.html @@ -29,9 +29,9 @@ diff --git a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html index cfd0e582..e2262213 100644 --- a/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/distinguishedName.tpl.html @@ -16,7 +16,7 @@ State
- +

You must enter a state

diff --git a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html index 025f41d2..4dab6044 100644 --- a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html @@ -20,40 +20,20 @@

You must give a short description about this authority will be used for, this description should only include alphanumeric characters

-
+
- + class="form-control" typeahead-wait-ms="100" typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
-
-
- -
-
- - - - -
- - - - - -
{{ domain.value }} - -
-
-
-
-
+
@@ -61,19 +41,19 @@
-
- +

You must enter a common name

-