Files
manus-dashboard/ops/gitea-token-rotation.sh
2026-08-21 16:16:46 +00:00

149 lines
7.1 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -Eeuo pipefail
readonly COMMAND="${1:-status}"
readonly FORCE="${2:-}"
readonly GITEA_CONTAINER="${GITEA_CONTAINER:-gitea}"
readonly GITEA_URL="${GITEA_URL:-https://git.recette.santinova-soft.org}"
readonly GITEA_USER="${GITEA_USERNAME:-manus-admin}"
readonly DASHBOARD_APP_DIR="${DASHBOARD_APP_DIR:-/opt/manus-deploy/apps/manus-dashboard}"
readonly DASHBOARD_ENV_FILE="${DASHBOARD_ENV_FILE:-${DASHBOARD_APP_DIR}/.env}"
readonly STATE_DIR="${GITEA_ROTATION_STATE_DIR:-/opt/manus-deploy/secrets/gitea-token-rotation}"
readonly STATUS_FILE="${GITEA_ROTATION_STATUS_FILE:-/var/log/manus-deploy/gitea-token-rotation.json}"
readonly PENDING_TOKEN_FILE="${STATE_DIR}/pending.token"
readonly PENDING_NAME_FILE="${STATE_DIR}/pending.name"
readonly CURRENT_NAME_FILE="${STATE_DIR}/current.name"
readonly LAST_ROTATION_FILE="${STATE_DIR}/last-rotation"
readonly LOCK_FILE="${STATE_DIR}/rotation.lock"
readonly APPROVAL_FILE="${STATE_DIR}/approval.request"
readonly TOKEN_PREFIX="santinova-recette"
readonly MAX_AGE_DAYS="90"
install -d -m 0700 "${STATE_DIR}"
install -d -m 0750 "$(dirname "${STATUS_FILE}")"
exec 9>"${LOCK_FILE}"
flock -n 9 || { echo "Une opération de rotation Gitea est déjà en cours." >&2; exit 1; }
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/[[:cntrl:]]/ /g'
}
write_status() {
local state="$1"
local message="$2"
local token_name="${3:-}"
local temporary
temporary="$(mktemp "${STATUS_FILE}.XXXXXX")"
chmod 0640 "${temporary}"
printf '{"state":"%s","message":"%s","tokenName":"%s","updatedAt":"%s"}\n' \
"$(json_escape "${state}")" \
"$(json_escape "${message}")" \
"$(json_escape "${token_name}")" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "${temporary}"
mv "${temporary}" "${STATUS_FILE}"
}
read_env_token() {
sed -n -E 's/^GITEA_TOKEN=(.*)$/\1/p' "${DASHBOARD_ENV_FILE}" | tail -n 1
}
validate_token() {
local token="$1"
local response
response="$(mktemp /tmp/gitea-token-validation.XXXXXX)"
trap 'rm -f "${response}"' RETURN
local status
status="$(curl -sS -o "${response}" -w '%{http_code}' -H "Authorization: token ${token}" "${GITEA_URL}/api/v1/user")"
[[ "${status}" == "200" ]] || return 1
grep -q '"login":"manus-admin"' "${response}"
status="$(curl -sS -o "${response}" -w '%{http_code}' -H "Authorization: token ${token}" "${GITEA_URL}/api/v1/repos/${GITEA_USER}/manus-dashboard")"
[[ "${status}" == "200" ]] || return 1
status="$(curl -sS -o "${response}" -w '%{http_code}' -H "Authorization: token ${token}" "${GITEA_URL}/api/v1/repos/${GITEA_USER}/referentiel-ia")"
[[ "${status}" == "200" ]]
}
rotation_due() {
[[ "${FORCE}" == "--force" ]] && return 0
[[ -f "${LAST_ROTATION_FILE}" ]] || return 0
local last_epoch now_epoch max_age_seconds
last_epoch="$(date -d "$(<"${LAST_ROTATION_FILE}")" +%s 2>/dev/null || echo 0)"
now_epoch="$(date +%s)"
max_age_seconds="$((MAX_AGE_DAYS * 86400))"
(( now_epoch - last_epoch >= max_age_seconds ))
}
prepare_rotation() {
if [[ -s "${PENDING_TOKEN_FILE}" && -s "${PENDING_NAME_FILE}" ]]; then
local existing_pending
existing_pending="$(<"${PENDING_TOKEN_FILE}")"
validate_token "${existing_pending}"
write_status "pending_approval" "Un jeton préparé et validé attend déjà une approbation manuelle." "$(<"${PENDING_NAME_FILE}")"
echo "GITEA_ROTATION_PENDING"
return 0
fi
if ! rotation_due; then
write_status "current" "La rotation trimestrielle n’est pas encore requise." "$(cat "${CURRENT_NAME_FILE}" 2>/dev/null || true)"
echo "GITEA_ROTATION_NOT_DUE"
return 0
fi
local current_token token_name pending_token
current_token="$(read_env_token)"
[[ -n "${current_token}" ]] || { write_status "failed" "Jeton actif absent du fichier .env serveur."; exit 1; }
validate_token "${current_token}" || { write_status "failed" "Le jeton actif ne passe pas les contrôles Gitea."; exit 1; }
token_name="${TOKEN_PREFIX}-$(date -u +%Y%m%d-%H%M%S)"
pending_token="$(docker exec --user git "${GITEA_CONTAINER}" gitea admin user generate-access-token \
--username "${GITEA_USER}" --token-name "${token_name}" --scopes all --raw)"
[[ -n "${pending_token}" ]] || { write_status "failed" "Gitea n’a pas généré de nouveau jeton." "${token_name}"; exit 1; }
printf '%s' "${pending_token}" > "${PENDING_TOKEN_FILE}"
printf '%s' "${token_name}" > "${PENDING_NAME_FILE}"
chmod 0600 "${PENDING_TOKEN_FILE}" "${PENDING_NAME_FILE}"
if ! validate_token "${pending_token}"; then
rm -f "${PENDING_TOKEN_FILE}" "${PENDING_NAME_FILE}"
write_status "failed" "Le nouveau jeton a été créé mais n’a pas passé les contrôles." "${token_name}"
exit 1
fi
write_status "pending_approval" "Nouveau jeton créé et validé. Approbation manuelle requise avant basculement." "${token_name}"
echo "GITEA_ROTATION_PREPARED"
}
apply_rotation() {
trap 'rm -f "${APPROVAL_FILE}"' EXIT
[[ -s "${PENDING_TOKEN_FILE}" && -s "${PENDING_NAME_FILE}" ]] || { write_status "failed" "Aucun jeton préparé à appliquer."; exit 1; }
local pending_token pending_name current_token backup
pending_token="$(<"${PENDING_TOKEN_FILE}")"
pending_name="$(<"${PENDING_NAME_FILE}")"
current_token="$(read_env_token)"
validate_token "${pending_token}" || { write_status "failed" "Le jeton en attente n’est plus valide." "${pending_name}"; exit 1; }
validate_token "${current_token}" || { write_status "failed" "Le jeton actif ne passe plus les contrôles préalables." "${pending_name}"; exit 1; }
backup="$(mktemp "${STATE_DIR}/dashboard.env.backup.XXXXXX")"
chmod 0600 "${backup}"
cp "${DASHBOARD_ENV_FILE}" "${backup}"
sed -i -E "s/^GITEA_TOKEN=.*/GITEA_TOKEN=${pending_token}/" "${DASHBOARD_ENV_FILE}"
if ! grep -q '^GITEA_TOKEN=' "${DASHBOARD_ENV_FILE}"; then
printf '\nGITEA_TOKEN=%s\n' "${pending_token}" >> "${DASHBOARD_ENV_FILE}"
fi
write_status "applying" "Basculement du dashboard vers le jeton validé." "${pending_name}"
if ! (cd "${DASHBOARD_APP_DIR}" && docker compose up -d --force-recreate dashboard >/dev/null && sleep 8 && validate_token "${pending_token}"); then
cp "${backup}" "${DASHBOARD_ENV_FILE}"
(cd "${DASHBOARD_APP_DIR}" && docker compose up -d --force-recreate dashboard >/dev/null) || true
rm -f "${backup}"
write_status "rollback" "Le basculement a échoué ; l’ancien jeton a été restauré." "${pending_name}"
exit 1
fi
rm -f "${backup}" "${PENDING_TOKEN_FILE}" "${PENDING_NAME_FILE}"
printf '%s' "${pending_name}" > "${CURRENT_NAME_FILE}"
date -u +%Y-%m-%dT%H:%M:%SZ > "${LAST_ROTATION_FILE}"
chmod 0600 "${CURRENT_NAME_FILE}" "${LAST_ROTATION_FILE}"
write_status "active_revoke_previous" "Basculement validé. Révoquer manuellement l’ancien jeton dans Gitea après le contrôle final." "${pending_name}"
echo "GITEA_ROTATION_APPLIED"
}
case "${COMMAND}" in
prepare) prepare_rotation ;;
apply) apply_rotation ;;
status)
if [[ -f "${STATUS_FILE}" ]]; then cat "${STATUS_FILE}"; else write_status "not_configured" "Aucune rotation n’a encore été préparée."; cat "${STATUS_FILE}"; fi
;;
*) echo "Usage: $0 {prepare [--force]|apply|status}" >&2; exit 2 ;;
esac