Merge branch 'master' of github.com:Netflix/lemur into fix-more-button-notification

This commit is contained in:
Jasmine Schladen 2020-11-10 14:50:10 -08:00
commit 2798692fa9
48 changed files with 544 additions and 160 deletions

View File

@ -47,4 +47,7 @@ after_success:
notifications: notifications:
email: email:
lemur@netflix.com recipients:
- lemur@netflix.com
on_success: never
on_failure: always

View File

@ -690,6 +690,20 @@ If you are not using a metric provider you do not need to configure any of these
Plugin Specific Options Plugin Specific Options
----------------------- -----------------------
ACME Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. data:: ACME_DNS_PROVIDER_TYPES
:noindex:
Dictionary of ACME DNS Providers and their requirements.
.. data:: ACME_ENABLE_DELEGATED_CNAME
:noindex:
Enables delegated DNS domain validation using CNAMES. When enabled, Lemur will attempt to follow CNAME records to authoritative DNS servers when creating DNS-01 challenges.
Active Directory Certificate Services Plugin Active Directory Certificate Services Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -237,7 +237,7 @@ gulp.task('addUrlContextPath',['addUrlContextPath:revreplace'], function(){
.forEach(function(file){ .forEach(function(file){
return gulp.src(file) return gulp.src(file)
.pipe(gulpif(urlContextPathExists, replace('api/', argv.urlContextPath + '/api/'))) .pipe(gulpif(urlContextPathExists, replace('api/', argv.urlContextPath + '/api/')))
.pipe(gulpif(urlContextPathExists, replace('angular/', argv.urlContextPath + '/angular/'))) .pipe(gulpif(urlContextPathExists, replace('/angular/', '/' + argv.urlContextPath + '/angular/')))
.pipe(gulp.dest(function(file){ .pipe(gulp.dest(function(file){
return file.base; return file.base;
})) }))
@ -256,10 +256,9 @@ gulp.task('addUrlContextPath:revreplace', ['addUrlContextPath:revision'], functi
var manifest = gulp.src("lemur/static/dist/rev-manifest.json"); var manifest = gulp.src("lemur/static/dist/rev-manifest.json");
var urlContextPathExists = argv.urlContextPath ? true : false; var urlContextPathExists = argv.urlContextPath ? true : false;
return gulp.src( "lemur/static/dist/index.html") return gulp.src( "lemur/static/dist/index.html")
.pipe(gulpif(urlContextPathExists, revReplace({prefix: argv.urlContextPath + '/', manifest: manifest}, revReplace({manifest: manifest}))))
.pipe(gulp.dest('lemur/static/dist')); .pipe(gulp.dest('lemur/static/dist'));
}) })
gulp.task('build', ['build:ngviews', 'build:inject', 'build:images', 'build:fonts', 'build:html', 'build:extras']); gulp.task('build', ['build:ngviews', 'build:inject', 'build:images', 'build:fonts', 'build:html', 'build:extras']);
gulp.task('package', ['addUrlContextPath', 'package:strip']); gulp.task('package', ['addUrlContextPath', 'package:strip']);

View File

@ -1,12 +1,15 @@
import time import time
import json import json
import arrow
from flask_script import Manager from flask_script import Manager
from flask import current_app from flask import current_app
from lemur.extensions import sentry from lemur.extensions import sentry
from lemur.constants import SUCCESS_METRIC_STATUS from lemur.constants import SUCCESS_METRIC_STATUS
from lemur.plugins import plugins
from lemur.plugins.lemur_acme.plugin import AcmeHandler from lemur.plugins.lemur_acme.plugin import AcmeHandler
from lemur.plugins.lemur_aws import s3
manager = Manager( manager = Manager(
usage="Handles all ACME related tasks" usage="Handles all ACME related tasks"
@ -84,3 +87,105 @@ def dnstest(domain, token):
status = SUCCESS_METRIC_STATUS status = SUCCESS_METRIC_STATUS
print("[+] Done with ACME Tests.") print("[+] Done with ACME Tests.")
@manager.option(
"-t",
"--token",
dest="token",
default="date: " + arrow.utcnow().format("YYYY-MM-DDTHH-mm-ss"),
required=False,
help="Value of the Token",
)
@manager.option(
"-n",
"--token_name",
dest="token_name",
default="Token-" + arrow.utcnow().format("YYYY-MM-DDTHH-mm-ss"),
required=False,
help="path",
)
@manager.option(
"-p",
"--prefix",
dest="prefix",
default="test/",
required=False,
help="S3 bucket prefix",
)
@manager.option(
"-a",
"--account_number",
dest="account_number",
required=True,
help="AWS Account",
)
@manager.option(
"-b",
"--bucket_name",
dest="bucket_name",
required=True,
help="Bucket Name",
)
def upload_acme_token_s3(token, token_name, prefix, account_number, bucket_name):
"""
This method serves for testing the upload_acme_token to S3, fetching the token to verify it, and then deleting it.
It mainly serves for testing purposes.
:param token:
:param token_name:
:param prefix:
:param account_number:
:param bucket_name:
:return:
"""
additional_options = [
{
"name": "bucket",
"value": bucket_name,
"type": "str",
"required": True,
"validation": r"[0-9a-z.-]{3,63}",
"helpMessage": "Must be a valid S3 bucket name!",
},
{
"name": "accountNumber",
"type": "str",
"value": account_number,
"required": True,
"validation": r"[0-9]{12}",
"helpMessage": "A valid AWS account number with permission to access S3",
},
{
"name": "region",
"type": "str",
"default": "us-east-1",
"required": False,
"helpMessage": "Region bucket exists",
"available": ["us-east-1", "us-west-2", "eu-west-1"],
},
{
"name": "encrypt",
"type": "bool",
"value": False,
"required": False,
"helpMessage": "Enable server side encryption",
"default": True,
},
{
"name": "prefix",
"type": "str",
"value": prefix,
"required": False,
"helpMessage": "Must be a valid S3 object prefix!",
},
]
p = plugins.get("aws-s3")
p.upload_acme_token(token_name, token, additional_options)
if not prefix.endswith("/"):
prefix + "/"
token_res = s3.get(bucket_name, prefix + token_name, account_number=account_number)
assert(token_res == token)
s3.delete(bucket_name, prefix + token_name, account_number=account_number)

View File

@ -210,7 +210,8 @@ class LdapPrincipal:
self.ldap_groups = [] self.ldap_groups = []
for group in lgroups: for group in lgroups:
(dn, values) = group (dn, values) = group
self.ldap_groups.append(values["cn"][0].decode("ascii")) if type(values) == dict:
self.ldap_groups.append(values["cn"][0].decode("ascii"))
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

View File

@ -101,7 +101,8 @@ def login_required(f):
return dict(message="Token is invalid"), 403 return dict(message="Token is invalid"), 403
try: try:
payload = jwt.decode(token, current_app.config["LEMUR_TOKEN_SECRET"]) header_data = fetch_token_header(token)
payload = jwt.decode(token, current_app.config["LEMUR_TOKEN_SECRET"], algorithms=[header_data["alg"]])
except jwt.DecodeError: except jwt.DecodeError:
return dict(message="Token is invalid"), 403 return dict(message="Token is invalid"), 403
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:

View File

@ -18,7 +18,7 @@ from sqlalchemy import (
func, func,
ForeignKey, ForeignKey,
DateTime, DateTime,
PassiveDefault, DefaultClause,
Boolean, Boolean,
) )
from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSON
@ -39,7 +39,7 @@ class Authority(db.Model):
plugin_name = Column(String(64)) plugin_name = Column(String(64))
description = Column(Text) description = Column(Text)
options = Column(JSON) options = Column(JSON)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) date_created = Column(DateTime, DefaultClause(func.now()), nullable=False)
roles = relationship( roles = relationship(
"Role", "Role",
secondary=roles_authorities, secondary=roles_authorities,
@ -93,9 +93,11 @@ class Authority(db.Model):
if not self.options: if not self.options:
return None return None
for option in json.loads(self.options): options_array = json.loads(self.options)
if "name" in option and option["name"] == 'cab_compliant': if isinstance(options_array, list):
return option["value"] for option in options_array:
if "name" in option and option["name"] == 'cab_compliant':
return option["value"]
return None return None

View File

@ -16,7 +16,7 @@ from sqlalchemy import (
Integer, Integer,
ForeignKey, ForeignKey,
String, String,
PassiveDefault, DefaultClause,
func, func,
Column, Column,
Text, Text,
@ -138,7 +138,7 @@ class Certificate(db.Model):
not_after = Column(ArrowType) not_after = Column(ArrowType)
not_after_ix = Index("ix_certificates_not_after", not_after.desc()) not_after_ix = Index("ix_certificates_not_after", not_after.desc())
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False) date_created = Column(ArrowType, DefaultClause(func.now()), nullable=False)
signing_algorithm = Column(String(128)) signing_algorithm = Column(String(128))
status = Column(String(128)) status = Column(String(128))
@ -184,7 +184,6 @@ class Certificate(db.Model):
"PendingCertificate", "PendingCertificate",
secondary=pending_cert_replacement_associations, secondary=pending_cert_replacement_associations,
backref="pending_replace", backref="pending_replace",
viewonly=True,
) )
logs = relationship("Log", backref="certificate") logs = relationship("Log", backref="certificate")

View File

@ -12,6 +12,7 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives import hashes, serialization
from flask import current_app from flask import current_app
from sqlalchemy import func, or_, not_, cast, Integer from sqlalchemy import func, or_, not_, cast, Integer
from sqlalchemy.sql.expression import false, true
from lemur import database from lemur import database
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
@ -150,7 +151,7 @@ def get_all_certs_attached_to_endpoint_without_autorotate():
""" """
return ( return (
Certificate.query.filter(Certificate.endpoints.any()) Certificate.query.filter(Certificate.endpoints.any())
.filter(Certificate.rotation == False) .filter(Certificate.rotation == false())
.filter(Certificate.not_after >= arrow.now()) .filter(Certificate.not_after >= arrow.now())
.filter(not_(Certificate.replaced.any())) .filter(not_(Certificate.replaced.any()))
.all() # noqa .all() # noqa
@ -205,9 +206,9 @@ def get_all_pending_reissue():
:return: :return:
""" """
return ( return (
Certificate.query.filter(Certificate.rotation == True) Certificate.query.filter(Certificate.rotation == true())
.filter(not_(Certificate.replaced.any())) .filter(not_(Certificate.replaced.any()))
.filter(Certificate.in_rotation_window == True) .filter(Certificate.in_rotation_window == true())
.all() .all()
) # noqa ) # noqa
@ -525,7 +526,7 @@ def render(args):
) )
if current_app.config.get("ALLOW_CERT_DELETION", False): if current_app.config.get("ALLOW_CERT_DELETION", False):
query = query.filter(Certificate.deleted == False) # noqa query = query.filter(Certificate.deleted == false())
result = database.sort_and_page(query, Certificate, args) result = database.sort_and_page(query, Certificate, args)
return result return result

View File

@ -82,4 +82,4 @@ def get_key_type_from_csr(data):
raise Exception("Unsupported key type") raise Exception("Unsupported key type")
except NotImplemented: except NotImplemented:
raise NotImplemented() raise NotImplementedError

View File

@ -1155,6 +1155,7 @@ class NotificationCertificatesList(AuthenticatedResource):
) )
parser.add_argument("creator", type=str, location="args") parser.add_argument("creator", type=str, location="args")
parser.add_argument("show", type=str, location="args") parser.add_argument("show", type=str, location="args")
parser.add_argument("showExpired", type=int, location="args")
args = parser.parse_args() args = parser.parse_args()
args["notification_id"] = notification_id args["notification_id"] = notification_id

View File

@ -31,6 +31,9 @@ class DestinationOutputSchema(LemurOutputSchema):
def fill_object(self, data): def fill_object(self, data):
if data: if data:
data["plugin"]["pluginOptions"] = data["options"] data["plugin"]["pluginOptions"] = data["options"]
for option in data["plugin"]["pluginOptions"]:
if "export-plugin" in option["type"]:
option["value"]["pluginOptions"] = option["value"]["plugin_options"]
return data return data

View File

@ -41,12 +41,14 @@ def create(label, plugin_name, options, description=None):
return database.create(destination) return database.create(destination)
def update(destination_id, label, options, description): def update(destination_id, label, plugin_name, options, description):
""" """
Updates an existing destination. Updates an existing destination.
:param destination_id: Lemur assigned ID :param destination_id: Lemur assigned ID
:param label: Destination common name :param label: Destination common name
:param plugin_name:
:param options:
:param description: :param description:
:rtype : Destination :rtype : Destination
:return: :return:
@ -54,6 +56,11 @@ def update(destination_id, label, options, description):
destination = get(destination_id) destination = get(destination_id)
destination.label = label destination.label = label
destination.plugin_name = plugin_name
# remove any sub-plugin objects before try to save the json options
for option in options:
if "plugin" in option["type"]:
del option["value"]["plugin_object"]
destination.options = options destination.options = options
destination.description = description destination.description = description

View File

@ -338,6 +338,7 @@ class Destinations(AuthenticatedResource):
return service.update( return service.update(
destination_id, destination_id,
data["label"], data["label"],
data["plugin"]["slug"],
data["plugin"]["plugin_options"], data["plugin"]["plugin_options"],
data["description"], data["description"],
) )

View File

@ -10,7 +10,7 @@
""" """
import os import os
import imp import importlib
import errno import errno
import pkg_resources import pkg_resources
import socket import socket
@ -73,8 +73,9 @@ def from_file(file_path, silent=False):
:param file_path: :param file_path:
:param silent: :param silent:
""" """
d = imp.new_module("config") module_spec = importlib.util.spec_from_file_location("config", file_path)
d.__file__ = file_path d = importlib.util.module_from_spec(module_spec)
try: try:
with open(file_path) as config_file: with open(file_path) as config_file:
exec( # nosec: config file safe exec( # nosec: config file safe

View File

@ -7,7 +7,7 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from sqlalchemy import Column, Integer, ForeignKey, PassiveDefault, func, Enum from sqlalchemy import Column, Integer, ForeignKey, DefaultClause, func, Enum
from sqlalchemy_utils.types.arrow import ArrowType from sqlalchemy_utils.types.arrow import ArrowType
@ -29,5 +29,5 @@ class Log(db.Model):
), ),
nullable=False, nullable=False,
) )
logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False) logged_at = Column(ArrowType(), DefaultClause(func.now()), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False)

