awx/awx/main/credential_plugins/github_app.py
Lila Yasin 9d9c125e47
[4.6][Backport][Feature] feat: 38589 GitHub App Authentication (#15807) (#6887)
* feat: 38589 GitHub App Authentication (#15807)

* feat: 38589 GitHub App Authentication

Allows both git@<personal-token> and x-access-token@<github-access-token> when authenticating using git.
This allows GitHub App tokens to work without interfering with existing authentication types.

---------

Co-authored-by: Jake Jackson <thedoubl3j@Jakes-MacBook-Pro.local>

* revert change made to allow UI to accept x-access-token, just use htt… (#15851)

revert change made to allow UI to accept x-access-token, just use https:// instead

* Add Github dep for new cred support if used (#15850)

* Add pygithub for new app token support

* fixed git requirements file with new
* added new github dep and relevant deps it needs

* add required licenses

* Add artifacts to satisfy license check

* Remove duplicated license

---------

Co-authored-by: Andrea Restle-Lay <arestlel@redhat.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>

* Remove deps update it came with the cherry-pick and is not needed in this version

Remove unneeded deps updates from requirements.in

Remove point to awx-plugins as it is not needed in tower

* Add a credential plugin that uses GitHub Apps to get tokens

* Add github app tests

* Ran requirements updater script

Ran black on github_app_test to fix formatting issue

Add scm_github_app to managed credentials

Ran updater script to reflect new deps

Added github app info to def build_passwords in jobs.py, cred now appears in credential types

Update ManagedCredentialType for GitHub App to match what we have in awx-plugins

Update inputs in maManagedCredentialType to github_app_inputs to communicate with awx/main/credential_plugins/github_app.py

Revert incorrect change in ManagedCredentialType, change github_app_lookup to call inputs instead of github_app_inputs

Updated namespace to github_app_lookup to agree with nomenclature used in the rest of the implementation and to resolve failing API test

Remove import pointing to awx plugins and update to point to credential_plugins

Remove references to gh_app_plugin_mod and change to github_app

Remove from awx_plugins.interfaces._temporary_private_django_api import (  # noqa: WPS436 to resolve failing test

Remove flake8 typing & typing references that do not exist in this version of Tower

Remove references in jobs.py and  __init__.py since this is an external cred type and registered it in setup.cfg instead

Remove blank line

REvise name in cfg from github_app_lookup to github_app to see if that ifxes module not found error

Revise first declaration of github_app to agree with file name to see if that resolves issue

Rename line 174 to agree with what's in config

Fix reference to github_app_lookup to github_app

Linters compliaining about the github_app in __all__ not being defined, renamed to see if that aligns them

Fix naming in test to correspond to naming of cred type

Update naming to be more specific and add blank line to setup.cfg

Remove __all__ from githubapp.py to satisfy linters

Revert formatting change since it is not needed in this repository

* Add blank line at the end of requirements.in

---------

Co-authored-by: Andrea Restle-Lay <andrearestlelay@gmail.com>
Co-authored-by: Jake Jackson <thedoubl3j@Jakes-MacBook-Pro.local>
Co-authored-by: Jake Jackson <jljacks93@gmail.com>
Co-authored-by: Andrea Restle-Lay <arestlel@redhat.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
2025-04-10 14:32:17 -04:00

177 lines
6.2 KiB
Python

"""GitHub App Installation Access Token Credential Plugin.
This module defines a credential plugin for making use of the
GitHub Apps mechanism, allowing authentication via GitHub App
installation-scoped access tokens.
Functions:
- :func:`extract_github_app_install_token`: Generates a GitHub App
Installation token.
- ``github_app_lookup``: Defines the credential plugin interface.
"""
from github import Auth as Auth, Github
from github.Consts import DEFAULT_BASE_URL as PUBLIC_GH_API_URL
from github.GithubException import (
BadAttributeException,
GithubException,
UnknownObjectException,
)
from django.utils.translation import gettext_lazy as _
from .plugin import CredentialPlugin
github_app_inputs = {
'fields': [
{
'id': 'github_api_url',
'label': _('GitHub API endpoint URL'),
'type': 'string',
'help_text': _(
'Specify the GitHub API URL here. In the case of an Enterprise: '
'https://gh.your.org/api/v3 (self-hosted) '
'or https://api.SUBDOMAIN.ghe.com (cloud)',
),
'default': 'https://api.github.com',
},
{
'id': 'app_or_client_id',
'label': _('GitHub App ID'),
'type': 'string',
'help_text': _(
'The GitHub App ID created by the GitHub Admin. '
'Example App ID: 1121547 '
'found on https://github.com/settings/apps/ '
'required for creating a JWT token for authentication.',
),
},
{
'id': 'install_id',
'label': _('GitHub App Installation ID'),
'type': 'string',
'help_text': _(
'The Installation ID from the GitHub App installation '
'generated by the GitHub Admin. '
'Example: 59980338 extracted from the installation link '
'https://github.com/settings/installations/59980338 '
'required for creating a limited GitHub app token.',
),
},
{
'id': 'private_rsa_key',
'label': _('RSA Private Key'),
'type': 'string',
'format': 'ssh_private_key',
'secret': True,
'multiline': True,
'help_text': _(
'Paste the contents of the PEM file that the GitHub Admin provided to you with the app and installation IDs.',
),
},
],
'metadata': [
{
'id': 'description',
'label': _('Description (Optional)'),
'type': 'string',
'help_text': _('To be removed after UI is updated'),
},
],
'required': ['app_or_client_id', 'install_id', 'private_rsa_key'],
}
GH_CLIENT_ID_TRAILER_LENGTH = 16
HEXADECIMAL_BASE = 16
def _is_intish(app_id_candidate):
return isinstance(app_id_candidate, int) or app_id_candidate.isdigit()
def _is_client_id(client_id_candidate):
client_id_prefix = 'Iv1.'
if not client_id_candidate.startswith(client_id_prefix):
return False
client_id_trailer = client_id_candidate[len(client_id_prefix) :]
if len(client_id_trailer) != GH_CLIENT_ID_TRAILER_LENGTH:
return False
try:
int(client_id_trailer, base=HEXADECIMAL_BASE)
except ValueError:
return False
return True
def _is_app_or_client_id(app_or_client_id_candidate):
if _is_intish(app_or_client_id_candidate):
return True
return _is_client_id(app_or_client_id_candidate)
def _assert_ids_look_acceptable(app_or_client_id, install_id):
if not _is_app_or_client_id(app_or_client_id):
raise ValueError(
'Expected GitHub App or Client ID to be an integer or a string '
f'starting with `Iv1.` followed by 16 hexadecimal digits, '
f'but got {app_or_client_id !r}',
)
if isinstance(app_or_client_id, str) and _is_client_id(app_or_client_id):
raise ValueError(
'Expected GitHub App ID must be an integer or a string '
f'with an all-digit value, but got {app_or_client_id !r}. '
'Client IDs are currently unsupported.',
)
if not _is_intish(install_id):
raise ValueError(
'Expected GitHub App Installation ID to be an integer' f' but got {install_id !r}',
)
def extract_github_app_install_token(github_api_url, app_or_client_id, private_rsa_key, install_id, **_discarded_kwargs):
"""Generate a GH App Installation access token."""
_assert_ids_look_acceptable(app_or_client_id, install_id)
auth = Auth.AppAuth(
app_id=str(app_or_client_id),
private_key=private_rsa_key,
).get_installation_auth(installation_id=int(install_id))
Github(
auth=auth,
base_url=github_api_url if github_api_url else PUBLIC_GH_API_URL,
)
doc_url = 'See https://docs.github.com/rest/reference/apps#create-an-installation-access-token-for-an-app'
app_install_context = f'app_or_client_id: {app_or_client_id}, install_id: {install_id}'
try:
return auth.token
except UnknownObjectException as github_install_not_found_exc:
raise ValueError(
f'Failed to retrieve a GitHub installation token from {github_api_url} using {app_install_context}. Is the app installed? {doc_url}.'
f'\n\n{github_install_not_found_exc}',
) from github_install_not_found_exc
except GithubException as pygithub_catchall_exc:
raise RuntimeError(
f'An unexpected error happened while talking to GitHub API @ {github_api_url} ({app_install_context}). '
f'Is the app or client ID correct? And the private RSA key? {doc_url}.'
f'\n\n{pygithub_catchall_exc}',
) from pygithub_catchall_exc
except BadAttributeException as github_broken_exc:
raise RuntimeError(
f'Broken GitHub @ {github_api_url} with {app_install_context}. It is a bug, please report it to the developers.\n\n{github_broken_exc}',
) from github_broken_exc
github_app_lookup_plugin = CredentialPlugin(
'GitHub App Installation Access Token Lookup',
inputs=github_app_inputs,
backend=extract_github_app_install_token,
)