From 22ecb2030ccffada49f5358c176831cf184bf344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=80=20Sala?= Date: Mon, 20 Jan 2025 13:25:51 +0100 Subject: [PATCH] feat: support insights service account credentials for project update (AAP-37464) --- awx/main/tasks/jobs.py | 6 +++ awx/playbooks/action_plugins/insights.py | 68 ++++++++++++++++++++---- awx/playbooks/project_update.yml | 12 ++++- awx/settings/defaults.py | 1 + 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 509ebc7e98..9c7f56dd39 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -1279,6 +1279,7 @@ class RunProjectUpdate(BaseTask): 'local_path': os.path.basename(project_update.project.local_path), 'project_path': project_update.get_project_path(check_if_exists=False), # deprecated 'insights_url': settings.INSIGHTS_URL_BASE, + 'oidc_endpoint': settings.INSIGHTS_OIDC_ENDPOINT, 'awx_license_type': get_license().get('license_type', 'UNLICENSED'), 'awx_version': get_awx_version(), 'scm_url': scm_url, @@ -1445,6 +1446,11 @@ class RunProjectUpdate(BaseTask): ) return params + def build_credentials_list(self, project_update): + if project_update.scm_type == 'insights' and project_update.credential: + return [project_update.credential] + return [] + @task(queue=get_task_queuename) class RunInventoryUpdate(SourceControlMixin, BaseTask): diff --git a/awx/playbooks/action_plugins/insights.py b/awx/playbooks/action_plugins/insights.py index ccb36d478a..b2f8403b64 100644 --- a/awx/playbooks/action_plugins/insights.py +++ b/awx/playbooks/action_plugins/insights.py @@ -9,6 +9,8 @@ import requests from ansible.plugins.action import ActionBase +DEFAULT_OIDC_ENDPOINT = 'https://sso.redhat.com/auth/realms/redhat-external' + class ActionModule(ActionBase): def save_playbook(self, proj_path, remediation, content): @@ -34,27 +36,75 @@ class ActionModule(ActionBase): with open(file_path, 'w') as f: f.write(etag) + def _obtain_auth_token(self, oidc_endpoint, client_id, client_secret): + if oidc_endpoint.endswith('/'): + oidc_endpoint = oidc_endpoint[:-1] + main_url = oidc_endpoint + '/.well-known/openid-configuration' + response = requests.get(url=main_url, headers={'Accept': 'application/json'}) + data = {} + if response.status_code != 200: + data['failed'] = True + data['msg'] = 'Expected {} to return a status code of 200 but returned status code "{}" instead with content "{}".'.format( + main_url, response.status_code, response.content + ) + return data + + auth_url = response.json().get('token_endpoint', None) + data = { + 'grant_type': 'client_credentials', + 'scope': 'api.console', + 'client_id': client_id, + 'client_secret': client_secret, + } + response = requests.post(url=auth_url, data=data) + + if response.status_code != 200: + data['failed'] = True + data['msg'] = 'Expected {} to return a status code of 200 but returned status code "{}" instead with content "{}".'.format( + auth_url, response.status_code, response.content + ) + else: + data['token'] = response.json().get('access_token', None) + data['token_type'] = response.json().get('token_type', None) + return data + def run(self, tmp=None, task_vars=None): self._supports_check_mode = False + session = requests.Session() result = super(ActionModule, self).run(tmp, task_vars) insights_url = self._task.args.get('insights_url', None) - username = self._task.args.get('username', None) - password = self._task.args.get('password', None) proj_path = self._task.args.get('project_path', None) license = self._task.args.get('awx_license_type', None) awx_version = self._task.args.get('awx_version', None) + authentication = self._task.args.get('authentication', None) + username = self._task.args.get('username', None) + password = self._task.args.get('password', None) + client_id = self._task.args.get('client_id', None) + client_secret = self._task.args.get('client_secret', None) + oidc_endpoint = self._task.args.get('oidc_endpoint', DEFAULT_OIDC_ENDPOINT) + + session.headers.update( + { + 'Content-Type': 'application/json', + 'User-Agent': '{} {} ({})'.format('AWX' if license == 'open' else 'Red Hat Ansible Automation Platform', awx_version, license), + } + ) + + if authentication == 'service_account' or (client_id and client_secret): + data = self._obtain_auth_token(oidc_endpoint, client_id, client_secret) + if 'token' not in data: + result['failed'] = data['failed'] + result['msg'] = data['msg'] + return result + session.headers.update({'Authorization': f'{data["token_type"]} {data["token"]}'}) + elif authentication == 'basic' or (username and password): + session.auth = requests.auth.HTTPBasicAuth(username, password) - session = requests.Session() - session.auth = requests.auth.HTTPBasicAuth(username, password) - headers = { - 'Content-Type': 'application/json', - 'User-Agent': '{} {} ({})'.format('AWX' if license == 'open' else 'Red Hat Ansible Automation Platform', awx_version, license), - } url = '/api/remediations/v1/remediations' while url: - res = session.get('{}{}'.format(insights_url, url), headers=headers, timeout=120) + res = session.get('{}{}'.format(insights_url, url), timeout=120) if res.status_code != 200: result['failed'] = True diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 26a4891c14..2f4ab183c7 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -19,6 +19,11 @@ # awx_version: Current running version of the awx or tower as a string # awx_license_type: "open" for AWX; else presume Tower # gpg_pubkey: the GPG public key to use for validation, when enabled +# client_id: Red Hat service account client ID; required for the 'service_account' authentication method used against the Insights API +# client_secret: Red Hat service account client secret; required for the 'service_account' authentication method used against the Insights API +# authentication: The authentication method to use against the Insights API +# client_id and client_secret are required for the 'service_account' authentication method +# scm_username and scm_password are required for the 'basic' authentication method - hosts: localhost gather_facts: false @@ -95,11 +100,14 @@ - name: Fetch Insights Playbook(s) insights: insights_url: "{{ insights_url }}" - username: "{{ scm_username }}" - password: "{{ scm_password }}" + username: "{{ scm_username | default(omit) }}" + password: "{{ scm_password | default(omit) }}" project_path: "{{ project_path }}" awx_license_type: "{{ awx_license_type }}" awx_version: "{{ awx_version }}" + client_id: "{{ client_id | default(omit) }}" + client_secret: "{{ client_secret | default(omit) }}" + authentication: "{{ authentication | default(omit) }}" register: results - name: Save Insights Version diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6b50b7870d..e54290de2d 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -848,6 +848,7 @@ DISABLE_LOCAL_AUTH = False TOWER_URL_BASE = "https://platformhost" INSIGHTS_URL_BASE = "https://example.org" +INSIGHTS_OIDC_ENDPOINT = "https://sso.example.org/" INSIGHTS_AGENT_MIME = 'application/example' # See https://github.com/ansible/awx-facts-playbooks INSIGHTS_SYSTEM_ID_FILE = '/etc/redhat-access-insights/machine-id'