Merge branch 'add-ca-cert-notifications' of github.com:jtschladen/lemur into add-ca-cert-notifications
This commit is contained in:
commit
b40cb5562a
|
@ -431,8 +431,8 @@ And the worker can be started with desired options such as the following::
|
||||||
|
|
||||||
supervisor or systemd configurations should be created for these in production environments as appropriate.
|
supervisor or systemd configurations should be created for these in production environments as appropriate.
|
||||||
|
|
||||||
Add support for LetsEncrypt
|
Add support for LetsEncrypt/ACME
|
||||||
===========================
|
================================
|
||||||
|
|
||||||
LetsEncrypt is a free, limited-feature certificate authority that offers publicly trusted certificates that are valid
|
LetsEncrypt is a free, limited-feature certificate authority that offers publicly trusted certificates that are valid
|
||||||
for 90 days. LetsEncrypt does not use organizational validation (OV), and instead relies on domain validation (DV).
|
for 90 days. LetsEncrypt does not use organizational validation (OV), and instead relies on domain validation (DV).
|
||||||
|
@ -440,7 +440,10 @@ LetsEncrypt requires that we prove ownership of a domain before we're able to is
|
||||||
time we want a certificate.
|
time we want a certificate.
|
||||||
|
|
||||||
The most common methods to prove ownership are HTTP validation and DNS validation. Lemur supports DNS validation
|
The most common methods to prove ownership are HTTP validation and DNS validation. Lemur supports DNS validation
|
||||||
through the creation of DNS TXT records.
|
through the creation of DNS TXT records as well as HTTP validation, reusing the destination concept.
|
||||||
|
|
||||||
|
ACME DNS Challenge
|
||||||
|
------------------
|
||||||
|
|
||||||
In a nutshell, when we send a certificate request to LetsEncrypt, they generate a random token and ask us to put that
|
In a nutshell, when we send a certificate request to LetsEncrypt, they generate a random token and ask us to put that
|
||||||
token in a DNS text record to prove ownership of a domain. If a certificate request has multiple domains, we must
|
token in a DNS text record to prove ownership of a domain. If a certificate request has multiple domains, we must
|
||||||
|
@ -478,6 +481,24 @@ possible. To enable this functionality, periodically (or through Cron/Celery) ru
|
||||||
This command will traverse all DNS providers, determine which zones they control, and upload this list of zones to
|
This command will traverse all DNS providers, determine which zones they control, and upload this list of zones to
|
||||||
Lemur's database (in the dns_providers table). Alternatively, you can manually input this data.
|
Lemur's database (in the dns_providers table). Alternatively, you can manually input this data.
|
||||||
|
|
||||||
|
ACME HTTP Challenge
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The flow for requesting a certificate using the HTTP challenge is not that different from the one described for the DNS
|
||||||
|
challenge. The only difference is, that instead of creating a DNS TXT record, a file is uploaded to a Webserver which
|
||||||
|
serves the file at `http://<domain>/.well-known/acme-challenge/<token>`
|
||||||
|
|
||||||
|
Currently the HTTP challenge also works without Celery, since it's done while creating the certificate, and doesn't
|
||||||
|
rely on celery to create the DNS record. This will change when we implement mix & match of acme challenge types.
|
||||||
|
|
||||||
|
To create a HTTP compatible Authority, you first need to create a new destination that will be used to deploy the
|
||||||
|
challenge token. Visit `Admin` -> `Destination` and click `Create`. The path you provide for the destination needs to
|
||||||
|
be the exact path that is called when the ACME providers calls ``http://<domain>/.well-known/acme-challenge/`. The
|
||||||
|
token part will be added dynamically by the acme_upload.
|
||||||
|
Currently only the SFTP and S3 Bucket destination support the ACME HTTP challenge.
|
||||||
|
|
||||||
|
Afterwards you can create a new certificate authority as described in the DNS challenge, but need to choose
|
||||||
|
`Acme HTTP-01` as the plugin type, and then the destination you created beforehand.
|
||||||
|
|
||||||
LetsEncrypt: pinning to cross-signed ICA
|
LetsEncrypt: pinning to cross-signed ICA
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
|
@ -211,7 +211,7 @@ class LdapPrincipal:
|
||||||
for group in lgroups:
|
for group in lgroups:
|
||||||
(dn, values) = group
|
(dn, values) = group
|
||||||
if type(values) == dict:
|
if type(values) == dict:
|
||||||
self.ldap_groups.append(values["cn"][0].decode("ascii"))
|
self.ldap_groups.append(values["cn"][0].decode("utf-8"))
|
||||||
else:
|
else:
|
||||||
lgroups = self.ldap_client.search_s(
|
lgroups = self.ldap_client.search_s(
|
||||||
self.ldap_base_dn, ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs
|
self.ldap_base_dn, ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs
|
||||||
|
|
|
@ -21,6 +21,7 @@ from lemur.certificates.schemas import CertificateOutputSchema, CertificateInput
|
||||||
from lemur.common.utils import generate_private_key, truthiness
|
from lemur.common.utils import generate_private_key, truthiness
|
||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
|
from lemur.endpoints import service as endpoint_service
|
||||||
from lemur.extensions import metrics, sentry, signals
|
from lemur.extensions import metrics, sentry, signals
|
||||||
from lemur.models import certificate_associations
|
from lemur.models import certificate_associations
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
|
@ -797,3 +798,61 @@ def reissue_certificate(certificate, replace=None, user=None):
|
||||||
new_cert = create(**primitives)
|
new_cert = create(**primitives)
|
||||||
|
|
||||||
return new_cert
|
return new_cert
|
||||||
|
|
||||||
|
|
||||||
|
def is_attached_to_endpoint(certificate_name, endpoint_name):
|
||||||
|
"""
|
||||||
|
Find if given certificate is attached to the endpoint. Both, certificate and endpoint, are identified by name.
|
||||||
|
This method talks to elb and finds the real time information.
|
||||||
|
:param certificate_name:
|
||||||
|
:param endpoint_name:
|
||||||
|
:return: True if certificate is attached to the given endpoint, False otherwise
|
||||||
|
"""
|
||||||
|
endpoint = endpoint_service.get_by_name(endpoint_name)
|
||||||
|
attached_certificates = endpoint.source.plugin.get_endpoint_certificate_names(endpoint)
|
||||||
|
return certificate_name in attached_certificates
|
||||||
|
|
||||||
|
|
||||||
|
def remove_from_destination(certificate, destination):
|
||||||
|
"""
|
||||||
|
Remove the certificate from given destination if clean() is implemented
|
||||||
|
:param certificate:
|
||||||
|
:param destination:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
plugin = plugins.get(destination.plugin_name)
|
||||||
|
if not hasattr(plugin, "clean"):
|
||||||
|
info_text = f"Cannot clean certificate {certificate.name}, {destination.plugin_name} plugin does not implement 'clean()'"
|
||||||
|
current_app.logger.warning(info_text)
|
||||||
|
else:
|
||||||
|
plugin.clean(certificate=certificate, options=destination.options)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_after_revoke(certificate):
|
||||||
|
"""
|
||||||
|
Perform the needed cleanup for a revoked certificate. This includes -
|
||||||
|
1. Disabling notification
|
||||||
|
2. Disabling auto-rotation
|
||||||
|
3. Update certificate status to 'revoked'
|
||||||
|
4. Remove from AWS
|
||||||
|
:param certificate: Certificate object to modify and update in DB
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
certificate.notify = False
|
||||||
|
certificate.rotation = False
|
||||||
|
certificate.status = 'revoked'
|
||||||
|
|
||||||
|
error_message = ""
|
||||||
|
|
||||||
|
for destination in list(certificate.destinations):
|
||||||
|
try:
|
||||||
|
remove_from_destination(certificate, destination)
|
||||||
|
certificate.destinations.remove(destination)
|
||||||
|
except Exception as e:
|
||||||
|
# This cleanup is the best-effort since certificate is already revoked at this point.
|
||||||
|
# We will capture the exception and move on to the next destination
|
||||||
|
sentry.captureException()
|
||||||
|
error_message = error_message + f"Failed to remove destination: {destination.label}. {str(e)}. "
|
||||||
|
|
||||||
|
database.update(certificate)
|
||||||
|
return error_message
|
||||||
|
|
|
@ -19,6 +19,7 @@ from lemur.auth.permissions import AuthorityPermission, CertificatePermission
|
||||||
|
|
||||||
from lemur.certificates import service
|
from lemur.certificates import service
|
||||||
from lemur.certificates.models import Certificate
|
from lemur.certificates.models import Certificate
|
||||||
|
from lemur.extensions import sentry
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
from lemur.certificates.schemas import (
|
from lemur.certificates.schemas import (
|
||||||
certificate_input_schema,
|
certificate_input_schema,
|
||||||
|
@ -888,8 +889,24 @@ class Certificates(AuthenticatedResource):
|
||||||
if cert.owner != data["owner"]:
|
if cert.owner != data["owner"]:
|
||||||
service.cleanup_owner_roles_notification(cert.owner, data)
|
service.cleanup_owner_roles_notification(cert.owner, data)
|
||||||
|
|
||||||
|
error_message = ""
|
||||||
|
# if destination is removed, cleanup the certificate from AWS
|
||||||
|
for destination in cert.destinations:
|
||||||
|
if destination not in data["destinations"]:
|
||||||
|
try:
|
||||||
|
service.remove_from_destination(cert, destination)
|
||||||
|
except Exception as e:
|
||||||
|
sentry.captureException()
|
||||||
|
# Add the removed destination back
|
||||||
|
data["destinations"].append(destination)
|
||||||
|
error_message = error_message + f"Failed to remove destination: {destination.label}. {str(e)}. "
|
||||||
|
|
||||||
|
# go ahead with DB update
|
||||||
cert = service.update(certificate_id, **data)
|
cert = service.update(certificate_id, **data)
|
||||||
log_service.create(g.current_user, "update_cert", certificate=cert)
|
log_service.create(g.current_user, "update_cert", certificate=cert)
|
||||||
|
|
||||||
|
if error_message:
|
||||||
|
return dict(message=f"Edit Successful except -\n\n {error_message}"), 400
|
||||||
return cert
|
return cert
|
||||||
|
|
||||||
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
|
@validate_schema(certificate_edit_input_schema, certificate_output_schema)
|
||||||
|
@ -1433,6 +1450,8 @@ class CertificateRevoke(AuthenticatedResource):
|
||||||
return dict(message="Cannot revoke certificate. No external id found."), 400
|
return dict(message="Cannot revoke certificate. No external id found."), 400
|
||||||
|
|
||||||
if cert.endpoints:
|
if cert.endpoints:
|
||||||
|
for endpoint in cert.endpoints:
|
||||||
|
if service.is_attached_to_endpoint(cert.name, endpoint.name):
|
||||||
return (
|
return (
|
||||||
dict(
|
dict(
|
||||||
message="Cannot revoke certificate. Endpoints are deployed with the given certificate."
|
message="Cannot revoke certificate. Endpoints are deployed with the given certificate."
|
||||||
|
@ -1442,7 +1461,13 @@ class CertificateRevoke(AuthenticatedResource):
|
||||||
|
|
||||||
plugin = plugins.get(cert.authority.plugin_name)
|
plugin = plugins.get(cert.authority.plugin_name)
|
||||||
plugin.revoke_certificate(cert, data)
|
plugin.revoke_certificate(cert, data)
|
||||||
|
|
||||||
log_service.create(g.current_user, "revoke_cert", certificate=cert)
|
log_service.create(g.current_user, "revoke_cert", certificate=cert)
|
||||||
|
|
||||||
|
# Perform cleanup after revoke
|
||||||
|
error_message = service.cleanup_after_revoke(cert)
|
||||||
|
if error_message:
|
||||||
|
return dict(message=f"Certificate (id:{cert.id}) is revoked - {error_message}"), 400
|
||||||
return dict(id=cert.id)
|
return dict(id=cert.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import pem
|
import pem
|
||||||
|
import base64
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
|
@ -34,6 +35,12 @@ paginated_parser.add_argument("filter", type=str, location="args")
|
||||||
paginated_parser.add_argument("owner", type=str, location="args")
|
paginated_parser.add_argument("owner", type=str, location="args")
|
||||||
|
|
||||||
|
|
||||||
|
def base64encode(string):
|
||||||
|
# Performs Base64 encoding of string to string using the base64.b64encode() function
|
||||||
|
# which encodes bytes to bytes.
|
||||||
|
return base64.b64encode(string.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
def get_psuedo_random_string():
|
def get_psuedo_random_string():
|
||||||
"""
|
"""
|
||||||
Create a random and strongish challenge.
|
Create a random and strongish challenge.
|
||||||
|
|
|
@ -224,7 +224,7 @@ class AcmeHandler(object):
|
||||||
def revoke_certificate(self, certificate):
|
def revoke_certificate(self, certificate):
|
||||||
if not self.reuse_account(certificate.authority):
|
if not self.reuse_account(certificate.authority):
|
||||||
raise InvalidConfiguration("There is no ACME account saved, unable to revoke the certificate.")
|
raise InvalidConfiguration("There is no ACME account saved, unable to revoke the certificate.")
|
||||||
acme_client, _ = self.acme.setup_acme_client(certificate.authority)
|
acme_client, _ = self.setup_acme_client(certificate.authority)
|
||||||
|
|
||||||
fullchain_com = jose.ComparableX509(
|
fullchain_com = jose.ComparableX509(
|
||||||
OpenSSL.crypto.load_certificate(
|
OpenSSL.crypto.load_certificate(
|
||||||
|
|
|
@ -149,6 +149,38 @@ def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@sts_client("elbv2")
|
||||||
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000, stop_max_attempt_number=5)
|
||||||
|
def get_load_balancer_arn_from_endpoint(endpoint_name, **kwargs):
|
||||||
|
"""
|
||||||
|
Get a load balancer ARN from an endpoint.
|
||||||
|
:param endpoint_name:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = kwargs.pop("client")
|
||||||
|
elbs = client.describe_load_balancers(Names=[endpoint_name])
|
||||||
|
if "LoadBalancers" in elbs and elbs["LoadBalancers"]:
|
||||||
|
return elbs["LoadBalancers"][0]["LoadBalancerArn"]
|
||||||
|
|
||||||
|
except Exception as e: # noqa
|
||||||
|
metrics.send(
|
||||||
|
"get_load_balancer_arn_from_endpoint",
|
||||||
|
"counter",
|
||||||
|
1,
|
||||||
|
metric_tags={
|
||||||
|
"error": str(e),
|
||||||
|
"endpoint_name": endpoint_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
sentry.captureException(
|
||||||
|
extra={
|
||||||
|
"endpoint_name": str(endpoint_name),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@sts_client("elb")
|
@sts_client("elb")
|
||||||
@retry(retry_on_exception=retry_throttled, wait_fixed=2000, stop_max_attempt_number=20)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000, stop_max_attempt_number=20)
|
||||||
def get_elbs(**kwargs):
|
def get_elbs(**kwargs):
|
||||||
|
|
|
@ -300,6 +300,41 @@ class AWSSourcePlugin(SourcePlugin):
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_endpoint_certificate_names(self, endpoint):
|
||||||
|
options = endpoint.source.options
|
||||||
|
account_number = self.get_option("accountNumber", options)
|
||||||
|
region = get_region_from_dns(endpoint.dnsname)
|
||||||
|
certificate_names = []
|
||||||
|
|
||||||
|
if endpoint.type == "elb":
|
||||||
|
elb_details = elb.get_elbs(account_number=account_number,
|
||||||
|
region=region,
|
||||||
|
LoadBalancerNames=[endpoint.name],)
|
||||||
|
|
||||||
|
for lb_description in elb_details["LoadBalancerDescriptions"]:
|
||||||
|
for listener_description in lb_description["ListenerDescriptions"]:
|
||||||
|
listener = listener_description.get("Listener")
|
||||||
|
if not listener.get("SSLCertificateId"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
certificate_names.append(iam.get_name_from_arn(listener.get("SSLCertificateId")))
|
||||||
|
elif endpoint.type == "elbv2":
|
||||||
|
listeners = elb.describe_listeners_v2(
|
||||||
|
account_number=account_number,
|
||||||
|
region=region,
|
||||||
|
LoadBalancerArn=elb.get_load_balancer_arn_from_endpoint(endpoint.name,
|
||||||
|
account_number=account_number,
|
||||||
|
region=region),
|
||||||
|
)
|
||||||
|
for listener in listeners["Listeners"]:
|
||||||
|
if not listener.get("Certificates"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for certificate in listener["Certificates"]:
|
||||||
|
certificate_names.append(iam.get_name_from_arn(certificate["CertificateArn"]))
|
||||||
|
|
||||||
|
return certificate_names
|
||||||
|
|
||||||
|
|
||||||
class AWSDestinationPlugin(DestinationPlugin):
|
class AWSDestinationPlugin(DestinationPlugin):
|
||||||
title = "AWS"
|
title = "AWS"
|
||||||
|
@ -344,6 +379,10 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||||
def deploy(self, elb_name, account, region, certificate):
|
def deploy(self, elb_name, account, region, certificate):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def clean(self, certificate, options, **kwargs):
|
||||||
|
account_number = self.get_option("accountNumber", options)
|
||||||
|
iam.delete_cert(certificate.name, account_number=account_number)
|
||||||
|
|
||||||
|
|
||||||
class S3DestinationPlugin(ExportDestinationPlugin):
|
class S3DestinationPlugin(ExportDestinationPlugin):
|
||||||
title = "AWS-S3"
|
title = "AWS-S3"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import boto3
|
import boto3
|
||||||
from moto import mock_sts, mock_elb
|
from moto import mock_sts, mock_ec2, mock_elb, mock_elbv2, mock_iam
|
||||||
|
|
||||||
|
|
||||||
@mock_sts()
|
@mock_sts()
|
||||||
|
@ -27,3 +27,107 @@ def test_get_all_elbs(app, aws_credentials):
|
||||||
|
|
||||||
elbs = get_all_elbs(account_number="123456789012", region="us-east-1")
|
elbs = get_all_elbs(account_number="123456789012", region="us-east-1")
|
||||||
assert elbs
|
assert elbs
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sts()
|
||||||
|
@mock_ec2
|
||||||
|
@mock_elbv2()
|
||||||
|
@mock_iam
|
||||||
|
def test_create_elb_with_https_listener_miscellaneous(app, aws_credentials):
|
||||||
|
from lemur.plugins.lemur_aws import iam, elb
|
||||||
|
endpoint_name = "example-lbv2"
|
||||||
|
account_number = "123456789012"
|
||||||
|
region_ue1 = "us-east-1"
|
||||||
|
|
||||||
|
client = boto3.client("elbv2", region_name="us-east-1")
|
||||||
|
ec2 = boto3.resource("ec2", region_name="us-east-1")
|
||||||
|
|
||||||
|
# Create VPC
|
||||||
|
vpc = ec2.create_vpc(CidrBlock="172.28.7.0/24")
|
||||||
|
|
||||||
|
# Create LB (elbv2) in above VPC
|
||||||
|
assert create_load_balancer(client, ec2, vpc.id, endpoint_name)
|
||||||
|
# Create target group
|
||||||
|
target_group_arn = create_target_group(client, vpc.id)
|
||||||
|
assert target_group_arn
|
||||||
|
|
||||||
|
# Test get_load_balancer_arn_from_endpoint
|
||||||
|
lb_arn = elb.get_load_balancer_arn_from_endpoint(endpoint_name,
|
||||||
|
account_number=account_number,
|
||||||
|
region=region_ue1)
|
||||||
|
assert lb_arn
|
||||||
|
|
||||||
|
# Test describe_listeners_v2
|
||||||
|
listeners = elb.describe_listeners_v2(account_number=account_number,
|
||||||
|
region=region_ue1,
|
||||||
|
LoadBalancerArn=lb_arn)
|
||||||
|
assert listeners
|
||||||
|
assert not listeners["Listeners"]
|
||||||
|
|
||||||
|
# Upload cert
|
||||||
|
response = iam.upload_cert("LemurTestCert", "testCert", "cert1", "cert2",
|
||||||
|
account_number=account_number)
|
||||||
|
assert response
|
||||||
|
cert_arn = response["ServerCertificateMetadata"]["Arn"]
|
||||||
|
assert cert_arn
|
||||||
|
|
||||||
|
# Create https listener using above cert
|
||||||
|
listeners = client.create_listener(
|
||||||
|
LoadBalancerArn=lb_arn,
|
||||||
|
Protocol="HTTPS",
|
||||||
|
Port=443,
|
||||||
|
Certificates=[{"CertificateArn": cert_arn}],
|
||||||
|
DefaultActions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
||||||
|
)
|
||||||
|
assert listeners
|
||||||
|
listener_arn = listeners["Listeners"][0]["ListenerArn"]
|
||||||
|
assert listener_arn
|
||||||
|
|
||||||
|
assert listeners["Listeners"]
|
||||||
|
for listener in listeners["Listeners"]:
|
||||||
|
if listener["Port"] == 443:
|
||||||
|
assert listener["Certificates"]
|
||||||
|
assert cert_arn == listener["Certificates"][0]["CertificateArn"]
|
||||||
|
|
||||||
|
# Test get_listener_arn_from_endpoint
|
||||||
|
assert listener_arn == elb.get_listener_arn_from_endpoint(
|
||||||
|
endpoint_name,
|
||||||
|
443,
|
||||||
|
account_number=account_number,
|
||||||
|
region=region_ue1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sts()
|
||||||
|
@mock_elb()
|
||||||
|
def test_get_all_elbs_v2():
|
||||||
|
from lemur.plugins.lemur_aws.elb import get_all_elbs_v2
|
||||||
|
|
||||||
|
elbs = get_all_elbs_v2(account_number="123456789012",
|
||||||
|
region="us-east-1")
|
||||||
|
assert elbs
|
||||||
|
|
||||||
|
|
||||||
|
def create_load_balancer(client, ec2, vpc_id, endpoint_name):
|
||||||
|
subnet1 = ec2.create_subnet(
|
||||||
|
VpcId=vpc_id,
|
||||||
|
CidrBlock="172.28.7.192/26",
|
||||||
|
AvailabilityZone="us-east-1a"
|
||||||
|
)
|
||||||
|
|
||||||
|
return client.create_load_balancer(
|
||||||
|
Name=endpoint_name,
|
||||||
|
Subnets=[
|
||||||
|
subnet1.id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_target_group(client, vpc_id):
|
||||||
|
response = client.create_target_group(
|
||||||
|
Name="a-target",
|
||||||
|
Protocol="HTTPS",
|
||||||
|
Port=443,
|
||||||
|
VpcId=vpc_id,
|
||||||
|
)
|
||||||
|
return response.get("TargetGroups")[0]["TargetGroupArn"]
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
try:
|
||||||
|
VERSION = __import__("pkg_resources").get_distribution(__name__).version
|
||||||
|
except Exception as e:
|
||||||
|
VERSION = "unknown"
|
|
@ -0,0 +1,184 @@
|
||||||
|
"""
|
||||||
|
.. module: lemur.plugins.lemur_azure_dest.plugin
|
||||||
|
:platform: Unix
|
||||||
|
:copyright: (c) 2019
|
||||||
|
:license: Apache, see LICENCE for more details.
|
||||||
|
|
||||||
|
Plugin for uploading certificates and private key as secret to azure key-vault
|
||||||
|
that can be pulled down by end point nodes.
|
||||||
|
|
||||||
|
.. moduleauthor:: sirferl
|
||||||
|
"""
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from lemur.common.defaults import common_name, bitstrength
|
||||||
|
from lemur.common.utils import parse_certificate, parse_private_key
|
||||||
|
from lemur.plugins.bases import DestinationPlugin
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def handle_response(my_response):
|
||||||
|
"""
|
||||||
|
Helper function for parsing responses from the Entrust API.
|
||||||
|
:param my_response:
|
||||||
|
:return: :raise Exception:
|
||||||
|
"""
|
||||||
|
msg = {
|
||||||
|
200: "The request was successful.",
|
||||||
|
400: "Keyvault Error"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(my_response.content)
|
||||||
|
except ValueError:
|
||||||
|
# catch an empty jason object here
|
||||||
|
data = {'response': 'No detailed message'}
|
||||||
|
status_code = my_response.status_code
|
||||||
|
if status_code > 399:
|
||||||
|
raise Exception(f"AZURE error: {msg.get(status_code, status_code)}\n{data}")
|
||||||
|
|
||||||
|
log_data = {
|
||||||
|
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
|
||||||
|
"message": "Response",
|
||||||
|
"status": status_code,
|
||||||
|
"response": data
|
||||||
|
}
|
||||||
|
current_app.logger.info(log_data)
|
||||||
|
if data == {'response': 'No detailed message'}:
|
||||||
|
# status if no data
|
||||||
|
return status_code
|
||||||
|
else:
|
||||||
|
# return data from the response
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_access_token(tenant, appID, password, self):
|
||||||
|
"""
|
||||||
|
Gets the access token with the appid and the password and returns it
|
||||||
|
|
||||||
|
Improvment option: we can try to save it and renew it only when necessary
|
||||||
|
|
||||||
|
:param tenant: Tenant used
|
||||||
|
:param appID: Application ID from Azure
|
||||||
|
:param password: password for Application ID
|
||||||
|
:return: Access token to post to the keyvault
|
||||||
|
"""
|
||||||
|
# prepare the call for the access_token
|
||||||
|
auth_url = f"https://login.microsoftonline.com/{tenant}/oauth2/token"
|
||||||
|
post_data = {
|
||||||
|
'grant_type': 'client_credentials',
|
||||||
|
'client_id': appID,
|
||||||
|
'client_secret': password,
|
||||||
|
'resource': 'https://vault.azure.net'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = self.session.post(auth_url, data=post_data)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
current_app.logger.exception(f"AZURE: Error for POST {e}")
|
||||||
|
|
||||||
|
access_token = json.loads(response.content)["access_token"]
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
|
class AzureDestinationPlugin(DestinationPlugin):
|
||||||
|
"""Azure Keyvault Destination plugin for Lemur"""
|
||||||
|
|
||||||
|
title = "Azure"
|
||||||
|
slug = "azure-keyvault-destination"
|
||||||
|
description = "Allow the uploading of certificates to Azure key vault"
|
||||||
|
|
||||||
|
author = "Sirferl"
|
||||||
|
author_url = "https://github.com/sirferl/lemur"
|
||||||
|
|
||||||
|
options = [
|
||||||
|
{
|
||||||
|
"name": "vaultUrl",
|
||||||
|
"type": "str",
|
||||||
|
"required": True,
|
||||||
|
"validation": "^https?://[a-zA-Z0-9.:-]+$",
|
||||||
|
"helpMessage": "Valid URL to Azure key vault instance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "azureTenant",
|
||||||
|
"type": "str",
|
||||||
|
"required": True,
|
||||||
|
"validation": "^([a-zA-Z0-9/-/?)+$",
|
||||||
|
"helpMessage": "Tenant for the Azure Key Vault",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "appID",
|
||||||
|
"type": "str",
|
||||||
|
"required": True,
|
||||||
|
"validation": "^([a-zA-Z0-9/-/?)+$",
|
||||||
|
"helpMessage": "AppID for the Azure Key Vault",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "azurePassword",
|
||||||
|
"type": "str",
|
||||||
|
"required": True,
|
||||||
|
"validation": "[0-9a-zA-Z.:_-~]+",
|
||||||
|
"helpMessage": "Tenant password for the Azure Key Vault",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.session = requests.Session()
|
||||||
|
super(AzureDestinationPlugin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||||
|
"""
|
||||||
|
Upload certificate and private key
|
||||||
|
|
||||||
|
:param private_key:
|
||||||
|
:param cert_chain:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# we use the common name to identify the certificate
|
||||||
|
# Azure does not allow "." in the certificate name we replace them with "-"
|
||||||
|
cert = parse_certificate(body)
|
||||||
|
certificate_name = common_name(cert).replace(".", "-")
|
||||||
|
|
||||||
|
vault_URI = self.get_option("vaultUrl", options)
|
||||||
|
tenant = self.get_option("azureTenant", options)
|
||||||
|
app_id = self.get_option("appID", options)
|
||||||
|
password = self.get_option("azurePassword", options)
|
||||||
|
|
||||||
|
access_token = get_access_token(tenant, app_id, password, self)
|
||||||
|
|
||||||
|
cert_url = f"{vault_URI}/certificates/{certificate_name}/import?api-version=7.1"
|
||||||
|
post_header = {
|
||||||
|
"Authorization": f"Bearer {access_token}"
|
||||||
|
}
|
||||||
|
key_pkcs8 = parse_private_key(private_key).private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.PKCS8,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
key_pkcs8 = key_pkcs8.decode("utf-8").replace('\\n', '\n')
|
||||||
|
cert_package = f"{body}\n{key_pkcs8}"
|
||||||
|
|
||||||
|
post_body = {
|
||||||
|
"value": cert_package,
|
||||||
|
"policy": {
|
||||||
|
"key_props": {
|
||||||
|
"exportable": True,
|
||||||
|
"kty": "RSA",
|
||||||
|
"key_size": bitstrength(cert),
|
||||||
|
"reuse_key": True
|
||||||
|
},
|
||||||
|
"secret_props": {
|
||||||
|
"contentType": "application/x-pem-file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.session.post(cert_url, headers=post_header, json=post_body)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
current_app.logger.exception(f"AZURE: Error for POST {e}")
|
||||||
|
return_value = handle_response(response)
|
|
@ -0,0 +1 @@
|
||||||
|
from lemur.tests.conftest import * # noqa
|
|
@ -10,7 +10,6 @@
|
||||||
|
|
||||||
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||||
"""
|
"""
|
||||||
import base64
|
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -18,7 +17,7 @@ import requests
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from lemur.common.defaults import common_name
|
from lemur.common.defaults import common_name
|
||||||
from lemur.common.utils import parse_certificate
|
from lemur.common.utils import parse_certificate, base64encode
|
||||||
from lemur.plugins.bases import DestinationPlugin
|
from lemur.plugins.bases import DestinationPlugin
|
||||||
|
|
||||||
DEFAULT_API_VERSION = "v1"
|
DEFAULT_API_VERSION = "v1"
|
||||||
|
@ -73,12 +72,6 @@ def _resolve_uri(k8s_base_uri, namespace, kind, name=None, api_ver=DEFAULT_API_V
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Performs Base64 encoding of string to string using the base64.b64encode() function
|
|
||||||
# which encodes bytes to bytes.
|
|
||||||
def base64encode(string):
|
|
||||||
return base64.b64encode(string.encode()).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def build_secret(secret_format, secret_name, body, private_key, cert_chain):
|
def build_secret(secret_format, secret_name, body, private_key, cert_chain):
|
||||||
secret = {
|
secret = {
|
||||||
"apiVersion": "v1",
|
"apiVersion": "v1",
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<select class="form-control" ng-model="certificate.keyType"
|
<select class="form-control" ng-model="certificate.keyType"
|
||||||
ng-options="option for option in ['RSA2048', 'RSA4096', 'ECCPRIME256V1', 'ECCSECP384R1']"
|
ng-options="option for option in ['RSA2048', 'RSA4096', 'ECCPRIME256V1', 'ECCSECP384R1']"
|
||||||
ng-init="certificate.keyType = 'RSA2048'"></select>
|
ng-init="certificate.keyType = 'ECCPRIME256V1'"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -26,9 +26,8 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div ng-if="certificate.endpoints.length">
|
<div ng-if="certificate.endpoints.length">
|
||||||
<p><strong>Certificate cannot be revoked, it is associated with the following endpoints. Disassociate this
|
<p><strong>Certificate might be associated with the following endpoints. Disassociate this
|
||||||
certificate
|
certificate before revoking or continue if you've already done so.</strong></p>
|
||||||
before revoking.</strong></p>
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li class="list-group-item" ng-repeat="endpoint in certificate.endpoints">
|
<li class="list-group-item" ng-repeat="endpoint in certificate.endpoints">
|
||||||
<span class="pull-right"><label class="label label-default">{{ endpoint.type }}</label></span>
|
<span class="pull-right"><label class="label label-default">{{ endpoint.type }}</label></span>
|
||||||
|
|
|
@ -24,7 +24,7 @@ keyring==21.2.0 # via twine
|
||||||
mccabe==0.6.1 # via flake8
|
mccabe==0.6.1 # via flake8
|
||||||
nodeenv==1.5.0 # via -r requirements-dev.in, pre-commit
|
nodeenv==1.5.0 # via -r requirements-dev.in, pre-commit
|
||||||
pkginfo==1.5.0.1 # via twine
|
pkginfo==1.5.0.1 # via twine
|
||||||
pre-commit==2.8.2 # via -r requirements-dev.in
|
pre-commit==2.9.2 # via -r requirements-dev.in
|
||||||
pycodestyle==2.6.0 # via flake8
|
pycodestyle==2.6.0 # via flake8
|
||||||
pycparser==2.20 # via cffi
|
pycparser==2.20 # via cffi
|
||||||
pyflakes==2.2.0 # via flake8
|
pyflakes==2.2.0 # via flake8
|
||||||
|
@ -32,7 +32,7 @@ pygments==2.6.1 # via readme-renderer
|
||||||
pyyaml==5.3.1 # via -r requirements-dev.in, pre-commit
|
pyyaml==5.3.1 # via -r requirements-dev.in, pre-commit
|
||||||
readme-renderer==25.0 # via twine
|
readme-renderer==25.0 # via twine
|
||||||
requests-toolbelt==0.9.1 # via twine
|
requests-toolbelt==0.9.1 # via twine
|
||||||
requests==2.24.0 # via requests-toolbelt, twine
|
requests==2.25.0 # via requests-toolbelt, twine
|
||||||
rfc3986==1.4.0 # via twine
|
rfc3986==1.4.0 # via twine
|
||||||
secretstorage==3.1.2 # via keyring
|
secretstorage==3.1.2 # via keyring
|
||||||
six==1.15.0 # via bleach, cryptography, readme-renderer, virtualenv
|
six==1.15.0 # via bleach, cryptography, readme-renderer, virtualenv
|
||||||
|
|
|
@ -17,8 +17,8 @@ bcrypt==3.1.7 # via -r requirements.txt, flask-bcrypt, paramiko
|
||||||
beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare
|
beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare
|
||||||
billiard==3.6.3.0 # via -r requirements.txt, celery
|
billiard==3.6.3.0 # via -r requirements.txt, celery
|
||||||
blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven
|
blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven
|
||||||
boto3==1.16.14 # via -r requirements.txt
|
boto3==1.16.25 # via -r requirements.txt
|
||||||
botocore==1.19.14 # via -r requirements.txt, boto3, s3transfer
|
botocore==1.19.25 # via -r requirements.txt, boto3, s3transfer
|
||||||
celery[redis]==4.4.2 # via -r requirements.txt
|
celery[redis]==4.4.2 # via -r requirements.txt
|
||||||
certifi==2020.11.8 # via -r requirements.txt, requests
|
certifi==2020.11.8 # via -r requirements.txt, requests
|
||||||
certsrv==2.1.1 # via -r requirements.txt
|
certsrv==2.1.1 # via -r requirements.txt
|
||||||
|
@ -73,25 +73,26 @@ pygments==2.6.1 # via sphinx
|
||||||
pyjks==20.0.0 # via -r requirements.txt
|
pyjks==20.0.0 # via -r requirements.txt
|
||||||
pyjwt==1.7.1 # via -r requirements.txt
|
pyjwt==1.7.1 # via -r requirements.txt
|
||||||
pynacl==1.3.0 # via -r requirements.txt, paramiko
|
pynacl==1.3.0 # via -r requirements.txt, paramiko
|
||||||
pyopenssl==19.1.0 # via -r requirements.txt, acme, josepy, ndg-httpsclient, requests
|
pyopenssl==20.0.0 # via -r requirements.txt, acme, josepy, ndg-httpsclient, requests
|
||||||
pyparsing==2.4.7 # via packaging
|
pyparsing==2.4.7 # via packaging
|
||||||
pyrfc3339==1.1 # via -r requirements.txt, acme
|
pyrfc3339==1.1 # via -r requirements.txt, acme
|
||||||
python-dateutil==2.8.1 # via -r requirements.txt, alembic, arrow, botocore
|
python-dateutil==2.8.1 # via -r requirements.txt, alembic, arrow, botocore
|
||||||
python-editor==1.0.4 # via -r requirements.txt, alembic
|
python-editor==1.0.4 # via -r requirements.txt, alembic
|
||||||
python-json-logger==0.1.11 # via -r requirements.txt, logmatic-python
|
python-json-logger==0.1.11 # via -r requirements.txt, logmatic-python
|
||||||
|
python-ldap==3.3.1 # via -r requirements.txt
|
||||||
pytz==2019.3 # via -r requirements.txt, acme, babel, celery, flask-restful, pyrfc3339
|
pytz==2019.3 # via -r requirements.txt, acme, babel, celery, flask-restful, pyrfc3339
|
||||||
pyyaml==5.3.1 # via -r requirements.txt, cloudflare
|
pyyaml==5.3.1 # via -r requirements.txt, cloudflare
|
||||||
raven[flask]==6.10.0 # via -r requirements.txt
|
raven[flask]==6.10.0 # via -r requirements.txt
|
||||||
redis==3.5.3 # via -r requirements.txt, celery
|
redis==3.5.3 # via -r requirements.txt, celery
|
||||||
requests-toolbelt==0.9.1 # via -r requirements.txt, acme
|
requests-toolbelt==0.9.1 # via -r requirements.txt, acme
|
||||||
requests[security]==2.24.0 # via -r requirements.txt, acme, certsrv, cloudflare, hvac, requests-toolbelt, sphinx
|
requests[security]==2.25.0 # via -r requirements.txt, acme, certsrv, cloudflare, hvac, requests-toolbelt, sphinx
|
||||||
retrying==1.3.3 # via -r requirements.txt
|
retrying==1.3.3 # via -r requirements.txt
|
||||||
s3transfer==0.3.3 # via -r requirements.txt, boto3
|
s3transfer==0.3.3 # via -r requirements.txt, boto3
|
||||||
six==1.15.0 # via -r requirements.txt, acme, bcrypt, cryptography, flask-cors, flask-restful, hvac, josepy, jsonlines, packaging, pynacl, pyopenssl, python-dateutil, retrying, sphinxcontrib-httpdomain, sqlalchemy-utils
|
six==1.15.0 # via -r requirements.txt, acme, bcrypt, cryptography, flask-cors, flask-restful, hvac, josepy, jsonlines, packaging, pynacl, pyopenssl, python-dateutil, retrying, sphinxcontrib-httpdomain, sqlalchemy-utils
|
||||||
snowballstemmer==2.0.0 # via sphinx
|
snowballstemmer==2.0.0 # via sphinx
|
||||||
soupsieve==2.0.1 # via -r requirements.txt, beautifulsoup4
|
soupsieve==2.0.1 # via -r requirements.txt, beautifulsoup4
|
||||||
sphinx-rtd-theme==0.5.0 # via -r requirements-docs.in
|
sphinx-rtd-theme==0.5.0 # via -r requirements-docs.in
|
||||||
sphinx==3.3.0 # via -r requirements-docs.in, sphinx-rtd-theme, sphinxcontrib-httpdomain
|
sphinx==3.3.1 # via -r requirements-docs.in, sphinx-rtd-theme, sphinxcontrib-httpdomain
|
||||||
sphinxcontrib-applehelp==1.0.2 # via sphinx
|
sphinxcontrib-applehelp==1.0.2 # via sphinx
|
||||||
sphinxcontrib-devhelp==1.0.2 # via sphinx
|
sphinxcontrib-devhelp==1.0.2 # via sphinx
|
||||||
sphinxcontrib-htmlhelp==1.0.3 # via sphinx
|
sphinxcontrib-htmlhelp==1.0.3 # via sphinx
|
||||||
|
|
|
@ -10,9 +10,9 @@ aws-sam-translator==1.22.0 # via cfn-lint
|
||||||
aws-xray-sdk==2.5.0 # via moto
|
aws-xray-sdk==2.5.0 # via moto
|
||||||
bandit==1.6.2 # via -r requirements-tests.in
|
bandit==1.6.2 # via -r requirements-tests.in
|
||||||
black==20.8b1 # via -r requirements-tests.in
|
black==20.8b1 # via -r requirements-tests.in
|
||||||
boto3==1.16.14 # via aws-sam-translator, moto
|
boto3==1.16.25 # via aws-sam-translator, moto
|
||||||
boto==2.49.0 # via moto
|
boto==2.49.0 # via moto
|
||||||
botocore==1.19.14 # via aws-xray-sdk, boto3, moto, s3transfer
|
botocore==1.19.25 # via aws-xray-sdk, boto3, moto, s3transfer
|
||||||
certifi==2020.11.8 # via requests
|
certifi==2020.11.8 # via requests
|
||||||
cffi==1.14.0 # via cryptography
|
cffi==1.14.0 # via cryptography
|
||||||
cfn-lint==0.29.5 # via moto
|
cfn-lint==0.29.5 # via moto
|
||||||
|
@ -24,8 +24,8 @@ decorator==4.4.2 # via networkx
|
||||||
docker==4.2.0 # via moto
|
docker==4.2.0 # via moto
|
||||||
ecdsa==0.14.1 # via moto, python-jose, sshpubkeys
|
ecdsa==0.14.1 # via moto, python-jose, sshpubkeys
|
||||||
factory-boy==3.1.0 # via -r requirements-tests.in
|
factory-boy==3.1.0 # via -r requirements-tests.in
|
||||||
faker==4.14.2 # via -r requirements-tests.in, factory-boy
|
faker==4.17.1 # via -r requirements-tests.in, factory-boy
|
||||||
fakeredis==1.4.4 # via -r requirements-tests.in
|
fakeredis==1.4.5 # via -r requirements-tests.in
|
||||||
flask==1.1.2 # via pytest-flask
|
flask==1.1.2 # via pytest-flask
|
||||||
freezegun==1.0.0 # via -r requirements-tests.in
|
freezegun==1.0.0 # via -r requirements-tests.in
|
||||||
future==0.18.2 # via aws-xray-sdk
|
future==0.18.2 # via aws-xray-sdk
|
||||||
|
@ -69,7 +69,7 @@ pyyaml==5.3.1 # via -r requirements-tests.in, bandit, cfn-lint, moto
|
||||||
redis==3.5.3 # via fakeredis
|
redis==3.5.3 # via fakeredis
|
||||||
regex==2020.4.4 # via black
|
regex==2020.4.4 # via black
|
||||||
requests-mock==1.8.0 # via -r requirements-tests.in
|
requests-mock==1.8.0 # via -r requirements-tests.in
|
||||||
requests==2.24.0 # via docker, moto, requests-mock, responses
|
requests==2.25.0 # via docker, moto, requests-mock, responses
|
||||||
responses==0.10.12 # via moto
|
responses==0.10.12 # via moto
|
||||||
rsa==4.0 # via python-jose
|
rsa==4.0 # via python-jose
|
||||||
s3transfer==0.3.3 # via boto3
|
s3transfer==0.3.3 # via boto3
|
||||||
|
|
|
@ -15,8 +15,8 @@ bcrypt==3.1.7 # via flask-bcrypt, paramiko
|
||||||
beautifulsoup4==4.9.1 # via cloudflare
|
beautifulsoup4==4.9.1 # via cloudflare
|
||||||
billiard==3.6.3.0 # via celery
|
billiard==3.6.3.0 # via celery
|
||||||
blinker==1.4 # via flask-mail, flask-principal, raven
|
blinker==1.4 # via flask-mail, flask-principal, raven
|
||||||
boto3==1.16.14 # via -r requirements.in
|
boto3==1.16.25 # via -r requirements.in
|
||||||
botocore==1.19.14 # via -r requirements.in, boto3, s3transfer
|
botocore==1.19.25 # via -r requirements.in, boto3, s3transfer
|
||||||
celery[redis]==4.4.2 # via -r requirements.in
|
celery[redis]==4.4.2 # via -r requirements.in
|
||||||
certifi==2020.11.8 # via -r requirements.in, requests
|
certifi==2020.11.8 # via -r requirements.in, requests
|
||||||
certsrv==2.1.1 # via -r requirements.in
|
certsrv==2.1.1 # via -r requirements.in
|
||||||
|
@ -67,7 +67,7 @@ pycryptodomex==3.9.7 # via pyjks
|
||||||
pyjks==20.0.0 # via -r requirements.in
|
pyjks==20.0.0 # via -r requirements.in
|
||||||
pyjwt==1.7.1 # via -r requirements.in
|
pyjwt==1.7.1 # via -r requirements.in
|
||||||
pynacl==1.3.0 # via paramiko
|
pynacl==1.3.0 # via paramiko
|
||||||
pyopenssl==19.1.0 # via -r requirements.in, acme, josepy, ndg-httpsclient, requests
|
pyopenssl==20.0.0 # via -r requirements.in, acme, josepy, ndg-httpsclient, requests
|
||||||
pyrfc3339==1.1 # via acme
|
pyrfc3339==1.1 # via acme
|
||||||
python-dateutil==2.8.1 # via alembic, arrow, botocore
|
python-dateutil==2.8.1 # via alembic, arrow, botocore
|
||||||
python-editor==1.0.4 # via alembic
|
python-editor==1.0.4 # via alembic
|
||||||
|
@ -78,7 +78,7 @@ pyyaml==5.3.1 # via -r requirements.in, cloudflare
|
||||||
raven[flask]==6.10.0 # via -r requirements.in
|
raven[flask]==6.10.0 # via -r requirements.in
|
||||||
redis==3.5.3 # via -r requirements.in, celery
|
redis==3.5.3 # via -r requirements.in, celery
|
||||||
requests-toolbelt==0.9.1 # via acme
|
requests-toolbelt==0.9.1 # via acme
|
||||||
requests[security]==2.24.0 # via -r requirements.in, acme, certsrv, cloudflare, hvac, requests-toolbelt
|
requests[security]==2.25.0 # via -r requirements.in, acme, certsrv, cloudflare, hvac, requests-toolbelt
|
||||||
retrying==1.3.3 # via -r requirements.in
|
retrying==1.3.3 # via -r requirements.in
|
||||||
s3transfer==0.3.3 # via boto3
|
s3transfer==0.3.3 # via boto3
|
||||||
six==1.15.0 # via -r requirements.in, acme, bcrypt, cryptography, flask-cors, flask-restful, hvac, josepy, jsonlines, pynacl, pyopenssl, python-dateutil, retrying, sqlalchemy-utils
|
six==1.15.0 # via -r requirements.in, acme, bcrypt, cryptography, flask-cors, flask-restful, hvac, josepy, jsonlines, pynacl, pyopenssl, python-dateutil, retrying, sqlalchemy-utils
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -157,7 +157,8 @@ setup(
|
||||||
'adcs_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin',
|
'adcs_issuer = lemur.plugins.lemur_adcs.plugin:ADCSIssuerPlugin',
|
||||||
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin',
|
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin',
|
||||||
'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin',
|
'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin',
|
||||||
'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin'
|
'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin',
|
||||||
|
'azure_destination = lemur.plugins.lemur_azure_dest.plugin:AzureDestinationPlugin'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
Loading…
Reference in New Issue