From 6187010b54f8d0fe998d1b2e34afc558e6ce697c Mon Sep 17 00:00:00 2001 From: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:33:21 -0400 Subject: [PATCH] Add ability to update scm project using github app - Add github_app_id, github_app_installation_id and github_api_url to scm credential type - Add ability to generate github app token to clone project with git for github --- awx/main/models/credential/__init__.py | 3 +++ awx/main/tasks/jobs.py | 36 ++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index e07553e1a9b5..d9440351701f 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -645,6 +645,9 @@ def create(self): {'id': 'password', 'label': gettext_noop('Password'), 'type': 'string', 'secret': True}, {'id': 'ssh_key_data', 'label': gettext_noop('SCM Private Key'), 'type': 'string', 'format': 'ssh_private_key', 'secret': True, 'multiline': True}, {'id': 'ssh_key_unlock', 'label': gettext_noop('Private Key Passphrase'), 'type': 'string', 'secret': True}, + {'id': 'github_app_id', 'label': gettext_noop('GitHub App ID'), 'type': 'string'}, + {'id': 'github_app_installation_id', 'label': gettext_noop('GitHub App Installation ID'), 'type': 'string'}, + {'id': 'github_api_url', 'label': gettext_noop('GitHub API URL'), 'type': 'string'}, ], }, ) diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 0a9e7f59755b..08aa8f91f9e7 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -14,6 +14,8 @@ import traceback import time import urllib.parse as urlparse +import jwt +import requests # Django from django.conf import settings @@ -1153,6 +1155,30 @@ def build_private_data(self, project_update, private_data_dir): private_data['credentials'][credential] = credential.get_input('ssh_key_data', default='') return private_data + def _get_github_app_installation_access_token(self, project_update): + jwt_token = jwt.encode( + { + 'iat': int(time.time()), # Issued at time + 'exp': int(time.time()) + (10 * 60), # JWT expiration time (10 minute maximum) + 'iss': project_update.credential.get_input('github_app_id', default=''), # GitHub App's identifier + }, + project_update.credential.get_input('ssh_key_data', default=''), + algorithm='RS256', + ) + + headers = {'Authorization': f'Bearer {jwt_token}', 'Accept': 'application/vnd.github.v3+json'} + + github_api_url = project_update.credential.get_input('github_api_url', default='https://api.github.com') + installation_id = project_update.credential.get_input('github_app_installation_id', default='') + url = f'{github_api_url}/app/installations/{installation_id}/access_tokens' + response = requests.post(url, headers=headers) + + if response.status_code == 201: + access_token = response.json()['token'] + return access_token + else: + raise Exception(f"Failed to get access token: {response.status_code} {response.text}") + def build_passwords(self, project_update, runtime_passwords): """ Build a dictionary of passwords for SSH private key unlock and SCM @@ -1161,8 +1187,14 @@ def build_passwords(self, project_update, runtime_passwords): passwords = super(RunProjectUpdate, self).build_passwords(project_update, runtime_passwords) if project_update.credential: passwords['scm_key_unlock'] = project_update.credential.get_input('ssh_key_unlock', default='') - passwords['scm_username'] = project_update.credential.get_input('username', default='') - passwords['scm_password'] = project_update.credential.get_input('password', default='') + if project_update.credential.get_input('ssh_key_data', default=''): + github_installation_access_token = self._get_github_app_installation_access_token(project_update) + passwords['scm_username'] = 'x-access-token' + passwords['scm_password'] = github_installation_access_token + else: + passwords['scm_username'] = project_update.credential.get_input('username', default='') + passwords['scm_password'] = project_update.credential.get_input('password', default='') + return passwords def build_env(self, project_update, private_data_dir, private_data_files=None):