View File

@ -74,6 +74,7 @@ def downgrade():
"update certificates set key_type=null where not_after > CURRENT_DATE - 32" "update certificates set key_type=null where not_after > CURRENT_DATE - 32"
) )
op.execute(stmt) op.execute(stmt)
commit()
""" """

View File

@ -16,6 +16,7 @@ from itertools import groupby
import arrow import arrow
from flask import current_app from flask import current_app
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.sql.expression import false, true
from lemur import database from lemur import database
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
@ -40,10 +41,10 @@ def get_certificates(exclude=None):
q = ( q = (
database.db.session.query(Certificate) database.db.session.query(Certificate)
.filter(Certificate.not_after <= max) .filter(Certificate.not_after <= max)
.filter(Certificate.notify == True) .filter(Certificate.notify == true())
.filter(Certificate.expired == False) .filter(Certificate.expired == false())
.filter(Certificate.revoked == False) .filter(Certificate.revoked == false())
) # noqa )
exclude_conditions = [] exclude_conditions = []
if exclude: if exclude:
@ -137,11 +138,11 @@ def send_expiration_notifications(exclude):
# security team gets all # security team gets all
security_email = current_app.config.get("LEMUR_SECURITY_TEAM_EMAIL") security_email = current_app.config.get("LEMUR_SECURITY_TEAM_EMAIL")
security_data = []
for owner, notification_group in get_eligible_certificates(exclude=exclude).items(): for owner, notification_group in get_eligible_certificates(exclude=exclude).items():
for notification_label, certificates in notification_group.items(): for notification_label, certificates in notification_group.items():
notification_data = [] notification_data = []
security_data = []
notification = certificates[0][0] notification = certificates[0][0]

View File

@ -43,7 +43,7 @@ def create_default_expiration_notifications(name, recipients, intervals=None):
"name": "recipients", "name": "recipients",
"type": "str", "type": "str",
"required": True, "required": True,
"validation": "^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},?)+$", "validation": r"^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},?)+$",
"helpMessage": "Comma delimited list of email addresses", "helpMessage": "Comma delimited list of email addresses",
"value": ",".join(recipients), "value": ",".join(recipients),
}, },
@ -63,7 +63,7 @@ def create_default_expiration_notifications(name, recipients, intervals=None):
"name": "interval", "name": "interval",
"type": "int", "type": "int",
"required": True, "required": True,
"validation": "^\d+$", "validation": r"^\d+$",
"helpMessage": "Number of days to be alert before expiration.", "helpMessage": "Number of days to be alert before expiration.",
"value": i, "value": i,
} }
@ -104,12 +104,13 @@ def create(label, plugin_name, options, description, certificates):
return database.create(notification) return database.create(notification)
def update(notification_id, label, options, description, active, certificates): def update(notification_id, label, plugin_name, options, description, active, certificates):
""" """
Updates an existing notification. Updates an existing notification.
:param notification_id: :param notification_id:
:param label: Notification label :param label: Notification label
:param plugin_name:
:param options: :param options:
:param description: :param description:
:param active: :param active:
@ -120,6 +121,7 @@ def update(notification_id, label, options, description, active, certificates):
notification = get(notification_id) notification = get(notification_id)
notification.label = label notification.label = label
notification.plugin_name = plugin_name
notification.options = options notification.options = options
notification.description = description notification.description = description
notification.active = active notification.active = active

