diff --git a/module_build_service/common/config.py b/module_build_service/common/config.py index 4ad3ad0c..8f8d356d 100644 --- a/module_build_service/common/config.py +++ b/module_build_service/common/config.py @@ -662,6 +662,21 @@ class Config(object): "stream has been released. If it has, the stream may be modified automatically " "to use a different support stream.", }, + "product_pages_token_endpoint": { + "type": str, + "default": "", + "desc": "The endpoint to request a token to authenticate with Product Pages.", + }, + "product_pages_oidc_client_id": { + "type": str, + "default": "", + "desc": "Client ID to authenticate with the token endpoint.", + }, + "product_pages_client_secret": { + "type": str, + "default": "", + "desc": "Client secret to authenticate with the token endpoint.", + }, "product_pages_module_streams": { "type": dict, "default": {}, diff --git a/module_build_service/web/submit.py b/module_build_service/web/submit.py index 9becdf76..5d639f0c 100644 --- a/module_build_service/web/submit.py +++ b/module_build_service/web/submit.py @@ -387,6 +387,29 @@ def resolve_base_module_virtual_streams(db_session, name, streams): return new_streams +def _product_pages_oidc_auth(): + """ + Obtain an OIDC access token to authenticate with Product Pages + """ + + try: + token_response = requests.post( + conf.product_pages_token_endpoint, + { + 'grant_type': 'client_credentials', + 'client_id': conf.product_pages_oidc_client_id, + 'client_secret': conf.product_pages_client_secret, + }, + ) + token_response.raise_for_status() + except requests.exceptions.RequestException as e: + log.error(f"Product Pages authentication failed: {e}") + raise RuntimeError("Failed to authenticate with Product Pages.") + + access_token = token_response.json()['access_token'] + return {'Authorization': f'Bearer {access_token}'} + + def _process_support_streams(db_session, mmd, params): """ Check if any buildrequired base modules require a support stream suffix. @@ -407,6 +430,17 @@ def _process_support_streams(db_session, mmd, params): elif not conf.product_pages_module_streams: log.debug(config_msg, "product_pages_module_streams") return + elif not conf.product_pages_token_endpoint: + log.debug(config_msg, "product_pages_token_endpoint") + return + elif not conf.product_pages_oidc_client_id: + log.debug(config_msg, "product_pages_oidc_client_id") + return + elif not conf.product_pages_client_secret: + log.debug(config_msg, "product_pages_client_secret") + return + + auth_header = _product_pages_oidc_auth() buildrequire_overrides = params.get("buildrequire_overrides", {}) @@ -427,7 +461,7 @@ def _process_support_streams(db_session, mmd, params): conf.product_pages_url.rstrip("/"), pp_release) try: - pp_rv = requests.get(schedule_url, timeout=15) + pp_rv = requests.get(schedule_url, timeout=15, headers=auth_header) # raise exception if we receive 404 pp_rv.raise_for_status() pp_json = pp_rv.json() @@ -459,7 +493,7 @@ def _process_support_streams(db_session, mmd, params): Check if the stream has been released. Return True if it has. """ try: - pp_rv = requests.get(url, timeout=15) + pp_rv = requests.get(url, timeout=15, headers=auth_header) pp_json = pp_rv.json() # Catch requests failures and JSON parsing errors except (requests.exceptions.RequestException, ValueError): diff --git a/tests/test_web/test_views.py b/tests/test_web/test_views.py index dc40a999..1023811a 100644 --- a/tests/test_web/test_views.py +++ b/tests/test_web/test_views.py @@ -2195,12 +2195,25 @@ class TestSubmitBuild: "module_build_service.common.config.Config.product_pages_schedule_task_name", new_callable=PropertyMock, ) + @patch( + "module_build_service.common.config.Config.product_pages_token_endpoint", + new_callable=PropertyMock, + ) + @patch( + "module_build_service.common.config.Config.product_pages_oidc_client_id", + new_callable=PropertyMock, + ) + @patch( + "module_build_service.common.config.Config.product_pages_client_secret", + new_callable=PropertyMock, + ) @patch("requests.get") @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.common.scm.SCM") + @patch("module_build_service.web.submit._product_pages_oidc_auth", return_value="authstring") def test_submit_build_automatic_z_stream_detection( - self, mocked_scm, mocked_get_user, mock_get, mock_pp_sched, mock_pp_streams, - mock_pp_url, + self, mocked_oidc_auth, mocked_scm, mocked_get_user, mock_get, mock_pp_secret, + mock_pp_id, mock_pp_endpoint, mock_pp_sched, mock_pp_streams, mock_pp_url, mock_datetime, pp_url, pp_streams, pp_sched, get_rv, br_stream, br_override, expected_stream, utcnow, ): @@ -2208,6 +2221,10 @@ class TestSubmitBuild: mock_pp_url.return_value = pp_url mock_pp_streams.return_value = pp_streams mock_pp_sched.return_value = pp_sched + # Configure the OIDC auth + mock_pp_endpoint.return_value = "endpoint" + mock_pp_id.return_value = "client_id" + mock_pp_secret.return_value = "secret" # Mock the Product Pages query mock_get.return_value.json.return_value = get_rv # Mock the date @@ -2255,7 +2272,8 @@ class TestSubmitBuild: expected_url = \ "{}api/v7/releases/{}/schedule-tasks/?fields=name,date_finish".format( pp_url, pp_release) - mock_get.assert_called_once_with(expected_url, timeout=15) + mock_get.assert_called_once_with(expected_url, timeout=15, headers="authstring") + mocked_oidc_auth.assert_called() else: mock_get.assert_not_called()