Initial work on #125
This commit is contained in:
parent
ff4cdd82ee
commit
920d595c12
|
@ -77,6 +77,24 @@ def find_duplicates(cert_body):
|
||||||
return Certificate.query.filter_by(body=cert_body).all()
|
return Certificate.query.filter_by(body=cert_body).all()
|
||||||
|
|
||||||
|
|
||||||
|
def export(cert_id, export_options):
|
||||||
|
"""
|
||||||
|
Exports a certificate to the requested format. This format
|
||||||
|
may be a binary format.
|
||||||
|
:param export_options:
|
||||||
|
:param cert_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
cert = get(cert_id)
|
||||||
|
export_plugin = plugins.get(export_options['slug'])
|
||||||
|
|
||||||
|
data = export_plugin.export(cert.body, cert.key)
|
||||||
|
if export_options.get('encrypted'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def update(cert_id, owner, description, active, destinations, notifications, replaces):
|
def update(cert_id, owner, description, active, destinations, notifications, replaces):
|
||||||
"""
|
"""
|
||||||
Updates a certificate
|
Updates a certificate
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
:license: Apache, see LICENSE for more details.
|
:license: Apache, see LICENSE for more details.
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
@ -13,20 +12,7 @@ from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from lemur.utils import mktempfile
|
||||||
from contextlib import contextmanager
|
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def mktempfile():
|
|
||||||
with NamedTemporaryFile(delete=False) as f:
|
|
||||||
name = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield name
|
|
||||||
finally:
|
|
||||||
os.unlink(name)
|
|
||||||
|
|
||||||
|
|
||||||
def ocsp_verify(cert_path, issuer_chain_path):
|
def ocsp_verify(cert_path, issuer_chain_path):
|
||||||
|
|
|
@ -775,6 +775,55 @@ class CertificatesReplacementsList(AuthenticatedResource):
|
||||||
return service.get(certificate_id).replaces
|
return service.get(certificate_id).replaces
|
||||||
|
|
||||||
|
|
||||||
|
class CertficiateExport(AuthenticatedResource):
|
||||||
|
def __init__(self):
|
||||||
|
self.reqparse = reqparse.RequestParser()
|
||||||
|
super(CertficiateExport, self).__init__()
|
||||||
|
|
||||||
|
def post(self, certificate_id):
|
||||||
|
"""
|
||||||
|
.. http:post:: /certificates/1/export
|
||||||
|
|
||||||
|
Export a certificate
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PUT /certificates/1/export 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
|
||||||
|
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth token to authenticate
|
||||||
|
:statuscode 200: no error
|
||||||
|
:statuscode 403: unauthenticated
|
||||||
|
"""
|
||||||
|
args = self.reqparse.parse_args()
|
||||||
|
|
||||||
|
cert = service.get(certificate_id)
|
||||||
|
role = role_service.get_by_name(cert.owner)
|
||||||
|
|
||||||
|
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||||
|
|
||||||
|
if permission.can():
|
||||||
|
data = service.export(certificate_id)
|
||||||
|
response = make_response(data)
|
||||||
|
response.headers['content-type'] = 'application/octet-stream'
|
||||||
|
return response
|
||||||
|
|
||||||
|
return dict(message='You are not authorized to export this certificate'), 403
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||||
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
||||||
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
||||||
|
|
|
@ -2,3 +2,4 @@ from .destination import DestinationPlugin # noqa
|
||||||
from .issuer import IssuerPlugin # noqa
|
from .issuer import IssuerPlugin # noqa
|
||||||
from .source import SourcePlugin # noqa
|
from .source import SourcePlugin # noqa
|
||||||
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa
|
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa
|
||||||
|
from .export import ExportPlugin # noqa
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.bases.export
|
||||||
|
: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 ExportPlugin(Plugin):
|
||||||
|
type = 'export'
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
raise NotImplemented
|
|
@ -0,0 +1,5 @@
|
||||||
|
try:
|
||||||
|
VERSION = __import__('pkg_resources') \
|
||||||
|
.get_distribution(__name__).version
|
||||||
|
except Exception as e:
|
||||||
|
VERSION = 'unknown'
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.plugins.lemur_java.plugin
|
||||||
|
: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>
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from lemur.utils import mktempfile, mktemppath
|
||||||
|
from lemur.plugins.bases import ExportPlugin
|
||||||
|
from lemur.plugins import lemur_java as java
|
||||||
|
from lemur.common.utils import get_psuedo_random_string
|
||||||
|
|
||||||
|
|
||||||
|
def run_process(command):
|
||||||
|
"""
|
||||||
|
Runs a given command with pOpen and wraps some
|
||||||
|
error handling around it.
|
||||||
|
:param command:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
|
||||||
|
if p.returncode != 0:
|
||||||
|
current_app.logger.debug(" ".join(command))
|
||||||
|
current_app.logger.error(stderr)
|
||||||
|
raise Exception(stderr)
|
||||||
|
|
||||||
|
|
||||||
|
class JavaExportPlugin(ExportPlugin):
|
||||||
|
title = 'Java'
|
||||||
|
slug = 'java-export'
|
||||||
|
description = 'Attempts to generate a JKS keystore or truststore'
|
||||||
|
version = java.VERSION
|
||||||
|
|
||||||
|
author = 'Kevin Glisson'
|
||||||
|
author_url = 'https://github.com/netflix/lemur'
|
||||||
|
|
||||||
|
additional_options = [
|
||||||
|
{
|
||||||
|
'name': 'type',
|
||||||
|
'type': 'select',
|
||||||
|
'required': True,
|
||||||
|
'available': ['jks'],
|
||||||
|
'helpMessage': 'Choose the format you wish to export',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'passphrase',
|
||||||
|
'type': 'str',
|
||||||
|
'required': False,
|
||||||
|
'helpMessage': 'If no passphrase is generated one will be generated for you.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'alias',
|
||||||
|
'type': 'str',
|
||||||
|
'required': False,
|
||||||
|
'helpMessage': 'Enter the alias you wish to use for the keystore.',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def export(body, key, options, **kwargs):
|
||||||
|
"""
|
||||||
|
Generates a Java Keystore or Truststore
|
||||||
|
|
||||||
|
:param key:
|
||||||
|
:param kwargs:
|
||||||
|
:param body:
|
||||||
|
:param options:
|
||||||
|
"""
|
||||||
|
if options.get('passphrase'):
|
||||||
|
passphrase = options['passphrase']
|
||||||
|
else:
|
||||||
|
passphrase = get_psuedo_random_string()
|
||||||
|
|
||||||
|
with mktempfile() as cert_tmp:
|
||||||
|
with open(cert_tmp, 'w') as f:
|
||||||
|
f.write(body)
|
||||||
|
with mktempfile() as key_tmp:
|
||||||
|
with open(key_tmp, 'w') as f:
|
||||||
|
f.write(key)
|
||||||
|
|
||||||
|
# Create PKCS12 keystore from private key and public certificate
|
||||||
|
with mktempfile() as p12_tmp:
|
||||||
|
run_process([
|
||||||
|
"openssl",
|
||||||
|
"pkcs12",
|
||||||
|
"-export",
|
||||||
|
"-name", options.get('alias', 'cert'),
|
||||||
|
"-in", cert_tmp,
|
||||||
|
"-inkey", key_tmp,
|
||||||
|
"-out", p12_tmp,
|
||||||
|
"-password", "pass:{}".format(passphrase)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Convert PKCS12 keystore into a JKS keystore
|
||||||
|
with mktemppath() as jks_tmp:
|
||||||
|
run_process([
|
||||||
|
"keytool",
|
||||||
|
"-importkeystore",
|
||||||
|
"-destkeystore", jks_tmp,
|
||||||
|
"-srckeystore", p12_tmp,
|
||||||
|
"-srcstoretype", "PKCS12",
|
||||||
|
"-alias", options.get('alias', 'cert'),
|
||||||
|
"-srcstorepass", passphrase,
|
||||||
|
"-deststorepass", passphrase
|
||||||
|
])
|
||||||
|
|
||||||
|
# Import signed cert in to JKS keystore
|
||||||
|
run_process([
|
||||||
|
"keytool",
|
||||||
|
"-importcert",
|
||||||
|
"-file", cert_tmp,
|
||||||
|
"-keystore", jks_tmp,
|
||||||
|
"-alias", "{0}_cert".format(options.get('alias'), 'cert'),
|
||||||
|
"-storepass", passphrase,
|
||||||
|
"-noprompt"
|
||||||
|
])
|
||||||
|
|
||||||
|
with open(jks_tmp, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
return raw
|
|
@ -0,0 +1 @@
|
||||||
|
from lemur.tests.conftest import * # noqa
|
|
@ -0,0 +1,63 @@
|
||||||
|
PRIVATE_KEY_STR = b"""
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEogIBAAKCAQEAsXn+QZRATxryRmGXI4fdI+0a2oBwuVh8fC/9bcqX6c5eDmgc
|
||||||
|
rj6esmc1hpIFxMM3DvkFXX6xISkU6B5fmYDEGZLi7NvcXF3+EoA/SCkP1MFlvqhn
|
||||||
|
EvNhb0t1fBLs0i/0gfTS/FHBZY1ekHisd/sUetCDZ7F11RxMwws0Oc8bl7j1TpRc
|
||||||
|
awXFAsh/aWwQOwFeyWU7TtZeAE7sMyWXInBg37tKk1wlv+mN+27WijI091+amkVy
|
||||||
|
zIV6mA5OHfqbjuqV8uQflN8jE244Qr7shtSk7LpBpWf0M6dC7dXbuUctHFhqcDjy
|
||||||
|
3IRUl+NisKRoMtq+a0uehfmpFNSUD7F4gdUtSwIDAQABAoIBAGITsZ+aBuPwVzzv
|
||||||
|
x286MMoeyL1BR4oVzU1v09Rtpf/uLGo3vMnKDzc19A12+rseynl6wi1FyysxIb2Y
|
||||||
|
s2oID9a2JrOQWLmus66TsuT01CvV6J0xQSzm1MyFXdqANuF84NlEa6hGoeK1+jFK
|
||||||
|
jr0LQukP+9484oovxnfu5CCiRHRWNZmeuekuYhI1SJf343Tr6jwvyr6KZpnIy0Yt
|
||||||
|
axuuIZdCfY9ZV2vFG89GwwgwVQrhf14Kv5vBMZrNh1lRGsr0Sqlx5cGkPRAy90lg
|
||||||
|
HjrRMogrtXr3AR5Pk2qqAYXzZBU2EFhJ3k2njpwOzlSj0r0ZwTmejZ89cco0sW5j
|
||||||
|
+eQ6aRECgYEA1tkNW75fgwU52Va5VETCzG8II/pZdqNygnoc3z8EutN+1w8f6Tr+
|
||||||
|
PdpKSICW0z7Iq4f5k/4wrA5xw1vy5RBMH0ZP29GwHTvCPiTBboR9vWvxQvZn1jb9
|
||||||
|
wvKa0RxE18KcF0YIyTnZMubkA17QTFlvCNyZg0iCqeyFYPyqVE+R4AkCgYEA03h1
|
||||||
|
XrqECZDDbG9HLUdGbkZNk4VzTcF6dQ3GAPY8M/H7rw5BbvH0RZLOrzl46DDVzKTg
|
||||||
|
B1VOReAHsxBKFdkqeq1A99CLDow6vHTIEG8DwxkA7/2QPkt8MybwdApUyYnQh5/v
|
||||||
|
CxwkRt4Mm+EiYfn5iyL8yI+vaQSRToVO/3BND7MCgYAJQSpBJG8qzqPSR9kN1zRo
|
||||||
|
5/N60ULfSGUbV7U8rJNAlPGmw+EFA+SFt4xxmRBmIxMzyFSo2k8waiLeXmyVD2Go
|
||||||
|
CzhPaLXkXHmegajPYOelrCulTcXlRVMi/Z5LmaMhhCGDIyInwNUpSybROllQoJ2W
|
||||||
|
zSHTtODj/usz5U5U+WR4OQKBgHQRosI6t2wUo96peTS18UdnmP7GeZINBuymga5X
|
||||||
|
eJW+VLkxpuKBNOTW/lCYx+8Rlte7CyebP9oEa9VxtGgniTRKUeVy9lAm0bpMkt7K
|
||||||
|
QBNebvBKiVhX0DS3Q7U9UmpIFUfLlcXQTW0ERYFtYZTLQpeGvZ5LlyiaFDM34jM7
|
||||||
|
7WAXAoGANDPJdQLEuimCOAMx/xoecNWeZIP6ieB0hVBrwLNxsaZlkn1KodUMuvla
|
||||||
|
VEowbtPRdc9o3VZRh4q9cEakssTvOD70hgUZCFcMarmc37RgRvvD2fsZmDZF6qd3
|
||||||
|
QfHplREs9F0sW+eiirczG7up4XL+CA162TtZxW+2GAiQhwhE5jA=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXTERNAL_VALID_STR = b"""
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID2zCCAsOgAwIBAgICA+0wDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAlVT
|
||||||
|
MRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlMb3MgR2F0b3MxDTALBgNV
|
||||||
|
BAMMBHRlc3QxFjAUBgNVBAoMDU5ldGZsaXgsIEluYy4xEzARBgNVBAsMCk9wZXJh
|
||||||
|
dGlvbnMxIzAhBgkqhkiG9w0BCQEWFGtnbGlzc29uQG5ldGZsaXguY29tMB4XDTE1
|
||||||
|
MTEyMzIxNDIxMFoXDTE1MTEyNjIxNDIxMFowcjENMAsGA1UEAwwEdGVzdDEWMBQG
|
||||||
|
A1UECgwNTmV0ZmxpeCwgSW5jLjETMBEGA1UECwwKT3BlcmF0aW9uczELMAkGA1UE
|
||||||
|
BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCUxvcyBHYXRvczCC
|
||||||
|
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALF5/kGUQE8a8kZhlyOH3SPt
|
||||||
|
GtqAcLlYfHwv/W3Kl+nOXg5oHK4+nrJnNYaSBcTDNw75BV1+sSEpFOgeX5mAxBmS
|
||||||
|
4uzb3Fxd/hKAP0gpD9TBZb6oZxLzYW9LdXwS7NIv9IH00vxRwWWNXpB4rHf7FHrQ
|
||||||
|
g2exddUcTMMLNDnPG5e49U6UXGsFxQLIf2lsEDsBXsllO07WXgBO7DMllyJwYN+7
|
||||||
|
SpNcJb/pjftu1ooyNPdfmppFcsyFepgOTh36m47qlfLkH5TfIxNuOEK+7IbUpOy6
|
||||||
|
QaVn9DOnQu3V27lHLRxYanA48tyEVJfjYrCkaDLavmtLnoX5qRTUlA+xeIHVLUsC
|
||||||
|
AwEAAaNVMFMwUQYDVR0fBEowSDBGoESgQoZAaHR0cDovL3Rlc3QuY2xvdWRjYS5j
|
||||||
|
cmwubmV0ZmxpeC5jb20vdGVzdERlY3JpcHRpb25DQVJvb3QvY3JsLnBlbTANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAQEAiHREBKg7zhlQ/N7hDIkxgodRSWD7CVbJGSCdkR3Pvr6+
|
||||||
|
jHBVNTJUrYqy7sL2pIutoeiSTQEH65/Gbm30mOnNu+lvFKxTxzof6kNYv8cyc8sX
|
||||||
|
eBuBfSrlTodPFSHXQIpOexZgA0f30LOuXegqzxgXkKg+uMXOez5Zo5pNjTUow0He
|
||||||
|
oe+V1hfYYvL1rocCmBOkhIGWz7622FxKDawRtZTGVsGsMwMIWyvS3+KQ04K8yHhp
|
||||||
|
bQOg9zZAoYQuHY1inKBnA0II8eW0hPpJrlZoSqN8Tp0NSBpFiUk3m7KNFP2kITIf
|
||||||
|
tTneAgyUsgfDxNDifZryZSzg7MH31sTBcYaotSmTXw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_certificate_to_jks(app):
|
||||||
|
from lemur.plugins.base import plugins
|
||||||
|
p = plugins.get('java-export')
|
||||||
|
options = {'passphrase': 'test1234'}
|
||||||
|
raw = p.export(EXTERNAL_VALID_STR, PRIVATE_KEY_STR, options)
|
||||||
|
assert raw != b""
|
|
@ -43,7 +43,7 @@ LOG_FILE = "lemur.log"
|
||||||
|
|
||||||
# modify this if you are not using a local database
|
# modify this if you are not using a local database
|
||||||
SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
|
SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
# AWS
|
# AWS
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,35 @@
|
||||||
:license: Apache, see LICENSE for more details.
|
:license: Apache, see LICENSE for more details.
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import six
|
import six
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from cryptography.fernet import Fernet, MultiFernet
|
from cryptography.fernet import Fernet, MultiFernet
|
||||||
import sqlalchemy.types as types
|
import sqlalchemy.types as types
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mktempfile():
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||||
|
name = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield name
|
||||||
|
finally:
|
||||||
|
os.unlink(name)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mktemppath():
|
||||||
|
try:
|
||||||
|
path = os.path.join(tempfile._get_default_tempdir(), next(tempfile._get_candidate_names()))
|
||||||
|
yield path
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
def get_keys():
|
def get_keys():
|
||||||
"""
|
"""
|
||||||
|
@ -26,7 +50,7 @@ def get_keys():
|
||||||
# the fact that there is not a current_app with a config at that point
|
# the fact that there is not a current_app with a config at that point
|
||||||
try:
|
try:
|
||||||
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||||
except:
|
except Exception:
|
||||||
print("no encryption keys")
|
print("no encryption keys")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -154,6 +154,7 @@ setup(
|
||||||
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
|
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
|
||||||
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin',
|
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin',
|
||||||
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',
|
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',
|
||||||
|
'java_export = lemur.plugins.lemur_java.plugin:JavaExportPlugin'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
Loading…
Reference in New Issue