View File

@ -340,6 +340,7 @@ class Notifications(AuthenticatedResource):
return service.update( return service.update(
notification_id, notification_id,
data["label"], data["label"],
data["plugin"]["slug"],
data["plugin"]["plugin_options"], data["plugin"]["plugin_options"],
data["description"], data["description"],
data["active"], data["active"],

View File

@ -9,7 +9,7 @@ from sqlalchemy import (
Integer, Integer,
ForeignKey, ForeignKey,
String, String,
PassiveDefault, DefaultClause,
func, func,
Column, Column,
Text, Text,
@ -76,14 +76,14 @@ class PendingCertificate(db.Model):
chain = Column(Text()) chain = Column(Text())
private_key = Column(Vault, nullable=True) private_key = Column(Vault, nullable=True)
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False) date_created = Column(ArrowType, DefaultClause(func.now()), nullable=False)
dns_provider_id = Column( dns_provider_id = Column(
Integer, ForeignKey("dns_providers.id", ondelete="CASCADE") Integer, ForeignKey("dns_providers.id", ondelete="CASCADE")
) )
status = Column(Text(), nullable=True) status = Column(Text(), nullable=True)
last_updated = Column( last_updated = Column(
ArrowType, PassiveDefault(func.now()), onupdate=func.now(), nullable=False ArrowType, DefaultClause(func.now()), onupdate=func.now(), nullable=False
) )
rotation = Column(Boolean, default=False) rotation = Column(Boolean, default=False)

View File

@ -42,7 +42,7 @@ class ExpirationNotificationPlugin(NotificationPlugin):
"name": "interval", "name": "interval",
"type": "int", "type": "int",
"required": True, "required": True,
"validation": "^\d+$", "validation": r"^\d+$",
"helpMessage": "Number of days to be alert before expiration.", "helpMessage": "Number of days to be alert before expiration.",
}, },
{ {

View File

@ -16,6 +16,7 @@ import json
import time import time
import OpenSSL.crypto import OpenSSL.crypto
import dns.resolver
import josepy as jose import josepy as jose
from acme import challenges, errors, messages from acme import challenges, errors, messages
from acme.client import BackwardsCompatibleClientV2, ClientNetwork from acme.client import BackwardsCompatibleClientV2, ClientNetwork
@ -23,7 +24,6 @@ from acme.errors import PollError, TimeoutError, WildcardUnsupportedError
from acme.messages import Error as AcmeError from acme.messages import Error as AcmeError
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from flask import current_app from flask import current_app
from lemur.authorizations import service as authorization_service from lemur.authorizations import service as authorization_service
from lemur.common.utils import generate_private_key from lemur.common.utils import generate_private_key
from lemur.dns_providers import service as dns_provider_service from lemur.dns_providers import service as dns_provider_service
@ -37,8 +37,9 @@ from retrying import retry
class AuthorizationRecord(object): class AuthorizationRecord(object):
def __init__(self, host, authz, dns_challenge, change_id): def __init__(self, domain, target_domain, authz, dns_challenge, change_id):
self.host = host self.domain = domain
self.target_domain = target_domain
self.authz = authz self.authz = authz
self.dns_challenge = dns_challenge self.dns_challenge = dns_challenge
self.change_id = change_id self.change_id = change_id
@ -91,19 +92,18 @@ class AcmeHandler(object):
self, self,
acme_client, acme_client,
account_number, account_number,
host, domain,
target_domain,
dns_provider, dns_provider,
order, order,
dns_provider_options, dns_provider_options,
): ):
current_app.logger.debug("Starting DNS challenge for {0}".format(host)) current_app.logger.debug(f"Starting DNS challenge for {domain} using target domain {target_domain}.")
change_ids = [] change_ids = []
dns_challenges = self.get_dns_challenges(host, order.authorizations) dns_challenges = self.get_dns_challenges(domain, order.authorizations)
host_to_validate, _ = self.strip_wildcard(host) host_to_validate, _ = self.strip_wildcard(target_domain)
host_to_validate = self.maybe_add_extension( host_to_validate = self.maybe_add_extension(host_to_validate, dns_provider_options)
host_to_validate, dns_provider_options
)
if not dns_challenges: if not dns_challenges:
sentry.captureException() sentry.captureException()
@ -111,15 +111,20 @@ class AcmeHandler(object):
raise Exception("Unable to determine DNS challenges from authorizations") raise Exception("Unable to determine DNS challenges from authorizations")
for dns_challenge in dns_challenges: for dns_challenge in dns_challenges:
# Only prepend '_acme-challenge' if not using CNAME redirection
if domain == target_domain:
host_to_validate = dns_challenge.validation_domain_name(host_to_validate)
change_id = dns_provider.create_txt_record( change_id = dns_provider.create_txt_record(
dns_challenge.validation_domain_name(host_to_validate), host_to_validate,
dns_challenge.validation(acme_client.client.net.key), dns_challenge.validation(acme_client.client.net.key),
account_number, account_number,
) )
change_ids.append(change_id) change_ids.append(change_id)
return AuthorizationRecord( return AuthorizationRecord(
host, order.authorizations, dns_challenges, change_ids domain, target_domain, order.authorizations, dns_challenges, change_ids
) )
def complete_dns_challenge(self, acme_client, authz_record): def complete_dns_challenge(self, acme_client, authz_record):
@ -128,11 +133,11 @@ class AcmeHandler(object):
authz_record.authz[0].body.identifier.value authz_record.authz[0].body.identifier.value
) )
) )
dns_providers = self.dns_providers_for_domain.get(authz_record.host) dns_providers = self.dns_providers_for_domain.get(authz_record.target_domain)
if not dns_providers: if not dns_providers:
metrics.send("complete_dns_challenge_error_no_dnsproviders", "counter", 1) metrics.send("complete_dns_challenge_error_no_dnsproviders", "counter", 1)
raise Exception( raise Exception(
"No DNS providers found for domain: {}".format(authz_record.host) "No DNS providers found for domain: {}".format(authz_record.target_domain)
) )
for dns_provider in dns_providers: for dns_provider in dns_providers:
@ -160,7 +165,7 @@ class AcmeHandler(object):
verified = response.simple_verify( verified = response.simple_verify(
dns_challenge.chall, dns_challenge.chall,
authz_record.host, authz_record.target_domain,
acme_client.client.net.key.public_key(), acme_client.client.net.key.public_key(),
) )
@ -311,12 +316,24 @@ class AcmeHandler(object):
authorizations = [] authorizations = []
for domain in order_info.domains: for domain in order_info.domains:
if not self.dns_providers_for_domain.get(domain):
# If CNAME exists, set host to the target address
target_domain = domain
if current_app.config.get("ACME_ENABLE_DELEGATED_CNAME", False):
cname_result, _ = self.strip_wildcard(domain)
cname_result = challenges.DNS01().validation_domain_name(cname_result)
cname_result = self.get_cname(cname_result)
if cname_result:
target_domain = cname_result
self.autodetect_dns_providers(target_domain)
if not self.dns_providers_for_domain.get(target_domain):
metrics.send( metrics.send(
"get_authorizations_no_dns_provider_for_domain", "counter", 1 "get_authorizations_no_dns_provider_for_domain", "counter", 1
) )
raise Exception("No DNS providers found for domain: {}".format(domain)) raise Exception("No DNS providers found for domain: {}".format(target_domain))
for dns_provider in self.dns_providers_for_domain[domain]:
for dns_provider in self.dns_providers_for_domain[target_domain]:
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type) dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
dns_provider_options = json.loads(dns_provider.credentials) dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id") account_number = dns_provider_options.get("account_id")
@ -324,6 +341,7 @@ class AcmeHandler(object):
acme_client, acme_client,
account_number, account_number,
domain, domain,
target_domain,
dns_provider_plugin, dns_provider_plugin,
order, order,
dns_provider.options, dns_provider.options,
@ -358,7 +376,7 @@ class AcmeHandler(object):
for authz_record in authorizations: for authz_record in authorizations:
dns_challenges = authz_record.dns_challenge dns_challenges = authz_record.dns_challenge
for dns_challenge in dns_challenges: for dns_challenge in dns_challenges:
dns_providers = self.dns_providers_for_domain.get(authz_record.host) dns_providers = self.dns_providers_for_domain.get(authz_record.target_domain)
for dns_provider in dns_providers: for dns_provider in dns_providers:
# Grab account number (For Route53) # Grab account number (For Route53)
dns_provider_plugin = self.get_dns_provider( dns_provider_plugin = self.get_dns_provider(
@ -366,14 +384,14 @@ class AcmeHandler(object):
) )
dns_provider_options = json.loads(dns_provider.credentials) dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id") account_number = dns_provider_options.get("account_id")
host_to_validate, _ = self.strip_wildcard(authz_record.host) host_to_validate, _ = self.strip_wildcard(authz_record.target_domain)
host_to_validate = self.maybe_add_extension( host_to_validate = self.maybe_add_extension(host_to_validate, dns_provider_options)
host_to_validate, dns_provider_options if authz_record.domain == authz_record.target_domain:
) host_to_validate = challenges.DNS01().validation_domain_name(host_to_validate)
dns_provider_plugin.delete_txt_record( dns_provider_plugin.delete_txt_record(
authz_record.change_id, authz_record.change_id,
account_number, account_number,
dns_challenge.validation_domain_name(host_to_validate), host_to_validate,
dns_challenge.validation(acme_client.client.net.key), dns_challenge.validation(acme_client.client.net.key),
) )
@ -392,23 +410,26 @@ class AcmeHandler(object):
:return: :return:
""" """
for authz_record in authorizations: for authz_record in authorizations:
dns_providers = self.dns_providers_for_domain.get(authz_record.host) dns_providers = self.dns_providers_for_domain.get(authz_record.target_domain)
for dns_provider in dns_providers: for dns_provider in dns_providers:
# Grab account number (For Route53) # Grab account number (For Route53)
dns_provider_options = json.loads(dns_provider.credentials) dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id") account_number = dns_provider_options.get("account_id")
dns_challenges = authz_record.dns_challenge dns_challenges = authz_record.dns_challenge
host_to_validate, _ = self.strip_wildcard(authz_record.host) host_to_validate, _ = self.strip_wildcard(authz_record.target_domain)
host_to_validate = self.maybe_add_extension( host_to_validate = self.maybe_add_extension(
host_to_validate, dns_provider_options host_to_validate, dns_provider_options
) )
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type) dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
for dns_challenge in dns_challenges: for dns_challenge in dns_challenges:
if authz_record.domain == authz_record.target_domain:
host_to_validate = dns_challenge.validation_domain_name(host_to_validate)
try: try:
dns_provider_plugin.delete_txt_record( dns_provider_plugin.delete_txt_record(
authz_record.change_id, authz_record.change_id,
account_number, account_number,
dns_challenge.validation_domain_name(host_to_validate), host_to_validate,
dns_challenge.validation(acme_client.client.net.key), dns_challenge.validation(acme_client.client.net.key),
) )
except Exception as e: except Exception as e:
@ -431,6 +452,18 @@ class AcmeHandler(object):
raise UnknownProvider("No such DNS provider: {}".format(type)) raise UnknownProvider("No such DNS provider: {}".format(type))
return provider return provider
def get_cname(self, domain):
"""
:param domain: Domain name to look up a CNAME for.
:return: First CNAME target or False if no CNAME record exists.
"""
try:
result = dns.resolver.query(domain, 'CNAME')
if len(result) > 0:
return str(result[0].target).rstrip('.')
except dns.exception.DNSException:
return False
class ACMEIssuerPlugin(IssuerPlugin): class ACMEIssuerPlugin(IssuerPlugin):
title = "Acme" title = "Acme"
@ -448,7 +481,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
"name": "acme_url", "name": "acme_url",
"type": "str", "type": "str",
"required": True, "required": True,
"validation": "/^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$/", "validation": r"/^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$/",
"helpMessage": "Must be a valid web url starting with http[s]://", "helpMessage": "Must be a valid web url starting with http[s]://",
}, },
{ {
@ -461,7 +494,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
"name": "email", "name": "email",
"type": "str", "type": "str",
"default": "", "default": "",
"validation": "/^?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)$/", "validation": r"/^?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)$/",
"helpMessage": "Email to use", "helpMessage": "Email to use",
}, },
{ {

View File

@ -3,6 +3,7 @@ from unittest.mock import patch, Mock
import josepy as jose import josepy as jose
from cryptography.x509 import DNSName from cryptography.x509 import DNSName
from flask import Flask
from lemur.plugins.lemur_acme import plugin from lemur.plugins.lemur_acme import plugin
from lemur.common.utils import generate_private_key from lemur.common.utils import generate_private_key
from mock import MagicMock from mock import MagicMock
@ -22,6 +23,16 @@ class TestAcme(unittest.TestCase):
"test.fakedomain.net": [mock_dns_provider], "test.fakedomain.net": [mock_dns_provider],
} }
# Creates a new Flask application for a test duration. In python 3.8, manual push of application context is
# needed to run tests in dev environment without getting error 'Working outside of application context'.
_app = Flask('lemur_test_acme')
self.ctx = _app.app_context()
assert self.ctx
self.ctx.push()
def tearDown(self):
self.ctx.pop()
@patch("lemur.plugins.lemur_acme.plugin.len", return_value=1) @patch("lemur.plugins.lemur_acme.plugin.len", return_value=1)
def test_get_dns_challenges(self, mock_len): def test_get_dns_challenges(self, mock_len):
assert mock_len assert mock_len
@ -49,7 +60,7 @@ class TestAcme(unittest.TestCase):
self.assertEqual(expected, result) self.assertEqual(expected, result)
def test_authz_record(self): def test_authz_record(self):
a = plugin.AuthorizationRecord("host", "authz", "challenge", "id") a = plugin.AuthorizationRecord("domain", "host", "authz", "challenge", "id")
self.assertEqual(type(a), plugin.AuthorizationRecord) self.assertEqual(type(a), plugin.AuthorizationRecord)
@patch("acme.client.Client") @patch("acme.client.Client")
@ -79,7 +90,7 @@ class TestAcme(unittest.TestCase):
iterator = iter(values) iterator = iter(values)
iterable.__iter__.return_value = iterator iterable.__iter__.return_value = iterator
result = self.acme.start_dns_challenge( result = self.acme.start_dns_challenge(
mock_acme, "accountid", "host", mock_dns_provider, mock_order, {} mock_acme, "accountid", "domain", "host", mock_dns_provider, mock_order, {}
) )
self.assertEqual(type(result), plugin.AuthorizationRecord) self.assertEqual(type(result), plugin.AuthorizationRecord)
@ -97,7 +108,7 @@ class TestAcme(unittest.TestCase):
mock_authz.dns_challenge.response = Mock() mock_authz.dns_challenge.response = Mock()
mock_authz.dns_challenge.response.simple_verify = Mock(return_value=True) mock_authz.dns_challenge.response.simple_verify = Mock(return_value=True)
mock_authz.authz = [] mock_authz.authz = []
mock_authz.host = "www.test.com" mock_authz.target_domain = "www.test.com"
mock_authz_record = Mock() mock_authz_record = Mock()
mock_authz_record.body.identifier.value = "test" mock_authz_record.body.identifier.value = "test"
mock_authz.authz.append(mock_authz_record) mock_authz.authz.append(mock_authz_record)
@ -117,22 +128,24 @@ class TestAcme(unittest.TestCase):
mock_dns_provider = Mock() mock_dns_provider = Mock()
mock_dns_provider.wait_for_dns_change = Mock(return_value=True) mock_dns_provider.wait_for_dns_change = Mock(return_value=True)
mock_dns_challenge = Mock()
response = Mock()
response.simple_verify = Mock(return_value=False)
mock_dns_challenge.response = Mock(return_value=response)
mock_authz = Mock() mock_authz = Mock()
mock_authz.dns_challenge.response = Mock() mock_authz.dns_challenge = []
mock_authz.dns_challenge.response.simple_verify = Mock(return_value=False) mock_authz.dns_challenge.append(mock_dns_challenge)
mock_authz.authz = []
mock_authz.host = "www.test.com" mock_authz.target_domain = "www.test.com"
mock_authz_record = Mock() mock_authz_record = Mock()
mock_authz_record.body.identifier.value = "test" mock_authz_record.body.identifier.value = "test"
mock_authz.authz = []
mock_authz.authz.append(mock_authz_record) mock_authz.authz.append(mock_authz_record)
mock_authz.change_id = [] mock_authz.change_id = []
mock_authz.change_id.append("123") mock_authz.change_id.append("123")
mock_authz.dns_challenge = [] with self.assertRaises(ValueError):
dns_challenge = Mock() self.acme.complete_dns_challenge(mock_acme, mock_authz)
mock_authz.dns_challenge.append(dns_challenge)
self.assertRaises(
ValueError, self.acme.complete_dns_challenge(mock_acme, mock_authz)
)
@patch("acme.client.Client") @patch("acme.client.Client")
@patch("OpenSSL.crypto", return_value="mock_cert") @patch("OpenSSL.crypto", return_value="mock_cert")
@ -270,11 +283,9 @@ class TestAcme(unittest.TestCase):
result, [options["common_name"], "test2.netflix.net"] result, [options["common_name"], "test2.netflix.net"]
) )
@patch( @patch("lemur.plugins.lemur_acme.plugin.AcmeHandler.start_dns_challenge", return_value="test")
"lemur.plugins.lemur_acme.plugin.AcmeHandler.start_dns_challenge", @patch("lemur.plugins.lemur_acme.plugin.current_app", return_value=False)
return_value="test", def test_get_authorizations(self, mock_current_app, mock_start_dns_challenge):
)
def test_get_authorizations(self, mock_start_dns_challenge):
mock_order = Mock() mock_order = Mock()
mock_order.body.identifiers = [] mock_order.body.identifiers = []
mock_domain = Mock() mock_domain = Mock()

