mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
fix(openstack): block SSRF via auth_url in connection test
This commit is contained in:
8 files changed
+487
No files matched your search
@@ -0,0 +1 @@
|
||||
OpenStack connection test rejects an `auth_url` with a non-HTTP scheme or a host that resolves to a loopback, link-local or private address, so a tenant-supplied clouds.yaml can no longer make the worker probe internal services
|
||||
Whitespace-only changes.
@@ -0,0 +1,142 @@
|
||||
"""Outbound URL validation for provider connection tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
ALLOWED_PRIVATE_NETWORKS_ENV = "PROWLER_ALLOWED_PRIVATE_NETWORKS"
|
||||
|
||||
_NON_PUBLIC_IP_PROPERTIES = (
|
||||
"is_private",
|
||||
"is_loopback",
|
||||
"is_link_local",
|
||||
"is_multicast",
|
||||
"is_reserved",
|
||||
"is_unspecified",
|
||||
)
|
||||
|
||||
# scp-like git remotes (user@host:path) carry no scheme, so urlparse cannot read them
|
||||
_SCP_LIKE_REMOTE = re.compile(r"^(?:[^@/]+@)?(?P<host>[^:/]+):(?!//)")
|
||||
|
||||
_NAT64_WELL_KNOWN_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
|
||||
|
||||
|
||||
class OutboundURLNotAllowedError(Exception):
|
||||
"""A supplied URL points at a destination the worker must not reach."""
|
||||
|
||||
|
||||
def _parse_allowed_networks(raw: str | None) -> tuple:
|
||||
if not raw or not raw.strip():
|
||||
return ()
|
||||
networks = []
|
||||
for entry in raw.split(","):
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
continue
|
||||
try:
|
||||
networks.append(ipaddress.ip_network(entry, strict=False))
|
||||
except ValueError as error:
|
||||
raise OutboundURLNotAllowedError(
|
||||
f"Malformed entry {entry!r} in {ALLOWED_PRIVATE_NETWORKS_ENV}: {error}"
|
||||
)
|
||||
return tuple(networks)
|
||||
|
||||
|
||||
def allowed_private_networks() -> tuple:
|
||||
"""Operator-configured private networks the SSRF guard must not block."""
|
||||
networks = _parse_allowed_networks(os.environ.get(ALLOWED_PRIVATE_NETWORKS_ENV))
|
||||
if networks:
|
||||
logger.warning(
|
||||
f"{ALLOWED_PRIVATE_NETWORKS_ENV} is set — SSRF protection relaxed for private networks: "
|
||||
+ ", ".join(str(network) for network in networks)
|
||||
)
|
||||
return networks
|
||||
|
||||
|
||||
def _unwrap_ipv6(address: ipaddress._BaseAddress) -> ipaddress._BaseAddress:
|
||||
if not isinstance(address, ipaddress.IPv6Address):
|
||||
return address
|
||||
embedded = address.ipv4_mapped or address.sixtofour
|
||||
if embedded is None and address in _NAT64_WELL_KNOWN_PREFIX:
|
||||
embedded = ipaddress.IPv4Address(int(address) & 0xFFFFFFFF)
|
||||
return embedded or address
|
||||
|
||||
|
||||
def _ip_is_non_public(address: str) -> bool:
|
||||
try:
|
||||
parsed = _unwrap_ipv6(ipaddress.ip_address(address))
|
||||
except ValueError:
|
||||
return False
|
||||
# is_global is the broad check; the properties stay because some multicast
|
||||
# ranges report is_global and would otherwise slip through
|
||||
if not parsed.is_global:
|
||||
return True
|
||||
return any(getattr(parsed, prop) for prop in _NON_PUBLIC_IP_PROPERTIES)
|
||||
|
||||
|
||||
def _ip_is_allowlisted(address: str, networks: tuple) -> bool:
|
||||
try:
|
||||
parsed = ipaddress.ip_address(address)
|
||||
except ValueError:
|
||||
return False
|
||||
return any(
|
||||
parsed.version == network.version and parsed in network for network in networks
|
||||
)
|
||||
|
||||
|
||||
def _resolve(host: str) -> set:
|
||||
try:
|
||||
return {sockaddr[0] for *_, sockaddr in socket.getaddrinfo(host, None)}
|
||||
except socket.gaierror as error:
|
||||
raise OutboundURLNotAllowedError(f"Could not resolve host {host!r}: {error}")
|
||||
|
||||
|
||||
def extract_host(url: str) -> str:
|
||||
"""Host of a URL, accepting scp-like git remotes that carry no scheme."""
|
||||
scp_like = _SCP_LIKE_REMOTE.match(url)
|
||||
if scp_like and "://" not in url:
|
||||
return scp_like.group("host")
|
||||
host = urlparse(url).hostname
|
||||
if not host:
|
||||
raise OutboundURLNotAllowedError(f"Could not read a host from URL {url!r}")
|
||||
return host
|
||||
|
||||
|
||||
def validate_outbound_host(host: str) -> None:
|
||||
"""Reject a host that is, or resolves to, a non-public address.
|
||||
|
||||
Resolution happens here and again inside the client that connects, so a
|
||||
hostile DNS server can still answer differently the second time.
|
||||
"""
|
||||
networks = allowed_private_networks()
|
||||
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
addresses = _resolve(host)
|
||||
else:
|
||||
addresses = {host}
|
||||
|
||||
for address in addresses:
|
||||
if _ip_is_non_public(address) and not _ip_is_allowlisted(address, networks):
|
||||
raise OutboundURLNotAllowedError(
|
||||
f"Host {host!r} resolves to non-public address {address} and cannot be reached"
|
||||
)
|
||||
|
||||
|
||||
def validate_outbound_url(
|
||||
url: str, *, allowed_schemes: tuple = ("http", "https")
|
||||
) -> None:
|
||||
"""Reject a URL whose scheme is not allowed or whose host is not public."""
|
||||
scheme = urlparse(url).scheme
|
||||
if scheme and scheme not in allowed_schemes:
|
||||
raise OutboundURLNotAllowedError(
|
||||
f"Disallowed URL scheme {scheme!r}. Allowed: {', '.join(allowed_schemes)}"
|
||||
)
|
||||
validate_outbound_host(extract_host(url))
|
||||
@@ -54,6 +54,10 @@ class OpenStackBaseException(ProwlerException):
|
||||
"message": "Ambiguous region configuration in clouds.yaml",
|
||||
"remediation": "Use either 'region_name' or 'regions' in your cloud configuration, not both.",
|
||||
},
|
||||
(17012, "OpenStackAuthUrlNotAllowedError"): {
|
||||
"message": "OpenStack auth_url points at a destination the connection test cannot reach",
|
||||
"remediation": "Use an http or https auth_url that resolves to a public address, or allow the private network through PROWLER_ALLOWED_PRIVATE_NETWORKS.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, code, file=None, original_exception=None, message=None):
|
||||
@@ -212,3 +216,15 @@ class OpenStackAmbiguousRegionError(OpenStackBaseException):
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
class OpenStackAuthUrlNotAllowedError(OpenStackBaseException):
|
||||
"""Exception for an auth_url rejected by the outbound URL guard"""
|
||||
|
||||
def __init__(self, file=None, original_exception=None, message=None):
|
||||
super().__init__(
|
||||
code=17012,
|
||||
file=file,
|
||||
original_exception=original_exception,
|
||||
message=message,
|
||||
)
|
||||
@@ -14,12 +14,14 @@ from prowler.config.config import (
|
||||
load_and_validate_config_file,
|
||||
)
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.network.ssrf import OutboundURLNotAllowedError, validate_outbound_url
|
||||
from prowler.lib.utils.utils import print_boxes
|
||||
from prowler.providers.common.models import Audit_Metadata, Connection
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.openstack.exceptions.exceptions import (
|
||||
OpenStackAmbiguousRegionError,
|
||||
OpenStackAuthenticationError,
|
||||
OpenStackAuthUrlNotAllowedError,
|
||||
OpenStackCloudNotFoundError,
|
||||
OpenStackConfigFileNotFoundError,
|
||||
OpenStackCredentialsError,
|
||||
@@ -454,6 +456,15 @@ class OpenstackProvider(Provider):
|
||||
message=f"Failed to load clouds.yaml configuration: {error}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_auth_url(auth_url: str) -> None:
|
||||
"""Reject an auth_url the worker must not reach; the detail stays in the log."""
|
||||
try:
|
||||
validate_outbound_url(auth_url, allowed_schemes=("http", "https"))
|
||||
except OutboundURLNotAllowedError as error:
|
||||
logger.warning(f"OpenStack auth_url rejected: {error}")
|
||||
raise OpenStackAuthUrlNotAllowedError()
|
||||
|
||||
@staticmethod
|
||||
def _create_connection(
|
||||
session: OpenStackSession,
|
||||
@@ -617,6 +628,8 @@ class OpenstackProvider(Provider):
|
||||
project_domain_name=project_domain_name,
|
||||
)
|
||||
|
||||
OpenstackProvider._validate_auth_url(session.auth_url)
|
||||
|
||||
# Validate provider_id matches project_id from config
|
||||
if provider_id and session.project_id != provider_id:
|
||||
raise OpenStackInvalidProviderIdError(
|
||||
@@ -636,6 +649,7 @@ class OpenstackProvider(Provider):
|
||||
except (
|
||||
OpenStackCredentialsError,
|
||||
OpenStackAuthenticationError,
|
||||
OpenStackAuthUrlNotAllowedError,
|
||||
OpenStackSessionError,
|
||||
OpenStackConfigFileNotFoundError,
|
||||
OpenStackCloudNotFoundError,
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,173 @@
|
||||
import ipaddress
|
||||
import socket
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from prowler.lib.network.ssrf import (
|
||||
ALLOWED_PRIVATE_NETWORKS_ENV,
|
||||
OutboundURLNotAllowedError,
|
||||
allowed_private_networks,
|
||||
extract_host,
|
||||
validate_outbound_host,
|
||||
validate_outbound_url,
|
||||
)
|
||||
|
||||
|
||||
def _resolves_to(*addresses):
|
||||
return mock.patch.object(
|
||||
socket,
|
||||
"getaddrinfo",
|
||||
return_value=[(2, 1, 6, "", (address, 0)) for address in addresses],
|
||||
)
|
||||
|
||||
|
||||
class TestValidateOutboundHost:
|
||||
@pytest.mark.parametrize(
|
||||
"address",
|
||||
[
|
||||
"127.0.0.1",
|
||||
"10.0.0.5",
|
||||
"192.168.1.1",
|
||||
"172.16.0.1",
|
||||
"169.254.169.254",
|
||||
"0.0.0.0",
|
||||
"224.0.0.1",
|
||||
"198.18.0.1",
|
||||
"203.0.113.1",
|
||||
"::1",
|
||||
"fe80::1",
|
||||
"fc00::1",
|
||||
"ff02::1",
|
||||
"2001:db8::1",
|
||||
],
|
||||
)
|
||||
def test_rejects_non_public_literals(self, address):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host(address)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"address", ["100.64.0.1", "100.100.100.200", "100.127.255.254"]
|
||||
)
|
||||
def test_rejects_shared_address_space(self, address):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host(address)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"address", ["8.8.8.8", "1.1.1.1", "140.82.121.4", "2606:4700:4700::1111"]
|
||||
)
|
||||
def test_allows_public_literals(self, address):
|
||||
validate_outbound_host(address)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"address",
|
||||
["::ffff:169.254.169.254", "2002:a00:5::", "64:ff9b::a00:5"],
|
||||
)
|
||||
def test_rejects_ipv6_embedding_a_non_public_ipv4(self, address):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host(address)
|
||||
|
||||
def test_rejects_a_hostname_resolving_to_a_private_address(self):
|
||||
with _resolves_to("10.0.0.5"):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host("registry.internal")
|
||||
|
||||
def test_rejects_a_hostname_with_one_non_public_answer(self):
|
||||
with _resolves_to("8.8.8.8", "127.0.0.1"):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host("split-horizon.example.com")
|
||||
|
||||
def test_allows_a_hostname_resolving_to_a_public_address(self):
|
||||
with _resolves_to("140.82.121.4"):
|
||||
validate_outbound_host("github.com")
|
||||
|
||||
def test_rejects_a_hostname_that_does_not_resolve(self):
|
||||
with mock.patch.object(
|
||||
socket, "getaddrinfo", side_effect=socket.gaierror("no such host")
|
||||
):
|
||||
with pytest.raises(OutboundURLNotAllowedError, match="Could not resolve"):
|
||||
validate_outbound_host("nowhere.invalid")
|
||||
|
||||
|
||||
class TestAllowedPrivateNetworks:
|
||||
def test_is_empty_when_unset(self, monkeypatch):
|
||||
monkeypatch.delenv(ALLOWED_PRIVATE_NETWORKS_ENV, raising=False)
|
||||
assert allowed_private_networks() == ()
|
||||
|
||||
@pytest.mark.parametrize("raw", ["", " ", ",", " , "])
|
||||
def test_is_empty_for_blank_values(self, monkeypatch, raw):
|
||||
monkeypatch.setenv(ALLOWED_PRIVATE_NETWORKS_ENV, raw)
|
||||
assert allowed_private_networks() == ()
|
||||
|
||||
def test_parses_addresses_and_cidrs(self, monkeypatch):
|
||||
monkeypatch.setenv(
|
||||
ALLOWED_PRIVATE_NETWORKS_ENV, "10.20.0.0/16, 192.168.65.254 ,fc00::/7"
|
||||
)
|
||||
assert allowed_private_networks() == (
|
||||
ipaddress.ip_network("10.20.0.0/16"),
|
||||
ipaddress.ip_network("192.168.65.254/32"),
|
||||
ipaddress.ip_network("fc00::/7"),
|
||||
)
|
||||
|
||||
def test_rejects_a_malformed_entry(self, monkeypatch):
|
||||
monkeypatch.setenv(ALLOWED_PRIVATE_NETWORKS_ENV, "10.20.0.0/16,not-a-network")
|
||||
with pytest.raises(OutboundURLNotAllowedError, match="Malformed entry"):
|
||||
allowed_private_networks()
|
||||
|
||||
def test_allows_an_address_inside_an_allowlisted_range(self, monkeypatch):
|
||||
monkeypatch.setenv(ALLOWED_PRIVATE_NETWORKS_ENV, "10.20.0.0/16")
|
||||
validate_outbound_host("10.20.1.5")
|
||||
|
||||
def test_still_rejects_an_address_outside_the_allowlisted_range(self, monkeypatch):
|
||||
monkeypatch.setenv(ALLOWED_PRIVATE_NETWORKS_ENV, "10.20.0.0/16")
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host("169.254.169.254")
|
||||
|
||||
def test_does_not_match_an_allowlist_entry_of_another_family(self, monkeypatch):
|
||||
monkeypatch.setenv(ALLOWED_PRIVATE_NETWORKS_ENV, "fc00::/7")
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_host("10.20.1.5")
|
||||
|
||||
|
||||
class TestExtractHost:
|
||||
@pytest.mark.parametrize(
|
||||
"url, expected",
|
||||
[
|
||||
("https://github.com/org/repo", "github.com"),
|
||||
(
|
||||
"https://user:token@github.com/org/repo", # trufflehog:ignore
|
||||
"github.com",
|
||||
),
|
||||
("https://github.com:8443/org/repo", "github.com"),
|
||||
("git@github.com:org/repo.git", "github.com"),
|
||||
("github.com:org/repo.git", "github.com"),
|
||||
("ssh://git@github.com/org/repo.git", "github.com"),
|
||||
],
|
||||
)
|
||||
def test_reads_the_host(self, url, expected):
|
||||
assert extract_host(url) == expected
|
||||
|
||||
def test_rejects_a_url_without_a_host(self):
|
||||
with pytest.raises(OutboundURLNotAllowedError, match="Could not read a host"):
|
||||
extract_host("not a url")
|
||||
|
||||
|
||||
class TestValidateOutboundURL:
|
||||
def test_rejects_a_disallowed_scheme(self):
|
||||
with pytest.raises(OutboundURLNotAllowedError, match="Disallowed URL scheme"):
|
||||
validate_outbound_url("file:///etc/passwd")
|
||||
|
||||
def test_allows_an_explicitly_permitted_scheme(self):
|
||||
with _resolves_to("140.82.121.4"):
|
||||
validate_outbound_url(
|
||||
"ssh://git@github.com/org/repo.git",
|
||||
allowed_schemes=("http", "https", "ssh", "git"),
|
||||
)
|
||||
|
||||
def test_rejects_a_non_public_host_on_an_allowed_scheme(self):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_url("http://169.254.169.254/latest/meta-data")
|
||||
|
||||
def test_rejects_shared_address_space_on_an_allowed_scheme(self):
|
||||
with pytest.raises(OutboundURLNotAllowedError):
|
||||
validate_outbound_url("http://100.100.100.200/latest/meta-data")
|
||||
@@ -16,6 +16,7 @@ from prowler.providers.common.models import Connection
|
||||
from prowler.providers.openstack.exceptions.exceptions import (
|
||||
OpenStackAmbiguousRegionError,
|
||||
OpenStackAuthenticationError,
|
||||
OpenStackAuthUrlNotAllowedError,
|
||||
OpenStackCloudNotFoundError,
|
||||
OpenStackConfigFileNotFoundError,
|
||||
OpenStackCredentialsError,
|
||||
@@ -26,6 +27,23 @@ from prowler.providers.openstack.exceptions.exceptions import (
|
||||
from prowler.providers.openstack.models import OpenStackIdentityInfo, OpenStackSession
|
||||
from prowler.providers.openstack.openstack_provider import OpenstackProvider
|
||||
|
||||
PUBLIC_IP = "8.8.8.8"
|
||||
|
||||
|
||||
def _fake_getaddrinfo(host_to_ip: dict):
|
||||
def _getaddrinfo(host, *_args, **_kwargs):
|
||||
return [(None, None, None, None, (host_to_ip.get(host, PUBLIC_IP), 0))]
|
||||
|
||||
return _getaddrinfo
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _dns_resolves_public(monkeypatch):
|
||||
"""Keep the outbound URL guard offline: every hostname resolves to a public IP."""
|
||||
monkeypatch.setattr(
|
||||
"prowler.lib.network.ssrf.socket.getaddrinfo", _fake_getaddrinfo({})
|
||||
)
|
||||
|
||||
|
||||
class TestOpenstackProvider:
|
||||
"""Test suite for OpenStack Provider initialization."""
|
||||
@@ -1620,3 +1638,126 @@ clouds:
|
||||
|
||||
assert result.is_connected is False
|
||||
assert isinstance(result.error, OpenStackInvalidProviderIdError)
|
||||
|
||||
|
||||
class TestOpenstackProviderAuthUrlGuard:
|
||||
"""Test suite for the outbound URL guard applied to auth_url in test_connection."""
|
||||
|
||||
CLOUDS_YAML = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: {auth_url}
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
region_name: RegionOne
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _test_connection(**kwargs) -> tuple[Connection, MagicMock]:
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = MagicMock()
|
||||
result = OpenstackProvider.test_connection(
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project-id",
|
||||
region_name="RegionOne",
|
||||
raise_on_exception=False,
|
||||
**kwargs,
|
||||
)
|
||||
return result, mock_connect
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"auth_url",
|
||||
[
|
||||
"https://127.0.0.1:5000/v3",
|
||||
"https://[::1]:5000/v3",
|
||||
"https://10.0.0.5:5000/v3",
|
||||
"https://172.16.0.5:5000/v3",
|
||||
"https://192.168.1.5:5000/v3",
|
||||
"http://169.254.169.254/latest/meta-data",
|
||||
"ftp://keystone.example.com:5000/v3",
|
||||
],
|
||||
)
|
||||
def test_test_connection_rejects_non_public_auth_url(self, auth_url):
|
||||
result, mock_connect = self._test_connection(auth_url=auth_url)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert isinstance(result.error, OpenStackAuthUrlNotAllowedError)
|
||||
mock_connect.assert_not_called()
|
||||
|
||||
def test_test_connection_rejects_hostname_resolving_to_private_ip(
|
||||
self, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
"prowler.lib.network.ssrf.socket.getaddrinfo",
|
||||
_fake_getaddrinfo({"keystone.example.com": "10.0.0.5"}),
|
||||
)
|
||||
|
||||
result, mock_connect = self._test_connection(
|
||||
auth_url="https://keystone.example.com:5000/v3"
|
||||
)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert isinstance(result.error, OpenStackAuthUrlNotAllowedError)
|
||||
mock_connect.assert_not_called()
|
||||
|
||||
def test_test_connection_rejection_does_not_echo_the_target(self):
|
||||
result, _ = self._test_connection(
|
||||
auth_url="https://placeholder-user:placeholder-token@10.0.0.5:5000/v3" # trufflehog:ignore
|
||||
)
|
||||
|
||||
assert isinstance(result.error, OpenStackAuthUrlNotAllowedError)
|
||||
assert result.error.original_exception is None
|
||||
for fragment in ("10.0.0.5", "placeholder-token", "non-public", "resolves"):
|
||||
assert fragment not in str(result.error)
|
||||
|
||||
def test_test_connection_rejection_raises_when_requested(self):
|
||||
with patch("prowler.providers.openstack.openstack_provider.connect"):
|
||||
with pytest.raises(OpenStackAuthUrlNotAllowedError):
|
||||
OpenstackProvider.test_connection(
|
||||
auth_url="https://127.0.0.1:5000/v3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project-id",
|
||||
region_name="RegionOne",
|
||||
raise_on_exception=True,
|
||||
)
|
||||
|
||||
def test_test_connection_allows_public_auth_url(self):
|
||||
result, mock_connect = self._test_connection(
|
||||
auth_url="https://openstack.example.com:5000/v3"
|
||||
)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
mock_connect.assert_called_once()
|
||||
|
||||
def test_test_connection_rejects_private_auth_url_from_clouds_yaml_content(
|
||||
self,
|
||||
):
|
||||
result, mock_connect = self._test_connection(
|
||||
clouds_yaml_content=self.CLOUDS_YAML.format(
|
||||
auth_url="https://127.0.0.1:5000/v3"
|
||||
),
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert isinstance(result.error, OpenStackAuthUrlNotAllowedError)
|
||||
mock_connect.assert_not_called()
|
||||
|
||||
def test_test_connection_allows_public_auth_url_from_clouds_yaml_content(self):
|
||||
result, mock_connect = self._test_connection(
|
||||
clouds_yaml_content=self.CLOUDS_YAML.format(
|
||||
auth_url="https://openstack.example.com:5000/v3"
|
||||
),
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
mock_connect.assert_called_once()
|
||||
Reference in new issue
Block a user