Files
welcome-to-codevalet-as-a-c…/scripts/git-credential-forge.py
FanaticPythoner (Nathan Trudeau) e27c8a2bd6 Rewrite onboarding prose to a neutral voice
2026-04-26 12:26:19 -04:00

223 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Git credential helper backed by the codevalet OAuth auth file.
Git invokes credential helpers with one of three verbs on argv[1]:
get : read key=value fields from stdin; emit credentials on stdout
store : persist credentials (no-op here; tokens are OAuth-owned)
erase : forget credentials (no-op here; ``just logout`` owns that)
On every ``get`` the helper:
1. Reads the auth file (same path the gateway uses:
``~/.forge-stack-devpi-gateway-gitea/client-auth.json`` by default,
or whatever ``FSDGG_AUTH_STORE_PATH`` / ``FSDGG_RUNTIME_DIR`` point
at). If the file is missing, the helper passes git's input through
unchanged so the normal prompt chain continues.
2. Checks that the git request host matches the host recorded in
``_forge_gitea_base_url`` (or ``FORGE_GITEA_URL``). If not, passes
through: this prevents OAuth token disclosure to unrelated
hosts even if git mis-scopes its lookup.
3. If ``gitea_access_token`` is live, emits
``username=<stored-user>`` and ``password=<gitea_access_token>``.
4. If the token is expired and a refresh token is present, runs the
OAuth refresh flow (``forge_auth.run_refresh``) and retries once.
Refresh failures are surfaced by exiting non-zero; git then falls
through to the user's configured helper chain (normally a prompt).
Security notes
--------------
* The helper never writes to stdout except the credential key=value
block. Logs go to stderr.
* On OAuth refresh failure the process exits **non-zero** rather than silently
returning stale credentials.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from urllib.parse import urlsplit
def _load_forge_auth_module():
"""Import ``forge_auth`` from the same directory as this script.
This works both when the helper is invoked directly from
``scripts/`` (development) and after installation under
``~/.local/bin`` (because the installer copies ``forge_auth.py``
alongside ``git-credential-forge``).
"""
here = Path(__file__).resolve().parent
if str(here) not in sys.path:
sys.path.insert(0, str(here))
import forge_auth # noqa: E402
return forge_auth
forge_auth = _load_forge_auth_module()
# --------------------------------------------------------------------
# git credential protocol I/O
# --------------------------------------------------------------------
def read_git_fields(stream) -> dict[str, str]:
fields: dict[str, str] = {}
for line in stream:
line = line.rstrip("\n")
if not line:
break
if "=" not in line:
raise ValueError(f"unexpected git credential input line: {line!r}")
key, _, value = line.partition("=")
fields[key] = value
return fields
def emit(fields: dict[str, str]) -> None:
for key, value in fields.items():
sys.stdout.write(f"{key}={value}\n")
# --------------------------------------------------------------------
# Host matching
# --------------------------------------------------------------------
def _configured_host() -> tuple[str, str, int | None] | None:
"""Return (scheme, host, port) of the host this helper is allowed
to produce credentials for.
Priority:
1. The ``_forge_gitea_base_url`` field inside the stored auth
file: the authenticated host.
2. The ``FORGE_GITEA_URL`` env var (pre-login override).
Returns None if neither is set; the helper then passes through.
"""
store = forge_auth.auth_store_path()
state = forge_auth.AuthFile.read(store)
stored_url = str(state.raw.get("_forge_gitea_base_url") or "").strip()
if stored_url:
parsed = urlsplit(stored_url)
return parsed.scheme.lower(), (parsed.hostname or "").lower(), parsed.port
env_url = os.environ.get("FORGE_GITEA_URL", "").strip()
if env_url:
parsed = urlsplit(env_url)
return parsed.scheme.lower(), (parsed.hostname or "").lower(), parsed.port
return None
def _request_matches(
fields: dict[str, str], configured: tuple[str, str, int | None]
) -> bool:
scheme, host, port = configured
git_scheme = fields.get("protocol", "").lower()
git_host = fields.get("host", "").lower()
# git passes host:port as "host" when the URL carried a port.
if ":" in git_host:
host_part, _, port_str = git_host.partition(":")
try:
git_port = int(port_str)
except ValueError:
return False
git_host = host_part
else:
git_port = None
if git_scheme != scheme:
return False
if git_host != host:
return False
if port is not None and git_port != port:
return False
if port is None and git_port is not None:
# Stored URL had no explicit port (default 443) but request does;
# only match the default-HTTPS case.
if scheme == "https" and git_port != 443:
return False
if scheme == "http" and git_port != 80:
return False
return True
# --------------------------------------------------------------------
# Commands
# --------------------------------------------------------------------
def cmd_get(fields: dict[str, str]) -> int:
configured = _configured_host()
if configured is None:
emit(fields) # pass-through: helper scope is undefined
return 0
if not _request_matches(fields, configured):
emit(fields) # pass-through: request is for a different host
return 0
state = forge_auth.AuthFile.read(forge_auth.auth_store_path())
# Fast path: stored access token is still live.
if state.has_live_gitea_token():
_emit_credentials(fields, state)
return 0
# Slow path: try to refresh.
try:
config = forge_auth.ForgeAuthConfig.from_env()
except forge_auth.AuthError:
forge_auth.cli_warn(
"stored token is expired and FORGE_GITEA_URL / "
"FSDGG_CLI_CLIENT_ID are not set in the environment; "
"cannot refresh. Run 'just login' to re-authenticate."
)
emit(fields)
return 0
try:
refreshed = forge_auth.run_refresh(config, must_refresh=True)
except forge_auth.AuthError as exc:
forge_auth.cli_warn(
f"token refresh failed: {exc}. "
f"Run 'just login' to re-authenticate."
)
emit(fields)
return 0
_emit_credentials(fields, refreshed)
return 0
def _emit_credentials(fields: dict[str, str], state: forge_auth.AuthFile) -> None:
token = state.gitea_access_token
if not token:
emit(fields)
return
out = dict(fields)
out["username"] = state.username or fields.get("username") or "oauth"
out["password"] = token
emit(out)
def cmd_consume_noop(stream) -> int:
for _ in stream:
pass
return 0
def main(argv: list[str]) -> int:
if len(argv) < 2:
print("usage: git-credential-forge <get|store|erase>", file=sys.stderr)
return 2
action = argv[1]
if action == "get":
fields = read_git_fields(sys.stdin)
return cmd_get(fields)
if action in ("store", "erase"):
return cmd_consume_noop(sys.stdin)
print(f"git-credential-forge: unsupported action: {action}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))