fix: fix: stop baking credentials into git remote URLs — use clean URLs + existing credential helper everywhere (#604)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/smoke-init Pipeline was successful

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-10 17:04:10 +00:00
parent d076528193
commit 5c4ea7373a
10 changed files with 336 additions and 72 deletions

View file

@ -7,7 +7,6 @@
# Globals expected:
# FORGE_URL - Forge instance URL (e.g. http://localhost:3000)
# FORGE_TOKEN - API token for Forge operations (used for API verification)
# FORGE_PASS - Bot password for git HTTP push (#361: tokens rejected by Forgejo 11.x)
# FACTORY_ROOT - Root of the disinto factory
# PRIMARY_BRANCH - Primary branch name (e.g. main)
#
@ -21,7 +20,6 @@ set -euo pipefail
_assert_forge_push_globals() {
local missing=()
[ -z "${FORGE_URL:-}" ] && missing+=("FORGE_URL")
[ -z "${FORGE_PASS:-}" ] && missing+=("FORGE_PASS")
[ -z "${FORGE_TOKEN:-}" ] && missing+=("FORGE_TOKEN")
[ -z "${FACTORY_ROOT:-}" ] && missing+=("FACTORY_ROOT")
[ -z "${PRIMARY_BRANCH:-}" ] && missing+=("PRIMARY_BRANCH")
@ -35,17 +33,11 @@ _assert_forge_push_globals() {
push_to_forge() {
local repo_root="$1" forge_url="$2" repo_slug="$3"
# Build authenticated remote URL: http://dev-bot:<password>@host:port/org/repo.git
# Forgejo 11.x rejects API tokens for git HTTP push (#361); password auth works.
if [ -z "${FORGE_PASS:-}" ]; then
echo "Error: FORGE_PASS not set — cannot push to Forgejo (see #361)" >&2
return 1
fi
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_PASS}@|")
local remote_url="${auth_url}/${repo_slug}.git"
# Display URL without token
local display_url="${forge_url}/${repo_slug}.git"
# Use clean URL — credential helper supplies auth (#604).
# Forgejo 11.x rejects API tokens for git HTTP push (#361); password auth works
# via the credential helper configured in configure_git_creds().
local remote_url="${forge_url}/${repo_slug}.git"
local display_url="$remote_url"
# Always set the remote URL to ensure credentials are current
if git -C "$repo_root" remote get-url forgejo >/dev/null 2>&1; then

View file

@ -113,11 +113,9 @@ ensure_profile_repo() {
# Define cache directory: /home/agent/data/.profile/{agent-name}
PROFILE_REPO_PATH="${HOME:-/home/agent}/data/.profile/${agent_identity}"
# Build clone URL from FORGE_URL and agent identity
# Build clone URL from FORGE_URL — credential helper supplies auth (#604)
local forge_url="${FORGE_URL:-http://localhost:3000}"
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://$(whoami):${FORGE_TOKEN}@|")
local clone_url="${auth_url}/${agent_identity}/.profile.git"
local clone_url="${forge_url}/${agent_identity}/.profile.git"
# Check if already cached and up-to-date
if [ -d "${PROFILE_REPO_PATH}/.git" ]; then
@ -592,14 +590,8 @@ ensure_ops_repo() {
local ops_repo="${FORGE_OPS_REPO:-}"
[ -n "$ops_repo" ] || return 0
local forge_url="${FORGE_URL:-http://localhost:3000}"
local clone_url
if [ -n "${FORGE_TOKEN:-}" ]; then
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://$(whoami):${FORGE_TOKEN}@|")
clone_url="${auth_url}/${ops_repo}.git"
else
clone_url="${forge_url}/${ops_repo}.git"
fi
# Use clean URL — credential helper supplies auth (#604)
local clone_url="${forge_url}/${ops_repo}.git"
log "Cloning ops repo: ${ops_repo} -> ${ops_root}"
if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then

View file

@ -391,6 +391,7 @@ services:
- FORGE_REPO=${FORGE_REPO:-disinto-admin/disinto}
- FORGE_OPS_REPO=${FORGE_OPS_REPO:-disinto-admin/disinto-ops}
- FORGE_TOKEN=${FORGE_TOKEN:-}
- FORGE_PASS=${FORGE_PASS:-}
- FORGE_ADMIN_USERS=${FORGE_ADMIN_USERS:-disinto-admin}
- FORGE_ADMIN_TOKEN=${FORGE_ADMIN_TOKEN:-}
- OPS_REPO_ROOT=/opt/disinto-ops

120
lib/git-creds.sh Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env bash
# git-creds.sh — Shared git credential helper configuration
#
# Configures a static credential helper for Forgejo password-based HTTP auth.
# Forgejo 11.x rejects API tokens for git push (#361); password auth works.
# This ensures all git operations (clone, fetch, push) use password auth
# without needing tokens embedded in remote URLs (#604).
#
# Usage:
# source "${FACTORY_ROOT}/lib/git-creds.sh"
# configure_git_creds [HOME_DIR] [RUN_AS_CMD]
# repair_baked_cred_urls DIR [DIR ...]
#
# Globals expected:
# FORGE_PASS — bot password for git HTTP auth
# FORGE_URL — Forge instance URL (e.g. http://forgejo:3000)
# FORGE_TOKEN — API token (used to resolve bot username)
set -euo pipefail
# configure_git_creds [HOME_DIR] [RUN_AS_CMD]
# HOME_DIR — home directory for the git user (default: $HOME or /home/agent)
# RUN_AS_CMD — command prefix to run as another user (e.g. "gosu agent")
#
# Writes a credential helper script and configures git to use it globally.
configure_git_creds() {
local home_dir="${1:-${HOME:-/home/agent}}"
local run_as="${2:-}"
if [ -z "${FORGE_PASS:-}" ] || [ -z "${FORGE_URL:-}" ]; then
return 0
fi
local forge_host forge_proto
forge_host=$(printf '%s' "$FORGE_URL" | sed 's|https\?://||; s|/.*||')
forge_proto=$(printf '%s' "$FORGE_URL" | sed 's|://.*||')
# Determine the bot username from FORGE_TOKEN identity (or default to dev-bot)
local bot_user=""
if [ -n "${FORGE_TOKEN:-}" ]; then
bot_user=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${FORGE_URL}/api/v1/user" 2>/dev/null | jq -r '.login // empty') || bot_user=""
fi
bot_user="${bot_user:-dev-bot}"
local helper_path="${home_dir}/.git-credentials-helper"
# Write a static credential helper script (git credential protocol)
cat > "$helper_path" <<CREDEOF
#!/bin/sh
# Auto-generated git credential helper for Forgejo password auth (#361, #604)
# Only respond to "get" action; ignore "store" and "erase".
[ "\$1" = "get" ] || exit 0
# Read and discard stdin (git sends protocol/host info)
cat >/dev/null
echo "protocol=${forge_proto}"
echo "host=${forge_host}"
echo "username=${bot_user}"
echo "password=${FORGE_PASS}"
CREDEOF
chmod 755 "$helper_path"
# Set ownership and configure git if running as a different user
if [ -n "$run_as" ]; then
local target_user
target_user=$(echo "$run_as" | awk '{print $NF}')
chown "${target_user}:${target_user}" "$helper_path" 2>/dev/null || true
$run_as bash -c "git config --global credential.helper '${helper_path}'"
else
git config --global credential.helper "$helper_path"
fi
# Set safe.directory to work around dubious ownership after container restart
if [ -n "$run_as" ]; then
$run_as bash -c "git config --global --add safe.directory '*'"
else
git config --global --add safe.directory '*'
fi
}
# repair_baked_cred_urls DIR [DIR ...]
# Scans git repos under each DIR and rewrites remote URLs that contain
# embedded credentials (user:pass@host) to clean URLs.
# Logs each repair so operators can see the migration happened.
#
# Set _GIT_CREDS_LOG_FN to a custom log function name (default: echo).
repair_baked_cred_urls() {
local log_fn="${_GIT_CREDS_LOG_FN:-echo}"
for dir in "$@"; do
[ -d "$dir" ] || continue
# Find git repos: either dir itself or immediate subdirectories
local -a repos=()
if [ -d "${dir}/.git" ]; then
repos+=("$dir")
else
local sub
for sub in "$dir"/*/; do
[ -d "${sub}.git" ] && repos+=("${sub%/}")
done
fi
local repo
for repo in "${repos[@]}"; do
local url
url=$(git -C "$repo" config --get remote.origin.url 2>/dev/null || true)
[ -n "$url" ] || continue
# Check if URL contains embedded credentials: http(s)://user:pass@host
if printf '%s' "$url" | grep -qE '^https?://[^/]+@'; then
# Strip credentials: http(s)://user:pass@host/path -> http(s)://host/path
local clean_url
clean_url=$(printf '%s' "$url" | sed -E 's|(https?://)[^@]+@|\1|')
git -C "$repo" remote set-url origin "$clean_url"
$log_fn "Repaired baked credentials in ${repo} (remote origin -> ${clean_url})"
fi
done
done
}

View file

@ -153,11 +153,10 @@ setup_ops_repo() {
echo " ! disinto-admin = admin (already set or failed)"
fi
# Clone ops repo locally if not present
# Clone ops repo locally if not present — use clean URL, credential helper
# supplies auth (#604).
if [ ! -d "${ops_root}/.git" ]; then
local auth_url
auth_url=$(printf '%s' "$forge_url" | sed "s|://|://dev-bot:${FORGE_TOKEN}@|")
local clone_url="${auth_url}/${actual_ops_slug}.git"
local clone_url="${forge_url}/${actual_ops_slug}.git"
echo "Cloning: ops repo -> ${ops_root}"
if git clone --quiet "$clone_url" "$ops_root" 2>/dev/null; then
echo "Ops repo: ${actual_ops_slug} cloned successfully"