View File

@ -1,5 +1,7 @@
import unittest import unittest
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
from flask import Flask
from lemur.plugins.lemur_acme import plugin, powerdns from lemur.plugins.lemur_acme import plugin, powerdns
@ -17,6 +19,16 @@ class TestPowerdns(unittest.TestCase):
"test.fakedomain.net": [mock_dns_provider], "test.fakedomain.net": [mock_dns_provider],
} }
# Creates a new Flask application for a test duration. In python 3.8, manual push of application context is
# needed to run tests in dev environment without getting error 'Working outside of application context'.
_app = Flask('lemur_test_acme')
self.ctx = _app.app_context()
assert self.ctx
self.ctx.push()
def tearDown(self):
self.ctx.pop()
@patch("lemur.plugins.lemur_acme.powerdns.current_app") @patch("lemur.plugins.lemur_acme.powerdns.current_app")
def test_get_zones(self, mock_current_app): def test_get_zones(self, mock_current_app):
account_number = "1234567890" account_number = "1234567890"

View File

@ -1,6 +1,7 @@
import unittest import unittest
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
from flask import Flask
from lemur.plugins.lemur_acme import plugin, ultradns from lemur.plugins.lemur_acme import plugin, ultradns
from requests.models import Response from requests.models import Response
@ -19,6 +20,16 @@ class TestUltradns(unittest.TestCase):
"test.fakedomain.net": [mock_dns_provider], "test.fakedomain.net": [mock_dns_provider],
} }
# Creates a new Flask application for a test duration. In python 3.8, manual push of application context is
# needed to run tests in dev environment without getting error 'Working outside of application context'.
_app = Flask('lemur_test_acme')
self.ctx = _app.app_context()
assert self.ctx
self.ctx.push()
def tearDown(self):
self.ctx.pop()
@patch("lemur.plugins.lemur_acme.ultradns.requests") @patch("lemur.plugins.lemur_acme.ultradns.requests")
@patch("lemur.plugins.lemur_acme.ultradns.current_app") @patch("lemur.plugins.lemur_acme.ultradns.current_app")
def test_ultradns_get_token(self, mock_current_app, mock_requests): def test_ultradns_get_token(self, mock_current_app, mock_requests):

