From d4e8a754edb050deb5121c1bd784005acfb53c31 Mon Sep 17 00:00:00 2001 From: Jaromir Wysoglad Date: Wed, 2 Jul 2025 15:37:47 -0400 Subject: [PATCH] Send keystone tokens to Aetos only Previously a keystone token was sent with every request, no matter what was on the receiving end. This could apparently pose a security threat. This patch changes the observabilityclient's behavior to send the token only when the receiving endpoint was discovered from the keystone endpoints and the service_type is 'prometheus' and service_name is 'aetos'. Change-Id: I39e84c20ef3ef4e399bf191da6b9c7ad31935fd5 Signed-off-by: Jaromir Wysoglad --- observabilityclient/tests/unit/test_utils.py | 32 ++++++++++++------- observabilityclient/utils/metric_utils.py | 20 ++++++++++-- ...unicating-with-aetos-21299ccc710f6473.yaml | 9 ++++++ 3 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/send-token-only-when-communicating-with-aetos-21299ccc710f6473.yaml diff --git a/observabilityclient/tests/unit/test_utils.py b/observabilityclient/tests/unit/test_utils.py index c779e54..950b7d2 100644 --- a/observabilityclient/tests/unit/test_utils.py +++ b/observabilityclient/tests/unit/test_utils.py @@ -16,6 +16,7 @@ import os from unittest import mock from keystoneauth1 import adapter +from keystoneauth1 import discover from keystoneauth1 import session import testtools @@ -50,6 +51,11 @@ class GetPrometheusClientTest(testtools.TestCase): super(GetPrometheusClientTest, self).setUp() config_data = 'host: "somehost"\nport: "1234"' self.config_file = mock.mock_open(read_data=config_data)("name", 'r') + self.prom_endpoint_data = discover.EndpointData( + catalog_url="http://localhost:1234/prometheus", + service_type="prometheus", + service_name="aetos" + ) def test_get_prometheus_client_from_file(self): with mock.patch.object(metric_utils, 'get_config_file', @@ -102,13 +108,12 @@ class GetPrometheusClientTest(testtools.TestCase): metric_utils.get_prometheus_client) def test_get_prometheus_client_from_keystone_http(self): - prometheus_endpoint = "http://localhost:1234/prometheus" keystone_session = session.Session() with mock.patch.dict(os.environ, {}), \ mock.patch.object(metric_utils, 'get_config_file', return_value=None), \ - mock.patch.object(adapter.Adapter, 'get_endpoint', - return_value=prometheus_endpoint), \ + mock.patch.object(adapter.Adapter, 'get_endpoint_data', + return_value=self.prom_endpoint_data), \ mock.patch.object(prometheus_client.PrometheusAPIClient, "__init__", return_value=None) as init_m, \ mock.patch.object(prometheus_client.PrometheusAPIClient, @@ -120,13 +125,14 @@ class GetPrometheusClientTest(testtools.TestCase): ca_m.assert_not_called() def test_get_prometheus_client_from_keystone_https(self): - prometheus_endpoint = "https://localhost:1234/prometheus" + self.prom_endpoint_data.catalog_url = \ + "https://localhost:1234/prometheus" keystone_session = session.Session() with mock.patch.dict(os.environ, {}), \ mock.patch.object(metric_utils, 'get_config_file', return_value=None), \ - mock.patch.object(adapter.Adapter, 'get_endpoint', - return_value=prometheus_endpoint), \ + mock.patch.object(adapter.Adapter, 'get_endpoint_data', + return_value=self.prom_endpoint_data), \ mock.patch.object(prometheus_client.PrometheusAPIClient, "__init__", return_value=None) as init_m, \ mock.patch.object(prometheus_client.PrometheusAPIClient, @@ -138,15 +144,16 @@ class GetPrometheusClientTest(testtools.TestCase): ca_m.assert_called_with(True) def test_get_prometheus_client_from_keystone_custom_ca(self): - prometheus_endpoint = "https://localhost:1234/prometheus" + self.prom_endpoint_data.catalog_url = \ + "https://localhost:1234/prometheus" keystone_session = session.Session() config_data = 'ca_cert: "ca/path"' config_file = mock.mock_open(read_data=config_data)("name", 'r') with mock.patch.dict(os.environ, {}), \ mock.patch.object(metric_utils, 'get_config_file', return_value=config_file), \ - mock.patch.object(adapter.Adapter, 'get_endpoint', - return_value=prometheus_endpoint), \ + mock.patch.object(adapter.Adapter, 'get_endpoint_data', + return_value=self.prom_endpoint_data), \ mock.patch.object(prometheus_client.PrometheusAPIClient, "__init__", return_value=None) as init_m, \ mock.patch.object(prometheus_client.PrometheusAPIClient, @@ -185,13 +192,14 @@ class GetPrometheusClientTest(testtools.TestCase): ) def test_get_prometheus_client_from_keystone_ipv6(self): - prometheus_endpoint = "http://[2607:5300:201:2000::654]:80/prometheus" + self.prom_endpoint_data.catalog_url = \ + "http://[2607:5300:201:2000::654]:80/prometheus" keystone_session = session.Session() with mock.patch.dict(os.environ, {}), \ mock.patch.object(metric_utils, 'get_config_file', return_value=None), \ - mock.patch.object(adapter.Adapter, 'get_endpoint', - return_value=prometheus_endpoint), \ + mock.patch.object(adapter.Adapter, 'get_endpoint_data', + return_value=self.prom_endpoint_data), \ mock.patch.object(prometheus_client.PrometheusAPIClient, "__init__", return_value=None) as init_m, \ mock.patch.object(prometheus_client.PrometheusAPIClient, diff --git a/observabilityclient/utils/metric_utils.py b/observabilityclient/utils/metric_utils.py index 3d06efd..b8267f5 100644 --- a/observabilityclient/utils/metric_utils.py +++ b/observabilityclient/utils/metric_utils.py @@ -54,6 +54,8 @@ def get_prometheus_client(session=None, adapter_options={}): port = None ca_cert = None root_path = "" + is_aetos = False + conf_file = get_config_file() if conf_file is not None: conf = yaml.safe_load(conf_file) @@ -71,8 +73,8 @@ def get_prometheus_client(session=None, adapter_options={}): try: endpoint = adapter.Adapter( session=session, **adapter_options - ).get_endpoint() - parsed_url = parse.urlparse(endpoint) + ).get_endpoint_data() + parsed_url = parse.urlparse(endpoint.catalog_url) host = parsed_url.hostname port = parsed_url.port if parsed_url.port is not None else 80 root_path = parsed_url.path.strip('/') @@ -82,6 +84,11 @@ def get_prometheus_client(session=None, adapter_options={}): # so that a custom certificate can be set in the config # file, while the endpoint is retrieved from keystone. ca_cert = True + if (endpoint.service_type == "prometheus" and + endpoint.service_name == "aetos"): + # We know this is Aetos and we can include keystone tokens + # when sending requests to it. + is_aetos = True except keystone_exception.EndpointNotFound: # NOTE(jwysogla): Don't do anything here. It's still possible # to get the correct endpoint configuration from the env vars. @@ -104,7 +111,14 @@ def get_prometheus_client(session=None, adapter_options={}): "port configuration and endpoint for service" "prometheus not found.") escaped_host = netutils.escape_ipv6(host) - client = PrometheusAPIClient(f"{escaped_host}:{port}", session, root_path) + if is_aetos: + client = PrometheusAPIClient( + f"{escaped_host}:{port}", session, root_path + ) + else: + client = PrometheusAPIClient( + f"{escaped_host}:{port}", None, root_path + ) if ca_cert is not None: client.set_ca_cert(ca_cert) return client diff --git a/releasenotes/notes/send-token-only-when-communicating-with-aetos-21299ccc710f6473.yaml b/releasenotes/notes/send-token-only-when-communicating-with-aetos-21299ccc710f6473.yaml new file mode 100644 index 0000000..3bce25e --- /dev/null +++ b/releasenotes/notes/send-token-only-when-communicating-with-aetos-21299ccc710f6473.yaml @@ -0,0 +1,9 @@ +--- +security: + - | + Modified the observabilityclient to send keystone token with each request + only when communicating with Aetos, instead of sending it everytime, which + could pose a security risk. This also restricts configuration + options for connecting to Aetos to keystone endpoints only. Configuration + for Prometheus is unchanged and can still be done through keystone + endpoint, through a configuration file or through environment variables.