feat(github): handle separately path and content of github app key

This commit is contained in:
Daniel Barranquero
2025-10-02 19:13:17 +02:00
parent efbf641bb3
commit 916eb0ff23
5 changed files with 176 additions and 24 deletions
+1
View File
@@ -242,6 +242,7 @@ class Provider(ABC):
provider_class(
personal_access_token=arguments.personal_access_token,
oauth_app_token=arguments.oauth_app_token,
github_app_key=arguments.github_app_key,
github_app_key_path=arguments.github_app_key_path,
github_app_id=arguments.github_app_id,
mutelist_path=arguments.mutelist_file,
+59 -20
View File
@@ -102,6 +102,7 @@ class GithubProvider(Provider):
# Authentication credentials
personal_access_token: str = "",
oauth_app_token: str = "",
github_app_key: str = "",
github_app_key_path: str = "",
github_app_key_content: str = "",
github_app_id: int = 0,
@@ -120,8 +121,9 @@ class GithubProvider(Provider):
Args:
personal_access_token (str): GitHub personal access token.
oauth_app_token (str): GitHub OAuth App token.
github_app_key (str): GitHub App key content.
github_app_key_path (str): Path to GitHub App private key file.
github_app_key_content (str): GitHub App private key content.
github_app_key_content (str): GitHub App private key content (legacy parameter).
github_app_id (int): GitHub App ID.
config_path (str): Path to the audit configuration file.
config_content (dict): Audit configuration content.
@@ -147,6 +149,7 @@ class GithubProvider(Provider):
personal_access_token,
oauth_app_token,
github_app_id,
github_app_key,
github_app_key_path,
github_app_key_content,
)
@@ -156,7 +159,9 @@ class GithubProvider(Provider):
self._auth_method = "Personal Access Token"
elif oauth_app_token:
self._auth_method = "OAuth App Token"
elif github_app_id and (github_app_key_path or github_app_key_content):
elif github_app_id and (
github_app_key or github_app_key_path or github_app_key_content
):
self._auth_method = "GitHub App Token"
elif environ.get("GITHUB_PERSONAL_ACCESS_TOKEN", ""):
self._auth_method = "Environment Variable for Personal Access Token"
@@ -250,6 +255,7 @@ class GithubProvider(Provider):
personal_access_token: str = None,
oauth_app_token: str = None,
github_app_id: int = 0,
github_app_key: str = None,
github_app_key_path: str = None,
github_app_key_content: str = None,
) -> GithubSession:
@@ -260,8 +266,9 @@ class GithubProvider(Provider):
personal_access_token (str): GitHub personal access token.
oauth_app_token (str): GitHub OAuth App token.
github_app_id (int): GitHub App ID.
github_app_key (str): GitHub App key content.
github_app_key_path (str): Path to GitHub App private key file.
github_app_key_content (str): GitHub App private key content.
github_app_key_content (str): GitHub App private key content (legacy parameter).
Returns:
GithubSession: Authenticated session token for API requests.
"""
@@ -278,11 +285,35 @@ class GithubProvider(Provider):
elif oauth_app_token:
session_token = oauth_app_token
elif github_app_id and (github_app_key_path or github_app_key_content):
elif github_app_id and (
github_app_key or github_app_key_path or github_app_key_content
):
app_id = github_app_id
if github_app_key_path:
with open(github_app_key_path, "r") as rsa_key:
app_key = rsa_key.read()
try:
with open(github_app_key_path, "r") as rsa_key:
app_key = rsa_key.read()
except OSError as e:
if e.errno == 63:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="--github-app-key-path expects a file path, not key content. Use --github-app-key for key content instead.",
)
else:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message=f"Could not read GitHub App key file '{github_app_key_path}': {e.message}",
)
elif github_app_key:
if github_app_key.startswith("-----BEGIN"):
app_key = format_rsa_key(github_app_key)
elif os.path.isfile(github_app_key):
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="--github-app-key expects key content, not a file path. Use --github-app-key-path for file paths instead.",
)
else:
app_key = format_rsa_key(github_app_key)
else:
app_key = format_rsa_key(github_app_key_content)
@@ -303,22 +334,27 @@ class GithubProvider(Provider):
if not session_token:
# APP
logger.info(
"Looking for GITHUB_APP_ID and GITHUB_APP_KEY environment variables as user has not provided any token...."
"Looking for GitHub App environment variables as user has not provided any token...."
)
app_id = environ.get("GITHUB_APP_ID", "")
env_key = environ.get("GITHUB_APP_KEY", "")
if app_id and env_key:
if env_key.startswith("-----BEGIN"):
app_key = format_rsa_key(env_key)
elif os.path.isfile(env_key):
with open(env_key, "r") as rsa_key:
app_key = rsa_key.read()
else:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="GITHUB_APP_KEY must contain either RSA key content (starting with -----BEGIN) or a valid file path.",
)
app_key_path = environ.get("GITHUB_APP_KEY_PATH", "")
if app_key_path:
with open(app_key_path, "r") as rsa_key:
app_key = rsa_key.read()
else:
env_key = environ.get("GITHUB_APP_KEY", "")
if env_key:
if env_key.startswith("-----BEGIN"):
app_key = format_rsa_key(env_key)
elif os.path.isfile(env_key):
with open(env_key, "r") as rsa_key:
app_key = rsa_key.read()
else:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="GITHUB_APP_KEY must contain either RSA key content (starting with -----BEGIN) or a valid file path.",
)
if not session_token and not (app_id and app_key):
raise GithubEnvironmentVariableError(
@@ -528,6 +564,7 @@ class GithubProvider(Provider):
def test_connection(
personal_access_token: str = "",
oauth_app_token: str = "",
github_app_key: str = "",
github_app_key_path: str = "",
github_app_key_content: str = "",
github_app_id: int = 0,
@@ -541,8 +578,9 @@ class GithubProvider(Provider):
Args:
personal_access_token (str): GitHub personal access token.
oauth_app_token (str): GitHub OAuth App token.
github_app_key (str): GitHub App key content.
github_app_key_path (str): Path to GitHub App private key file.
github_app_key_content (str): GitHub App private key content.
github_app_key_content (str): GitHub App private key content (legacy parameter).
github_app_id (int): GitHub App ID.
raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails.
provider_id (str): The provider ID, in this case it's the GitHub organization/username.
@@ -573,6 +611,7 @@ class GithubProvider(Provider):
personal_access_token=personal_access_token,
oauth_app_token=oauth_app_token,
github_app_id=github_app_id,
github_app_key=github_app_key,
github_app_key_path=github_app_key_path,
github_app_key_content=github_app_key_content,
)
@@ -30,9 +30,15 @@ def init_parser(self):
metavar="GITHUB_APP_ID",
)
github_auth_subparser.add_argument(
"--github-app-key-path",
"--github-app-key",
nargs="?",
help="GitHub App Key content (PEM format) to log in against GitHub",
default=None,
metavar="GITHUB_APP_KEY_CONTENT",
)
github_auth_subparser.add_argument(
"--github-app-key-path",
nargs="?",
help="Path to GitHub App private key file",
default=None,
metavar="GITHUB_APP_KEY_PATH",
@@ -55,3 +61,31 @@ def init_parser(self):
default=None,
metavar="ORGANIZATION",
)
def validate_arguments(arguments) -> tuple[bool, str]:
"""
Validate GitHub provider arguments
Args:
arguments: Parsed command line arguments
Returns:
tuple[bool, str]: (is_valid, error_message)
"""
if arguments.github_app_key and arguments.github_app_key_path:
return (
False,
"Cannot specify both --github-app-key and --github-app-key-path simultaneously",
)
if arguments.github_app_id and not (
arguments.github_app_key or arguments.github_app_key_path
):
return (
False,
"GitHub App ID requires either --github-app-key or --github-app-key-path",
)
return True, ""
@@ -217,7 +217,7 @@ class TestGitHubProvider:
),
):
connection = GithubProvider.test_connection(
github_app_id=APP_ID, github_app_key_path=APP_KEY
github_app_id=APP_ID, github_app_key_content=APP_KEY
)
assert isinstance(connection, Connection)
@@ -61,7 +61,7 @@ class Test_GitHubArguments:
arguments.init_parser(mock_github_args)
# Verify authentication arguments were added
assert self.mock_auth_group.add_argument.call_count == 4
assert self.mock_auth_group.add_argument.call_count == 5
# Check that all authentication arguments are present
calls = self.mock_auth_group.add_argument.call_args_list
@@ -76,8 +76,8 @@ class Test_GitHubArguments:
assert "--personal-access-token" in auth_args
assert "--oauth-app-token" in auth_args
assert "--github-app-id" in auth_args
assert "--github-app-key-path" in auth_args
assert "--github-app-key" in auth_args
assert "--github-app-key-path" in auth_args
def test_init_parser_adds_scoping_arguments(self):
"""Test that init_parser adds all scoping arguments"""
@@ -309,3 +309,81 @@ class Test_GitHubArguments_Integration:
assert args.personal_access_token == "test-token"
assert args.repository == []
assert args.organization == []
class Test_GitHubArguments_Validation:
def test_validate_arguments_both_key_methods_provided(self):
"""Test validation fails when both key methods are provided"""
from argparse import Namespace
args = Namespace()
args.github_app_key = "key-content"
args.github_app_key_path = "/path/to/key.pem"
args.github_app_id = "12345"
is_valid, error_msg = arguments.validate_arguments(args)
assert not is_valid
assert (
"Cannot specify both --github-app-key and --github-app-key-path simultaneously"
in error_msg
)
def test_validate_arguments_app_id_without_key(self):
"""Test validation fails when app ID is provided without any key"""
from argparse import Namespace
args = Namespace()
args.github_app_key = None
args.github_app_key_path = None
args.github_app_id = "12345"
is_valid, error_msg = arguments.validate_arguments(args)
assert not is_valid
assert (
"GitHub App ID requires either --github-app-key or --github-app-key-path"
in error_msg
)
def test_validate_arguments_valid_key_content(self):
"""Test validation passes with valid key content"""
from argparse import Namespace
args = Namespace()
args.github_app_key = "-----BEGIN RSA PRIVATE KEY-----"
args.github_app_key_path = None
args.github_app_id = "12345"
is_valid, error_msg = arguments.validate_arguments(args)
assert is_valid
assert error_msg == ""
def test_validate_arguments_valid_key_path(self):
"""Test validation passes with valid key path"""
from argparse import Namespace
args = Namespace()
args.github_app_key = None
args.github_app_key_path = "/path/to/key.pem"
args.github_app_id = "12345"
is_valid, error_msg = arguments.validate_arguments(args)
assert is_valid
assert error_msg == ""
def test_validate_arguments_no_app_auth(self):
"""Test validation passes when no app authentication is used"""
from argparse import Namespace
args = Namespace()
args.github_app_key = None
args.github_app_key_path = None
args.github_app_id = None
is_valid, error_msg = arguments.validate_arguments(args)
assert is_valid
assert error_msg == ""