View File

@ -33,6 +33,7 @@
.. moduleauthor:: Harm Weites <harm@weites.com> .. moduleauthor:: Harm Weites <harm@weites.com>
""" """
import sys
from acme.errors import ClientError from acme.errors import ClientError
from flask import current_app from flask import current_app
@ -408,6 +409,47 @@ class S3DestinationPlugin(ExportDestinationPlugin):
account_number=self.get_option("accountNumber", options), account_number=self.get_option("accountNumber", options),
) )
def upload_acme_token(self, token_path, token, options, **kwargs):
"""
This is called from the acme http challenge
:param self:
:param token_path:
:param token:
:param options:
:param kwargs:
:return:
"""
current_app.logger.debug("S3 destination plugin is started for HTTP-01 challenge")
function = f"{__name__}.{sys._getframe().f_code.co_name}"
account_number = self.get_option("accountNumber", options)
bucket_name = self.get_option("bucket", options)
prefix = self.get_option("prefix", options)
region = self.get_option("region", options)
filename = token_path.split("/")[-1]
if not prefix.endswith("/"):
prefix + "/"
res = s3.put(bucket_name=bucket_name,
region_name=region,
prefix=prefix + filename,
data=token,
encrypt=False,
account_number=account_number)
res = "Success" if res else "Failure"
log_data = {
"function": function,
"message": "check if any valid certificate is revoked",
"result": res,
"bucket_name": bucket_name,
"filename": filename
}
current_app.logger.info(log_data)
metrics.send(f"{function}", "counter", 1, metric_tags={"result": res,
"bucket_name": bucket_name,
"filename": filename})
class SNSNotificationPlugin(ExpirationNotificationPlugin): class SNSNotificationPlugin(ExpirationNotificationPlugin):
title = "AWS SNS" title = "AWS SNS"

