diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index e07553e1a9b5..2f9aca23c5a5 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -634,6 +634,28 @@ def create(self): }, ) +ManagedCredentialType( + namespace='scm_github_app', + kind='scm', + name=gettext_noop('GitHub App'), + managed=True, + inputs={ + 'fields': [ + {'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': 'ssh_key_data', + 'label': gettext_noop('GitHub App Private Key'), + 'type': 'string', + 'format': 'ssh_private_key', + 'secret': True, + 'multiline': True, + }, + {'id': 'github_api_url', 'label': gettext_noop('GitHub API URL'), 'type': 'string'}, + ], + }, +) + ManagedCredentialType( namespace='scm', kind='scm', diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 0a9e7f59755b..9a93250a8fef 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, credential): + jwt_token = jwt.encode( + { + 'iat': int(time.time()), # Issued at time + 'exp': int(time.time()) + (10 * 60), # JWT expiration time (10 minute maximum) + 'iss': credential.get_input('github_app_id', default=''), # GitHub App's identifier + }, + credential.get_input('ssh_key_data', default=''), + algorithm='RS256', + ) + + headers = {'Authorization': f'Bearer {jwt_token}', 'Accept': 'application/vnd.github.v3+json'} + + github_api_url = credential.get_input('github_api_url', default='https://api.github.com') + installation_id = 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 @@ -1160,9 +1186,15 @@ 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.credential_type.namespace == 'github_app': + passwords['scm_username'] = 'x-access-token' + passwords['scm_password'] = self._get_github_app_installation_access_token(project_update.credential) + else: + passwords['scm_key_unlock'] = project_update.credential.get_input('ssh_key_unlock', default='') + passwords['scm_key_data'] = project_update.credential.get_input('ssh_key_data', default='') + 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): diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 97cf2beb2d81..83d6cb5685c1 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -106,6 +106,7 @@ def test_default_cred_types(): 'thycotic_tss', 'vault', 'vmware', + 'scm_github_app', ] ) diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 6c460e3236ed..b370d6984016 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1595,6 +1595,10 @@ def test_ssh_key_auth(self, project_update, scm_type, mock_me): expect_passwords = task.create_expect_passwords_data_struct(password_prompts, passwords) assert 'bob' in expect_passwords.values() + def test_github_app_auth(self, project_update, mock_me): + # TODO: Implement me + pass + def test_awx_task_env(self, project_update, settings, private_data_dir, scm_type, execution_environment, mock_me): project_update.execution_environment = execution_environment settings.AWX_TASK_ENV = {'FOO': 'BAR'}