Adding support for multiple plugin types.

This commit is contained in:
kevgliss 2015-07-10 17:09:22 -07:00
parent c79905cd92
commit a30a8481d0
8 changed files with 256 additions and 71 deletions

View File

@ -8,7 +8,6 @@
from flask import current_app from flask import current_app
from lemur.common.managers import InstanceManager from lemur.common.managers import InstanceManager
# inspired by https://github.com/getsentry/sentry # inspired by https://github.com/getsentry/sentry
class PluginManager(InstanceManager): class PluginManager(InstanceManager):
def __iter__(self): def __iter__(self):
@ -17,8 +16,10 @@ class PluginManager(InstanceManager):
def __len__(self): def __len__(self):
return sum(1 for i in self.all()) 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()): 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(): if not plugin.is_enabled():
continue continue
if version is not None and plugin.__version__ != version: if version is not None and plugin.__version__ != version:

View File

@ -47,12 +47,13 @@ class IPlugin(local):
# Configuration specifics # Configuration specifics
conf_key = None conf_key = None
conf_title = None conf_title = None
options = {}
# Global enabled state # Global enabled state
enabled = True enabled = True
can_disable = True can_disable = True
def is_enabled(self, project=None): def is_enabled(self):
""" """
Returns a boolean representing if this plugin is enabled. Returns a boolean representing if this plugin is enabled.
If ``project`` is passed, it will limit the scope to that project. If ``project`` is passed, it will limit the scope to that project.

View File

@ -1,2 +1,3 @@
from .destination import DestinationPlugin # NOQA from .destination import DestinationPlugin # NOQA
from .issuer import IssuerPlugin # NOQA from .issuer import IssuerPlugin # NOQA
from .source import SourcePlugin

View File

@ -13,6 +13,8 @@ class IssuerPlugin(Plugin):
This is the base class from which all of the supported This is the base class from which all of the supported
issuers will inherit from. issuers will inherit from.
""" """
type = 'issuer'
def create_certificate(self): def create_certificate(self):
raise NotImplementedError raise NotImplementedError

View File

@ -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 <kglisson@netflix.com>
"""
from lemur.plugins.base import Plugin
class SourcePlugin(Plugin):
type = 'source'
def get_certificates(self):
raise NotImplemented
def get_options(self):
return {}

View File

@ -18,12 +18,12 @@ from requests.adapters import HTTPAdapter
from flask import current_app from flask import current_app
from lemur.exceptions import LemurException 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.plugins import lemur_cloudca as cloudca
from lemur.authorities import service as authority_service 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): 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)) raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
class CloudCAPlugin(IssuerPlugin): class CloudCA(object):
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'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.session = requests.Session() self.session = requests.Session()
self.session.mount('https://', CloudCAHostNameCheckingAdapter()) self.session.mount('https://', CloudCAHostNameCheckingAdapter())
@ -162,7 +154,69 @@ class CloudCAPlugin(IssuerPlugin):
else: else:
current_app.logger.warning("No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA") 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): def create_authority(self, options):
""" """
@ -205,22 +259,6 @@ class CloudCAPlugin(IssuerPlugin):
return cert, "".join(intermediates), roles, 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): def create_certificate(self, csr, options):
""" """
Creates a new certificate from cloudca Creates a new certificate from cloudca
@ -259,16 +297,25 @@ class CloudCAPlugin(IssuerPlugin):
return cert, "".join(intermediates), return cert, "".join(intermediates),
def random(self, length=10):
"""
Uses CloudCA as a decent source of randomness.
:param length: class CloudCASourcePlugin(SourcePlugin, CloudCA):
:return: title = 'CloudCA'
""" slug = 'cloudca-source'
endpoint = '/v1/random/{0}'.format(length) description = 'Discovers all SSL certificates in CloudCA'
response = self.session.get(self.url + endpoint, verify=self.ca_bundle) version = cloudca.VERSION
return response
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): def get_cert(self, ca_name=None, cert_handle=None):
""" """
@ -297,29 +344,3 @@ class CloudCAPlugin(IssuerPlugin):
}) })
return certs 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)

View File

@ -75,9 +75,9 @@ def handle_response(content):
return d return d
class VerisignPlugin(IssuerPlugin): class VerisignIssuerPlugin(IssuerPlugin):
title = 'VeriSign' title = 'Verisign'
slug = 'verisign' slug = 'verisign-issuer'
description = 'Enables the creation of certificates by the VICE2.0 verisign API.' description = 'Enables the creation of certificates by the VICE2.0 verisign API.'
version = verisign.VERSION version = verisign.VERSION
@ -87,7 +87,7 @@ class VerisignPlugin(IssuerPlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.session = requests.Session() self.session = requests.Session()
self.session.cert = current_app.config.get('VERISIGN_PEM_PATH') 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): def create_certificate(self, csr, issuer_options):
""" """

140
lemur/plugins/views.py Normal file
View File

@ -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 <kglisson@netflix.com>
"""
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/<plugin_type>', endpoint='pluginType')