View File

@ -6,12 +6,15 @@
: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>
""" """
from botocore.exceptions import ClientError
from flask import current_app from flask import current_app
from lemur.extensions import sentry
from .sts import sts_client from .sts import sts_client
@sts_client("s3", service_type="resource") @sts_client("s3", service_type="resource")
def put(bucket_name, region, prefix, data, encrypt, **kwargs): def put(bucket_name, region_name, prefix, data, encrypt, **kwargs):
""" """
Use STS to write to an S3 bucket Use STS to write to an S3 bucket
""" """
@ -32,4 +35,41 @@ def put(bucket_name, region, prefix, data, encrypt, **kwargs):
ServerSideEncryption="AES256", ServerSideEncryption="AES256",
) )
else: else:
bucket.put_object(Key=prefix, Body=data, ACL="bucket-owner-full-control") try:
bucket.put_object(Key=prefix, Body=data, ACL="bucket-owner-full-control")
return True
except ClientError:
sentry.captureException()
return False
@sts_client("s3", service_type="client")
def delete(bucket_name, prefixed_object_name, **kwargs):
"""
Use STS to delete an object
"""
try:
response = kwargs["client"].delete_object(Bucket=bucket_name, Key=prefixed_object_name)
current_app.logger.debug(f"Delete data from S3."
f"Bucket: {bucket_name},"
f"Prefix: {prefixed_object_name},"
f"Status_code: {response}")
return response['ResponseMetadata']['HTTPStatusCode'] < 300
except ClientError:
sentry.captureException()
return False
@sts_client("s3", service_type="client")
def get(bucket_name, prefixed_object_name, **kwargs):
"""
Use STS to get an object
"""
try:
response = kwargs["client"].get_object(Bucket=bucket_name, Key=prefixed_object_name)
current_app.logger.debug(f"Get data from S3. Bucket: {bucket_name},"
f"object_name: {prefixed_object_name}")
return response['Body'].read().decode("utf-8")
except ClientError:
sentry.captureException()
return None

View File

@ -1,5 +1,82 @@
import boto3
from moto import mock_sts, mock_s3
def test_get_certificates(app): def test_get_certificates(app):
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
p = plugins.get("aws-s3") p = plugins.get("aws-s3")
assert p assert p
@mock_sts()
@mock_s3()
def test_upload_acme_token(app):
from lemur.plugins.base import plugins
from lemur.plugins.lemur_aws.s3 import get
bucket = "public-bucket"
account = "123456789012"
prefix = "some-path/more-path/"
token_content = "Challenge"
token_name = "TOKEN"
token_path = ".well-known/acme-challenge/" + token_name
additional_options = [
{
"name": "bucket",
"value": bucket,
"type": "str",
"required": True,
"validation": r"[0-9a-z.-]{3,63}",
"helpMessage": "Must be a valid S3 bucket name!",
},
{
"name": "accountNumber",
"type": "str",
"value": account,
"required": True,
"validation": r"[0-9]{12}",
"helpMessage": "A valid AWS account number with permission to access S3",
},
{
"name": "region",
"type": "str",
"default": "us-east-1",
"required": False,
"helpMessage": "Region bucket exists",
"available": ["us-east-1", "us-west-2", "eu-west-1"],
},
{
"name": "encrypt",
"type": "bool",
"value": False,
"required": False,
"helpMessage": "Enable server side encryption",
"default": True,
},
{
"name": "prefix",
"type": "str",
"value": prefix,
"required": False,
"helpMessage": "Must be a valid S3 object prefix!",
},
]
s3_client = boto3.client('s3')
s3_client.create_bucket(Bucket=bucket)
p = plugins.get("aws-s3")
p.upload_acme_token(token_path=token_path,
token_content=token_content,
token=token_content,
options=additional_options)
response = get(bucket_name=bucket,
prefixed_object_name=prefix + token_name,
encrypt=False,
account_number=account)
# put data, and getting the same data
assert (response == token_content)

View File

@ -0,0 +1,41 @@
import boto3
from moto import mock_sts, mock_s3
@mock_sts()
@mock_s3()
def test_put_delete_s3_object(app):
from lemur.plugins.lemur_aws.s3 import put, delete, get
bucket = "public-bucket"
region = "us-east-1"
account = "123456789012"
path = "some-path/foo"
data = "dummy data"
s3_client = boto3.client('s3')
s3_client.create_bucket(Bucket=bucket)
put(bucket_name=bucket,
region_name=region,
prefix=path,
data=data,
encrypt=False,
account_number=account,
region=region)
response = get(bucket_name=bucket, prefixed_object_name=path, account_number=account)
# put data, and getting the same data
assert (response == data)
response = get(bucket_name="wrong-bucket", prefixed_object_name=path, account_number=account)
# attempting to get thccle wrong data
assert (response is None)
delete(bucket_name=bucket, prefixed_object_name=path, account_number=account)
response = get(bucket_name=bucket, prefixed_object_name=path, account_number=account)
# delete data, and getting the same data
assert (response is None)

View File

@ -91,7 +91,7 @@ class EmailNotificationPlugin(ExpirationNotificationPlugin):
"name": "recipients", "name": "recipients",
"type": "str", "type": "str",
"required": True, "required": True,
"validation": "^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},?)+$", "validation": r"^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},?)+$",
"helpMessage": "Comma delimited list of email addresses", "helpMessage": "Comma delimited list of email addresses",
} }
] ]

View File

