-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ability to update scm project using github app #15472
base: devel
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you trying to surface this into a UI field? This is usually discoverable automatically. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. automatic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So ideally, there should be no UI inputs at all. Log into https://pre-commit.ci or Codecov / RTD to see their UX for working with repos accessible through GH app installations. Whenever the app is installed, you already get an event with all the information needed via webhooks. |
||
{'id': 'github_api_url', 'label': gettext_noop('GitHub API URL'), 'type': 'string'}, | ||
], | ||
}, | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GH App should be global for the entire AWX. The whole idea is that the users don't need to set up and maintain multiple apps. They just need to click “Install” on GH UI. |
||
}, | ||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This as well should be a deployment-global value rather than bound to a credential. |
||
installation_id = project_update.credential.get_input('github_app_installation_id', default='') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be discovered from the GitHub repository URL and querying https://docs.github.com/en/rest/apps/installations?apiVersion=2022-11-28#list-app-installations-accessible-to-the-user-access-token, and stored somewhere in the DB, not surfaced to the users. |
||
url = f'{github_api_url}/app/installations/{installation_id}/access_tokens' | ||
response = requests.post(url, headers=headers) | ||
|
||
if response.status_code == 201: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you invert it to |
||
access_token = response.json()['token'] | ||
return access_token | ||
else: | ||
raise Exception(f"Failed to get access token: {response.status_code} {response.text}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any chance not to use an unspecified exception? |
||
|
||
def build_passwords(self, project_update, runtime_passwords): | ||
""" | ||
Build a dictionary of passwords for SSH private key unlock and SCM | ||
|
@@ -1161,8 +1187,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='') | ||
passwords['scm_key_data'] = project_update.credential.get_input('ssh_key_data', default='') | ||
if project_update.credential.get_input('github_app_id', 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): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this also surfaced to the end-users? Why? It should be a platform-global value, it's not directly a credential.