Repository: Netflix/lemur

Issue:
Choice of Route53 DNS 
https://github.com/Netflix/lemur/blob/8b60f10dd63065f4da6eaf9e338e4b3dee50fda4/lemur/plugins/lemur_acme/route53.py#L29

Choice should be on min of DNS - route53 domain suffix.


Example: Two Route53 Zones

Cert requested for (A) test.dev.acme.identity.uq.edu.au

Zones:
(B) acme.identity.uq.edu.au
(C) dev.acme.identity.uq.edu.au

Txt record needed at _acme-validation.test.dev.acme.identity.uq.edu.au

(A) - (B) = 9
(A) - (C) = 5

Choose C for zone.


Discussion:

Resolving patch:
diff --git a/lemur/plugins/base/v1.py b/lemur/plugins/base/v1.py
--- a/lemur/plugins/base/v1.py
+++ b/lemur/plugins/base/v1.py
@@ -6,10 +6,11 @@
 
 .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
 """
-from flask import current_app
+import re
 from threading import local
 from typing import Optional, Dict, List, Any
-import re
+
+from flask import current_app
 
 
 # stolen from https://github.com/getsentry/sentry/
@@ -133,7 +134,7 @@ def validate_option_value(self, option_name, value):
             validation = class_opt.get("validation")
             if not validation:
                 return value
-            if not re.match(validation, value):
+            if (callable(validation) and not validation(value)) or not re.match(validation, value):
                 raise ValueError(f"Option '{option_name}' cannot be validated")
         elif opt_type == "select":
             available = class_opt.get("available")
diff --git a/lemur/plugins/lemur_acme/route53.py b/lemur/plugins/lemur_acme/route53.py
--- a/lemur/plugins/lemur_acme/route53.py
+++ b/lemur/plugins/lemur_acme/route53.py
@@ -16,24 +16,27 @@ def wait_for_dns_change(change_id, client=None):
 
 @sts_client("route53")
 def find_zone_id(domain, client=None):
+    return _find_zone_id(domain, client)
+
+
+def _find_zone_id(domain, client=None):
     paginator = client.get_paginator("list_hosted_zones")
-    zones = []
-    match_length = 0
+    min_diff_length = float("inf")
+    chosen_zone = None
+
     for page in paginator.paginate():
         for zone in page["HostedZones"]:
             if domain.endswith(zone["Name"]) or (domain + ".").endswith(zone["Name"]):
                 if not zone["Config"]["PrivateZone"]:
-                    if len(zone["Name"]) > match_length:
-                        # reset the list, as we have found a longer match
-                        zones = [(zone["Name"], zone["Id"])]
-                        match_length = len(zone["Name"])
-                    elif len(zone["Name"]) == match_length:
-                        # add all equal length zones, though only the first one will be returned
-                        zones.append((zone["Name"], zone["Id"]))
-
-    if not zones:
+                    diff_length = len(domain) - len(zone["Name"])
+                    if diff_length < min_diff_length:
+                        min_diff_length = diff_length
+                        chosen_zone = (zone["Name"], zone["Id"])
+
+    if chosen_zone is None:
         raise ValueError("Unable to find a Route53 hosted zone for {}".format(domain))
-    return zones[0][1]
+
+    return chosen_zone[1]  # Return the chosen zone ID
 
 
 @sts_client("route53")
diff --git a/lemur/plugins/lemur_vault_dest/plugin.py b/lemur/plugins/lemur_vault_dest/plugin.py
--- a/lemur/plugins/lemur_vault_dest/plugin.py
+++ b/lemur/plugins/lemur_vault_dest/plugin.py
@@ -11,19 +11,18 @@
 """
 import os
 import re
+
 import hvac
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
 from flask import current_app
+from validators.url import url
 
 from lemur.common.defaults import common_name, country, state, location, organizational_unit, organization
 from lemur.common.utils import parse_certificate, check_validation
 from lemur.plugins.bases import DestinationPlugin
 from lemur.plugins.bases import SourcePlugin
 
-from cryptography import x509
-from cryptography.hazmat.backends import default_backend
-
-from validators.url import regex as url_regex
-
 
 class VaultSourcePlugin(SourcePlugin):
     """ Class for importing certificates from Hashicorp Vault"""
@@ -40,7 +39,7 @@ class VaultSourcePlugin(SourcePlugin):
             "name": "vaultUrl",
             "type": "str",
             "required": True,
-            "validation": url_regex.pattern,
+            "validation": url,
             "helpMessage": "Valid URL to Hashi Vault instance",
         },
         {
@@ -167,7 +166,7 @@ class VaultDestinationPlugin(DestinationPlugin):
             "name": "vaultUrl",
             "type": "str",
             "required": True,
-            "validation": url_regex.pattern,
+            "validation": url,
             "helpMessage": "Valid URL to Hashi Vault instance",
         },
         {


Test patch:
diff --git a/lemur/plugins/lemur_acme/tests/test_route53.py b/lemur/plugins/lemur_acme/tests/test_route53.py
new file mode 100644
--- /dev/null
+++ b/lemur/plugins/lemur_acme/tests/test_route53.py
@@ -0,0 +1,31 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from lemur.plugins.lemur_acme.route53 import _find_zone_id
+from lemur.tests.conftest import app  # noqa
+
+
+def test_zone_selection(app):  # noqa
+    # Mocking AWS client
+    client = MagicMock()
+
+    zones = [
+        {"Config": {"PrivateZone": False}, "Name": "acme.identity.uq.edu.au.", "Id": "Z1"},
+        {"Config": {"PrivateZone": False}, "Name": "dev.acme.identity.uq.edu.au.", "Id": "Z2"},
+        {"Config": {"PrivateZone": True}, "Name": "test.dev.acme.identity.uq.edu.au.", "Id": "Z3"}
+    ]
+
+    # Mocking the paginator
+    paginator = MagicMock()
+    paginator.paginate.return_value = [{"HostedZones": zones}]
+    client.get_paginator.return_value = paginator
+
+    # Replace this with reference to your function
+    assert _find_zone_id("test.dev.acme.identity.uq.edu.au", client) == "Z2"
+    assert _find_zone_id("another.dev.acme.identity.uq.edu.au", client) == "Z2"
+    assert _find_zone_id("test2.acme.identity.uq.edu.au", client) == "Z1"
+
+    # Test that it raises a ValueError for a domain where no matching zone is found
+    with pytest.raises(ValueError):
+        _find_zone_id("test3.some.other.domain", client)