@ -47,7 +47,7 @@ class SFTPDestinationPlugin(DestinationPlugin):
"type": "int", "type": "int",
"required": True, "required": True,
"helpMessage": "The SFTP port, default is 22.", "helpMessage": "The SFTP port, default is 22.",
"validation": "^(6553[0-5]|655[0-2][0-9]\d|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|[1-9](\d){0,3})", "validation": r"^(6553[0-5]|655[0-2][0-9]\d|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|[1-9](\d){0,3})",
"default": "22", "default": "22",
}, },
{ {

View File

@ -89,7 +89,7 @@ class SlackNotificationPlugin(ExpirationNotificationPlugin):
"name": "webhook", "name": "webhook",
"type": "str", "type": "str",
"required": True, "required": True,
"validation": "^https:\/\/hooks\.slack\.com\/services\/.+$", "validation": r"^https:\/\/hooks\.slack\.com\/services\/.+$",
"helpMessage": "The url Slack told you to use for this integration", "helpMessage": "The url Slack told you to use for this integration",
}, },
{ {

View File

@ -264,13 +264,14 @@ def create(label, plugin_name, options, description=None):
return database.create(source) return database.create(source)
def update(source_id, label, options, description): def update(source_id, label, plugin_name, options, description):
""" """
Updates an existing source. Updates an existing source.
:param source_id: Lemur assigned ID :param source_id: Lemur assigned ID
:param label: Source common name :param label: Source common name
:param options: :param options:
:param plugin_name:
:param description: :param description:
:rtype : Source :rtype : Source
:return: :return:
@ -278,6 +279,7 @@ def update(source_id, label, options, description):
source = get(source_id) source = get(source_id)
source.label = label source.label = label
source.plugin_name = plugin_name
source.options = options source.options = options
source.description = description source.description = description

View File

@ -284,6 +284,7 @@ class Sources(AuthenticatedResource):
return service.update( return service.update(
source_id, source_id,
data["label"], data["label"],
data["plugin"]["slug"],
data["plugin"]["plugin_options"], data["plugin"]["plugin_options"],
data["description"], data["description"],
) )

View File

@ -21,13 +21,7 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" ng-model="authority.keyType" <select class="form-control" ng-model="authority.keyType"
ng-options="option.value as option.name for option in [ ng-options="option for option in ['RSA2048', 'RSA4096', 'ECCPRIME256V1', 'ECCSECP384R1', 'ECCSECP521R1']"
{'name': 'RSA-2048', 'value': 'RSA2048'},
{'name': 'RSA-4096', 'value': 'RSA4096'},
{'name': 'ECC-PRIME256V1', 'value': 'ECCPRIME256V1'},
{'name': 'ECC-SECP384R1', 'value': 'ECCSECP384R1'},
{'name': 'ECC-SECP521R1', 'value': 'ECCSECP521R1'}]"
ng-init="authority.keyType = 'RSA2048'"> ng-init="authority.keyType = 'RSA2048'">
</select> </select>
</div> </div>

View File

@ -32,12 +32,7 @@
</label> </label>
<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.value as option.name for option in [ ng-options="option for option in ['RSA2048', 'RSA4096', 'ECCPRIME256V1', 'ECCSECP384R1']"
{'name': 'RSA-2048', 'value': 'RSA2048'},
{'name': 'RSA-4096', 'value': 'RSA4096'},
{'name': 'ECC-PRIME256V1', 'value': 'ECCPRIME256V1'},
{'name': 'ECC-SECP384R1', 'value': 'ECCSECP384R1'}]"
ng-init="certificate.keyType = 'RSA2048'"></select> ng-init="certificate.keyType = 'RSA2048'"></select>
</div> </div>
</div> </div>

View File

@ -52,19 +52,19 @@ angular.module('lemur')
if (plugin.slug === $scope.destination.plugin.slug) { if (plugin.slug === $scope.destination.plugin.slug) {
plugin.pluginOptions = $scope.destination.plugin.pluginOptions; plugin.pluginOptions = $scope.destination.plugin.pluginOptions;
$scope.destination.plugin = plugin; $scope.destination.plugin = plugin;
_.each($scope.destination.plugin.pluginOptions, function (option) { PluginService.getByType('export').then(function (plugins) {
if (option.type === 'export-plugin') { $scope.exportPlugins = plugins;
PluginService.getByType('export').then(function (plugins) {
$scope.exportPlugins = plugins;
_.each($scope.destination.plugin.pluginOptions, function (option) {
if (option.type === 'export-plugin') {
_.each($scope.exportPlugins, function (plugin) { _.each($scope.exportPlugins, function (plugin) {
if (plugin.slug === option.value.slug) { if (plugin.slug === option.value.slug) {
plugin.pluginOptions = option.value.pluginOptions; plugin.pluginOptions = option.value.pluginOptions;
option.value = plugin; option.value = plugin;
} }
}); });
}); }
} });
}); });
} }
}); });

View File

