From a30a8481d0b7d07b3bd47bec7a660adfa3a44fff Mon Sep 17 00:00:00 2001 From: kevgliss Date: Fri, 10 Jul 2015 17:09:22 -0700 Subject: [PATCH] Adding support for multiple plugin types. --- lemur/plugins/base/manager.py | 5 +- lemur/plugins/base/v1.py | 3 +- lemur/plugins/bases/__init__.py | 3 +- lemur/plugins/bases/issuer.py | 2 + lemur/plugins/bases/source.py | 19 ++++ lemur/plugins/lemur_cloudca/plugin.py | 147 ++++++++++++++----------- lemur/plugins/lemur_verisign/plugin.py | 8 +- lemur/plugins/views.py | 140 +++++++++++++++++++++++ 8 files changed, 256 insertions(+), 71 deletions(-) create mode 100644 lemur/plugins/views.py diff --git a/lemur/plugins/base/manager.py b/lemur/plugins/base/manager.py index 32234cdc..0ec270d0 100644 --- a/lemur/plugins/base/manager.py +++ b/lemur/plugins/base/manager.py @@ -8,7 +8,6 @@ from flask import current_app from lemur.common.managers import InstanceManager - # inspired by https://github.com/getsentry/sentry class PluginManager(InstanceManager): def __iter__(self): @@ -17,8 +16,10 @@ class PluginManager(InstanceManager): def __len__(self): return sum(1 for i in self.all()) - def all(self, version=1): + def all(self, version=1, plugin_type=None): for plugin in sorted(super(PluginManager, self).all(), key=lambda x: x.get_title()): + if not plugin.type == plugin_type and plugin_type: + continue if not plugin.is_enabled(): continue if version is not None and plugin.__version__ != version: diff --git a/lemur/plugins/base/v1.py b/lemur/plugins/base/v1.py index 448e6d95..2055577b 100644 --- a/lemur/plugins/base/v1.py +++ b/lemur/plugins/base/v1.py @@ -47,12 +47,13 @@ class IPlugin(local): # Configuration specifics conf_key = None conf_title = None + options = {} # Global enabled state enabled = True can_disable = True - def is_enabled(self, project=None): + def is_enabled(self): """ Returns a boolean representing if this plugin is enabled. If ``project`` is passed, it will limit the scope to that project. diff --git a/lemur/plugins/bases/__init__.py b/lemur/plugins/bases/__init__.py index d43aa85e..2e501d35 100644 --- a/lemur/plugins/bases/__init__.py +++ b/lemur/plugins/bases/__init__.py @@ -1,2 +1,3 @@ from .destination import DestinationPlugin # NOQA -from .issuer import IssuerPlugin # NOQA \ No newline at end of file +from .issuer import IssuerPlugin # NOQA +from .source import SourcePlugin \ No newline at end of file diff --git a/lemur/plugins/bases/issuer.py b/lemur/plugins/bases/issuer.py index b2e5c964..bfa7dbd6 100644 --- a/lemur/plugins/bases/issuer.py +++ b/lemur/plugins/bases/issuer.py @@ -13,6 +13,8 @@ class IssuerPlugin(Plugin): This is the base class from which all of the supported issuers will inherit from. """ + type = 'issuer' + def create_certificate(self): raise NotImplementedError diff --git a/lemur/plugins/bases/source.py b/lemur/plugins/bases/source.py index e69de29b..a706acf2 100644 --- a/lemur/plugins/bases/source.py +++ b/lemur/plugins/bases/source.py @@ -0,0 +1,19 @@ +""" +.. module: lemur.bases.source + :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 SourcePlugin(Plugin): + type = 'source' + + def get_certificates(self): + raise NotImplemented + + def get_options(self): + return {} + diff --git a/lemur/plugins/lemur_cloudca/plugin.py b/lemur/plugins/lemur_cloudca/plugin.py index 8d42cb3b..68de48d3 100644 --- a/lemur/plugins/lemur_cloudca/plugin.py +++ b/lemur/plugins/lemur_cloudca/plugin.py @@ -18,12 +18,12 @@ from requests.adapters import HTTPAdapter from flask import current_app from lemur.exceptions import LemurException -from lemur.plugins.bases import IssuerPlugin +from lemur.plugins.bases import IssuerPlugin, SourcePlugin from lemur.plugins import lemur_cloudca as cloudca from lemur.authorities import service as authority_service -API_ENDPOINT = '/v1/ca/netflix' +API_ENDPOINT = '/v1/ca/netflix' # TODO this should be configurable class CloudCAException(LemurException): @@ -142,15 +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 CloudCAPlugin(IssuerPlugin): - title = 'CloudCA' - slug = 'cloudca' - description = 'Enables the creation of certificates from the cloudca API.' - version = cloudca.VERSION - - author = 'Kevin Glisson' - author_url = 'https://github.com/netflix/lemur' - +class CloudCA(object): def __init__(self, *args, **kwargs): self.session = requests.Session() self.session.mount('https://', CloudCAHostNameCheckingAdapter()) @@ -162,7 +154,69 @@ class CloudCAPlugin(IssuerPlugin): else: current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA") - super(CloudCAPlugin, self).__init__(*args, **kwargs) + super(CloudCA, self).__init__(*args, **kwargs) + + def post(self, endpoint, data): + """ + HTTP POST to CloudCA + + :param endpoint: + :param data: + :return: + """ + data = dumps(dict(data.items() + get_auth_data(data['caName']).items())) + + # we set a low timeout, if cloudca is down it shouldn't bring down + # lemur + response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle) + return process_response(response) + + def get(self, endpoint): + """ + HTTP GET to CloudCA + + :param endpoint: + :return: + """ + response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle) + return process_response(response) + + def random(self, length=10): + """ + Uses CloudCA as a decent source of randomness. + + :param length: + :return: + """ + endpoint = '/v1/random/{0}'.format(length) + response = self.session.get(self.url + endpoint, verify=self.ca_bundle) + return response + + def get_authorities(self): + """ + Retrieves authorities that were made outside of Lemur. + + :return: + """ + endpoint = '{0}/listCAs'.format(API_ENDPOINT) + authorities = [] + for ca in self.get(endpoint)['data']['caList']: + try: + authorities.append(ca['caName']) + except AttributeError as e: + current_app.logger.error("No authority has been defined for {}".format(ca['caName'])) + + return authorities + + +class CloudCAIssuerPlugin(IssuerPlugin, CloudCA): + title = 'CloudCA' + slug = 'cloudca-issuer' + description = 'Enables the creation of certificates from the cloudca API.' + version = cloudca.VERSION + + author = 'Kevin Glisson' + author_url = 'https://github.com/netflix/lemur' def create_authority(self, options): """ @@ -205,22 +259,6 @@ class CloudCAPlugin(IssuerPlugin): return cert, "".join(intermediates), roles, - def get_authorities(self): - """ - Retrieves authorities that were made outside of Lemur. - - :return: - """ - endpoint = '{0}/listCAs'.format(API_ENDPOINT) - authorities = [] - for ca in self.get(endpoint)['data']['caList']: - try: - authorities.append(ca['caName']) - except AttributeError as e: - current_app.logger.error("No authority has been defined for {}".format(ca['caName'])) - - return authorities - def create_certificate(self, csr, options): """ Creates a new certificate from cloudca @@ -259,16 +297,25 @@ class CloudCAPlugin(IssuerPlugin): return cert, "".join(intermediates), - def random(self, length=10): - """ - Uses CloudCA as a decent source of randomness. - :param length: - :return: - """ - endpoint = '/v1/random/{0}'.format(length) - response = self.session.get(self.url + endpoint, verify=self.ca_bundle) - return response +class CloudCASourcePlugin(SourcePlugin, CloudCA): + title = 'CloudCA' + slug = 'cloudca-source' + description = 'Discovers all SSL certificates in CloudCA' + version = cloudca.VERSION + + author = 'Kevin Glisson' + author_url = 'https://github.com/netflix/lemur' + + options = { + 'pollRate': {'type': 'int', 'default': '60'} + } + + def get_certificates(self, **kwargs): + certs = [] + for authority in self.get_authorities(): + certs += self.get_cert(ca_name=authority) + return def get_cert(self, ca_name=None, cert_handle=None): """ @@ -297,29 +344,3 @@ class CloudCAPlugin(IssuerPlugin): }) return certs - - def post(self, endpoint, data): - """ - HTTP POST to CloudCA - - :param endpoint: - :param data: - :return: - """ - data = dumps(dict(data.items() + get_auth_data(data['caName']).items())) - - # we set a low timeout, if cloudca is down it shouldn't bring down - # lemur - response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle) - return process_response(response) - - def get(self, endpoint): - """ - HTTP GET to CloudCA - - :param endpoint: - :return: - """ - response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle) - return process_response(response) - diff --git a/lemur/plugins/lemur_verisign/plugin.py b/lemur/plugins/lemur_verisign/plugin.py index 59adaeaa..eb00907d 100644 --- a/lemur/plugins/lemur_verisign/plugin.py +++ b/lemur/plugins/lemur_verisign/plugin.py @@ -75,9 +75,9 @@ def handle_response(content): return d -class VerisignPlugin(IssuerPlugin): - title = 'VeriSign' - slug = 'verisign' +class VerisignIssuerPlugin(IssuerPlugin): + title = 'Verisign' + slug = 'verisign-issuer' description = 'Enables the creation of certificates by the VICE2.0 verisign API.' version = verisign.VERSION @@ -87,7 +87,7 @@ class VerisignPlugin(IssuerPlugin): def __init__(self, *args, **kwargs): self.session = requests.Session() self.session.cert = current_app.config.get('VERISIGN_PEM_PATH') - super(VerisignPlugin, self).__init__(*args, **kwargs) + super(VerisignIssuerPlugin, self).__init__(*args, **kwargs) def create_certificate(self, csr, issuer_options): """ diff --git a/lemur/plugins/views.py b/lemur/plugins/views.py new file mode 100644 index 00000000..a1b7a000 --- /dev/null +++ b/lemur/plugins/views.py @@ -0,0 +1,140 @@ +""" +.. module: lemur.plugins.views + :platform: Unix + :synopsis: This module contains all of the accounts view code. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import Blueprint +from flask.ext.restful import Api, reqparse, fields +from lemur.auth.service import AuthenticatedResource + +from lemur.common.utils import marshal_items + +from lemur.plugins.base import plugins + +mod = Blueprint('plugins', __name__) +api = Api(mod) + + +FIELDS = { + 'title': fields.String, + 'pluginOptions': fields.Raw(attribute='options'), + 'description': fields.String, + 'version': fields.String, + 'author': fields.String, + 'authorUrl': fields.String, + 'type': fields.String, + 'slug': fields.String, +} + + +class PluginsList(AuthenticatedResource): + """ Defines the 'plugins' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(PluginsList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /plugins + + The current plugin list + + **Example request**: + + .. sourcecode:: http + + GET /plugins 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": 2, + "accountNumber": 222222222, + "label": "account2", + "comments": "this is a thing" + }, + { + "id": 1, + "accountNumber": 11111111111, + "label": "account1", + "comments": "this is a thing" + }, + ] + "total": 2 + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return plugins.all() + + +class PluginsTypeList(AuthenticatedResource): + """ Defines the 'plugins' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(PluginsTypeList, self).__init__() + + @marshal_items(FIELDS) + def get(self, plugin_type): + """ + .. http:get:: /plugins/issuer + + The current plugin list + + **Example request**: + + .. sourcecode:: http + + GET /plugins/issuer 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": 2, + "accountNumber": 222222222, + "label": "account2", + "comments": "this is a thing" + }, + { + "id": 1, + "accountNumber": 11111111111, + "label": "account1", + "comments": "this is a thing" + }, + ] + "total": 2 + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return list(plugins.all(plugin_type=plugin_type)) + +api.add_resource(PluginsList, '/plugins', endpoint='plugins') +api.add_resource(PluginsTypeList, '/plugins/', endpoint='pluginType') +