@ -42,8 +42,8 @@ angular.module('lemur')
PluginService.getByType('notification').then(function (plugins) { PluginService.getByType('notification').then(function (plugins) {
$scope.plugins = plugins; $scope.plugins = plugins;
_.each($scope.plugins, function (plugin) { _.each($scope.plugins, function (plugin) {
if (plugin.slug === $scope.notification.pluginName) { if (plugin.slug === $scope.notification.plugin.slug) {
plugin.pluginOptions = $scope.notification.notificationOptions; plugin.pluginOptions = $scope.notification.plugin.pluginOptions;
$scope.notification.plugin = plugin; $scope.notification.plugin = plugin;
} }
}); });
@ -52,16 +52,6 @@ angular.module('lemur')
$scope.page = 1; $scope.page = 1;
}); });
PluginService.getByType('notification').then(function (plugins) {
$scope.plugins = plugins;
_.each($scope.plugins, function (plugin) {
if (plugin.slug === $scope.notification.pluginName) {
plugin.pluginOptions = $scope.notification.notificationOptions;
$scope.notification.plugin = plugin;
}
});
});
$scope.save = function (notification) { $scope.save = function (notification) {
NotificationService.update(notification).then( NotificationService.update(notification).then(
function () { function () {

View File

@ -27,7 +27,7 @@ angular.module('lemur')
}; };
NotificationService.getCertificates = function (notification) { NotificationService.getCertificates = function (notification) {
notification.getList('certificates').then(function (certificates) { notification.getList('certificates', {showExpired: 0}).then(function (certificates) {
notification.certificates = certificates; notification.certificates = certificates;
}); });
}; };
@ -40,7 +40,7 @@ angular.module('lemur')
NotificationService.loadMoreCertificates = function (notification, page) { NotificationService.loadMoreCertificates = function (notification, page) {
notification.getList('certificates', {page: page}).then(function (certificates) { notification.getList('certificates', {page: page, showExpired: 0}).then(function (certificates) {
_.each(certificates, function (certificate) { _.each(certificates, function (certificate) {
notification.certificates.push(certificate); notification.certificates.push(certificate);
}); });

View File

@ -41,22 +41,14 @@ angular.module('lemur')
PluginService.getByType('source').then(function (plugins) { PluginService.getByType('source').then(function (plugins) {
$scope.plugins = plugins; $scope.plugins = plugins;
_.each($scope.plugins, function (plugin) { _.each($scope.plugins, function (plugin) {
if (plugin.slug === $scope.source.pluginName) { if (plugin.slug === $scope.source.plugin.slug) {
plugin.pluginOptions = $scope.source.plugin.pluginOptions;
$scope.source.plugin = plugin; $scope.source.plugin = plugin;
} }
}); });
}); });
}); });
PluginService.getByType('source').then(function (plugins) {
$scope.plugins = plugins;
_.each($scope.plugins, function (plugin) {
if (plugin.slug === $scope.source.pluginName) {
$scope.source.plugin = plugin;
}
});
});
$scope.save = function (source) { $scope.save = function (source) {
SourceService.update(source).then( SourceService.update(source).then(
function () { function () {

View File

@ -13,7 +13,7 @@ class TestDNSProvider(unittest.TestCase):
self.assertFalse(dnsutil.is_valid_domain('example-of-over-63-character-domain-label-length-limit-123456789.com')) self.assertFalse(dnsutil.is_valid_domain('example-of-over-63-character-domain-label-length-limit-123456789.com'))
self.assertTrue(dnsutil.is_valid_domain('_acme-chall.example.com')) self.assertTrue(dnsutil.is_valid_domain('_acme-chall.example.com'))
self.assertFalse(dnsutil.is_valid_domain('e/xample.com')) self.assertFalse(dnsutil.is_valid_domain('e/xample.com'))
self.assertFalse(dnsutil.is_valid_domain('exam\ple.com')) self.assertFalse(dnsutil.is_valid_domain('exam\\ple.com'))
self.assertFalse(dnsutil.is_valid_domain('<example.com')) self.assertFalse(dnsutil.is_valid_domain('<example.com'))
self.assertFalse(dnsutil.is_valid_domain('*.example.com')) self.assertFalse(dnsutil.is_valid_domain('*.example.com'))
self.assertFalse(dnsutil.is_valid_domain('-example.io')) self.assertFalse(dnsutil.is_valid_domain('-example.io'))

View File

@ -81,7 +81,7 @@ class Vault(types.TypeDecorator):
""" """
# required by SQLAlchemy. defines the underlying column type # required by SQLAlchemy. defines the underlying column type
impl = types.Binary impl = types.LargeBinary
def process_bind_param(self, value, dialect): def process_bind_param(self, value, dialect):
""" """

View File

@ -1,6 +1,6 @@
# Run `make up-reqs` to update pinned dependencies in requirement text files # Run `make up-reqs` to update pinned dependencies in requirement text files
flake8==3.5.0 # flake8 3.6.0 is giving erroneous "W605 invalid escape sequence" errors. flake8==3.8.4 # flake8 latest version
pre-commit pre-commit
invoke invoke
twine twine

View File

@ -6,16 +6,16 @@
# #
appdirs==1.4.3 # via virtualenv appdirs==1.4.3 # via virtualenv
bleach==3.1.4 # via readme-renderer bleach==3.1.4 # via readme-renderer
certifi==2020.6.20 # via requests certifi==2020.11.8 # via requests
cffi==1.14.0 # via cryptography cffi==1.14.0 # via cryptography
cfgv==3.1.0 # via pre-commit cfgv==3.1.0 # via pre-commit
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
colorama==0.4.3 # via twine colorama==0.4.3 # via twine
cryptography==3.2 # via secretstorage cryptography==3.2.1 # via secretstorage
distlib==0.3.0 # via virtualenv distlib==0.3.0 # via virtualenv
docutils==0.16 # via readme-renderer docutils==0.16 # via readme-renderer
filelock==3.0.12 # via virtualenv filelock==3.0.12 # via virtualenv
flake8==3.5.0 # via -r requirements-dev.in flake8==3.8.4 # via -r requirements-dev.in
identify==1.4.14 # via pre-commit identify==1.4.14 # via pre-commit
idna==2.9 # via requests idna==2.9 # via requests
invoke==1.4.1 # via -r requirements-dev.in invoke==1.4.1 # via -r requirements-dev.in
@ -24,10 +24,10 @@ 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.7.1 # via -r requirements-dev.in pre-commit==2.8.2 # via -r requirements-dev.in
pycodestyle==2.3.1 # via flake8 pycodestyle==2.6.0 # via flake8
pycparser==2.20 # via cffi pycparser==2.20 # via cffi
pyflakes==1.6.0 # via flake8 pyflakes==2.2.0 # via flake8
pygments==2.6.1 # via readme-renderer 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

View File

@ -17,16 +17,16 @@ 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.5 # via -r requirements.txt boto3==1.16.14 # via -r requirements.txt
botocore==1.19.5 # via -r requirements.txt, boto3, s3transfer botocore==1.19.14 # 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.6.20 # 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
cffi==1.14.0 # via -r requirements.txt, bcrypt, cryptography, pynacl cffi==1.14.0 # via -r requirements.txt, bcrypt, cryptography, pynacl
chardet==3.0.4 # via -r requirements.txt, requests chardet==3.0.4 # via -r requirements.txt, requests
click==7.1.1 # via -r requirements.txt, flask click==7.1.2 # via -r requirements.txt, flask
cloudflare==2.8.13 # via -r requirements.txt cloudflare==2.8.13 # via -r requirements.txt
cryptography==3.2 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests cryptography==3.2.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests
dnspython3==1.15.0 # via -r requirements.txt dnspython3==1.15.0 # via -r requirements.txt
dnspython==1.15.0 # via -r requirements.txt, dnspython3 dnspython==1.15.0 # via -r requirements.txt, dnspython3
docutils==0.15.2 # via sphinx docutils==0.15.2 # via sphinx
@ -92,7 +92,7 @@ six==1.15.0 # via -r requirements.txt, acme, bcrypt, cryptography,
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.2.1 # via -r requirements-docs.in, sphinx-rtd-theme, sphinxcontrib-httpdomain sphinx==3.3.0 # 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

View File

@ -10,21 +10,21 @@ 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.5 # via aws-sam-translator, moto boto3==1.16.14 # via aws-sam-translator, moto
boto==2.49.0 # via moto boto==2.49.0 # via moto
botocore==1.19.5 # via aws-xray-sdk, boto3, moto, s3transfer botocore==1.19.14 # via aws-xray-sdk, boto3, moto, s3transfer
certifi==2020.6.20 # 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
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==7.1.2 # via black, flask click==7.1.2 # via black, flask
coverage==5.3 # via -r requirements-tests.in coverage==5.3 # via -r requirements-tests.in
cryptography==3.2 # via moto, python-jose, sshpubkeys cryptography==3.2.1 # via moto, python-jose, sshpubkeys
decorator==4.4.2 # via networkx 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.0 # via -r requirements-tests.in, factory-boy faker==4.14.2 # via -r requirements-tests.in, factory-boy
fakeredis==1.4.4 # via -r requirements-tests.in fakeredis==1.4.4 # 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
@ -59,9 +59,9 @@ pycparser==2.20 # via cffi
pyflakes==2.2.0 # via -r requirements-tests.in pyflakes==2.2.0 # via -r requirements-tests.in
pyparsing==2.4.7 # via packaging pyparsing==2.4.7 # via packaging
pyrsistent==0.16.0 # via jsonschema pyrsistent==0.16.0 # via jsonschema
pytest-flask==1.0.0 # via -r requirements-tests.in pytest-flask==1.1.0 # via -r requirements-tests.in
pytest-mock==3.3.1 # via -r requirements-tests.in pytest-mock==3.3.1 # via -r requirements-tests.in
pytest==6.1.1 # via -r requirements-tests.in, pytest-flask, pytest-mock pytest==6.1.2 # via -r requirements-tests.in, pytest-flask, pytest-mock
python-dateutil==2.8.1 # via botocore, faker, freezegun, moto python-dateutil==2.8.1 # via botocore, faker, freezegun, moto
python-jose[cryptography]==3.1.0 # via moto python-jose[cryptography]==3.1.0 # via moto
pytz==2019.3 # via moto pytz==2019.3 # via moto

View File

@ -15,16 +15,16 @@ 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.5 # via -r requirements.in boto3==1.16.14 # via -r requirements.in
botocore==1.19.5 # via -r requirements.in, boto3, s3transfer botocore==1.19.14 # 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.6.20 # 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
cffi==1.14.0 # via bcrypt, cryptography, pynacl cffi==1.14.0 # via bcrypt, cryptography, pynacl
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==7.1.1 # via flask click==7.1.2 # via flask
cloudflare==2.8.13 # via -r requirements.in cloudflare==2.8.13 # via -r requirements.in
cryptography==3.2 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests cryptography==3.2.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests
dnspython3==1.15.0 # via -r requirements.in dnspython3==1.15.0 # via -r requirements.in
dnspython==1.15.0 # via dnspython3 dnspython==1.15.0 # via dnspython3
dyn==1.8.1 # via -r requirements.in dyn==1.8.1 # via -r requirements.in