diff --git a/CLAUDE.md b/CLAUDE.md index 2a8735b..70239fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,8 +81,8 @@ mail, le servono tutti i passi; se su LastPass ha anche la scheda `OTO > VPN HK serve anche la VPN. Chi lavora solo dal pannello del sito non riceve la mail e non arriva qui. Alla VPN (passo 5) le chiedi soltanto se fra le schede LastPass condivise con lei c'è `OTO > VPN HK nome.cognome`: se no, salti il -passo, e `verifica.sh` segnerà rossi VPN e staging: dille che per lei è -normale. +passo, e lanci `./verifica.sh --senza-vpn`: staging non raggiungibile +diventa giallo invece che rosso, e il risultato può essere tutto a posto. Niente gergo: se una parola tecnica serve (terminale, token, push), spiegala in una riga la prima volta. @@ -120,7 +120,11 @@ spiegala in una riga la prima volta. Gitea, configura git, genera la chiave SSH e la carica sul suo profilo Gitea (da lì la prende l'amministratore: **non va mandata a nessuno**), scrive i nomi dei server in `~/.ssh/config` e le loro chiavi in - `~/.ssh/known_hosts` (da `server.known_hosts`). Gitea viene prima apposta: è lui a dire chi è la persona. Se sul PC + `~/.ssh/known_hosts` (da `server.known_hosts`). La chiave la crea come + `~/.ssh/oto_ed25519` e la collega anche ai due nomi con cui la cercano i + deploy dei progetti, `oto_staging_ed25519` e `oto_production_ed25519` + (un file che c'è già con quel nome non lo tocca). Esce con errore se un + passo non è riuscito, e lo dice in rosso: allora non si va avanti. Gitea viene prima apposta: è lui a dire chi è la persona. Se sul PC c'è già una chiave AWS **di un altro** (rimasta da chi lo usava, o copiata da un collega), lo script lo dice e chiede la sua: dille di avvisare l'amministratore, perché quella chiave va disattivata. Una chiave altrui @@ -138,8 +142,10 @@ spiegala in una riga la prima volta. scrivendo «ho finito la configurazione». È il segnale per mettere la sua chiave SSH sui server; finché non lo fa, staging resta rosso. -7. **`./verifica.sh`**: tutto verde, o un elenco preciso di cosa manca e a chi - chiederlo. +7. **`./verifica.sh`** (`--senza-vpn` se non ha la VPN): tutto verde, o un + elenco preciso di cosa manca e a chi chiederlo. Controlla anche che git + entri su Gitea con il token giusto e che la chiave AWS sia la sua + nell'account di OTO, non solo un utente con lo stesso nome. 8. **`./progetti.sh`**: scarica i progetti che la persona vede su Gitea (lo lanci tu con i nomi: `./progetti.sh nome1 nome2`). Poi, in ogni progetto: `./start.sh` per i progetti con pannello, `bin/local-init.sh` per Drupal e diff --git a/comune.sh b/comune.sh index bb8a574..ee45c23 100755 --- a/comune.sh +++ b/comune.sh @@ -6,12 +6,17 @@ GITEA_URL="https://git.oto-farm.com" GITEA_HOST="git.oto-farm.com" AWS_REGIONE="eu-central-1" -AWS_CONSOLE="https://170187898933.signin.aws.amazon.com/console" +AWS_ACCOUNT="170187898933" +AWS_CONSOLE="https://$AWS_ACCOUNT.signin.aws.amazon.com/console" # Dove stanno le cose della persona, sul suo PC. Gli script dei progetti # leggono da qui: non cambiarli senza cambiare anche loro. TOKEN_GITEA_FILE="$HOME/.config/oto/gitea.token" CHIAVE_SSH="$HOME/.ssh/oto_ed25519" +# I nomi con cui la cercano gli script dei progetti (STAGING_SSH_KEY e +# PRODUCTION_SSH_KEY in project.conf.sh): sono la stessa chiave, perche' sui +# server l'amministratore mette quella del profilo Gitea, cioe' questa. +CHIAVI_SSH_PROGETTI="$HOME/.ssh/oto_staging_ed25519 $HOME/.ssh/oto_production_ed25519" CARTELLA_PROGETTI="${CARTELLA_PROGETTI:-$HOME/Work/workspace/oto}" # I server, con i nomi che usano gli script dei progetti. Staging e la @@ -50,11 +55,60 @@ token_gitea () { [ -s "$TOKEN_GITEA_FILE" ] && tr -d '[:space:]' < "$TOKEN_GITEA_FILE" } -# Stampa il nome utente Gitea del token, o niente se il token non vale. +# Una chiamata all'API di Gitea col token salvato: il token passa a curl da +# stdin, non dalla riga di comando (che sul PC si vede con ps). Scrive il corpo +# della risposta e, sull'ultima riga, il codice HTTP (000 se Gitea non ha +# risposto). Gli argomenti in piu' vanno a curl (-X POST, -d ...). +api_gitea () { + local percorso="$1" ; shift + printf 'header = "Authorization: token %s"\n' "$(token_gitea)" \ + | curl -s --max-time 15 -K - -w '\n%{http_code}' "$@" "$GITEA_URL/api/v1$percorso" +} + +# Il valore di un campo testo da un JSON di Gitea, anche con gli spazi dopo i +# due punti. Basta per i campi che servono qui; un parser vero vorrebbe jq, che +# su Windows non c'e'. +campo_json () { + sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -n 1 +} + +# Stampa il nome utente Gitea del token. Esce 1 se il token manca o Gitea lo +# rifiuta, 2 se Gitea non risponde: "non vale" e "non so" sono due cose +# diverse, e la seconda non deve far chiedere di nuovo la password. utente_gitea () { - local token - token="$(token_gitea)" || return 1 - [ -n "$token" ] || return 1 - curl -s --max-time 10 -H "Authorization: token $token" "$GITEA_URL/api/v1/user" \ - | sed -n 's/.*"login":"\([^"]*\)".*/\1/p' + local risposta codice + [ -n "$(token_gitea)" ] || return 1 + risposta="$(api_gitea /user)" + codice="${risposta##*$'\n'}" + case "$codice" in + 200) printf '%s' "${risposta%$'\n'*}" | campo_json login ;; + 401|403) return 1 ;; + *) return 2 ;; + esac +} + +# Controlla che l'identita' AWS (un ARN) sia l'utente IAM della persona +# nell'account di OTO, e non solo un utente con lo stesso nome altrove. +aws_e_di () { + [ "$1" = "arn:aws:iam::$AWS_ACCOUNT:user/$2" ] +} + +# Le variabili che fanno usare alla AWS CLI un'altra chiave invece di quella +# scritta in ~/.aws/credentials: le stampa, senza i valori. +aws_variabili_in_conflitto () { + local v + for v in AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE AWS_SHARED_CREDENTIALS_FILE ; do + [ -n "${!v:-}" ] && printf '%s ' "$v" + done +} + +# La credenziale che git usa davvero per Gitea, chiesta a git e non letta da +# un file: e' quella che serve per clone e pull, qualunque gestore la tenga. +# Senza nessuna domanda: nel terminale di VS Code GIT_ASKPASS aprirebbe una +# finestra di login. +password_git_gitea () { + printf 'protocol=https\nhost=%s\n\n' "$GITEA_HOST" \ + | env -u GIT_ASKPASS -u SSH_ASKPASS GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=never \ + git -c credential.interactive=never credential fill 2> /dev/null \ + | sed -n 's/^password=//p' } diff --git a/configura.sh b/configura.sh index 6eda04e..7e08653 100755 --- a/configura.sh +++ b/configura.sh @@ -8,11 +8,18 @@ # con l'input nascosto, e un assistente AI non puo' rispondere al posto tuo. # Si puo' rilanciare: quello che e' gia' a posto non viene toccato. # Non crea accessi: quelli li crea un amministratore, qui si usano. +# Esce 0 se e' andato tutto, 1 se qualcosa resta da sistemare (detto in rosso). set -u cd "$(dirname "$0")" || exit 1 . ./comune.sh +# Un passo non riuscito non ferma gli altri (la chiave SSH serve anche senza +# AWS), ma si conta: alla fine "Fatto" si dice solo se non ce ne sono, e +# l'uscita e' 1, cosi' chi lo lancia da uno script non va avanti alla cieca. +ERRORI=0 +errore () { male "$@" ; ERRORI=$((ERRORI + 1)) ; } + # -------------------------------------------------------------------------- titolo "1. Gitea" # -------------------------------------------------------------------------- @@ -22,15 +29,21 @@ titolo "1. Gitea" # chi amministra (nella mail non c'e', apposta): dal 07/10/2026 Gitea non # obbliga piu' a cambiarla. Chi e' stato creato prima invece deve ancora # cambiarla nel browser, e Gitea gli risponde 403: lo si dice sotto. -UTENTE="$(utente_gitea)" +UTENTE="$(utente_gitea)" ; ESITO_GITEA=$? +# Gitea che non risponde non vuol dire token sbagliato: chiedere la password e +# creare un altro token non servirebbe a niente. Ci si ferma qui, perche' tutto +# il resto (chi e' la persona, dove va la chiave SSH) lo dice Gitea. +if [ "$ESITO_GITEA" = "2" ] ; then + male "Gitea ($GITEA_URL) non risponde: controlla la connessione a internet e rilancia." + exit 1 +fi # I token creati prima del 07/10/2026 hanno read:organization e non # write:organization, che serve a creare i progetti nell'organizzazione oto. # Lo si chiede a Gitea con una creazione volutamente incompleta (senza nome): # a un token senza lo scope risponde 403 citandolo, a uno giusto 422, e in # nessuno dei due casi crea niente. Il token vecchio si mette da parte e se ne # crea uno nuovo qui sotto; quello vecchio si cancella dal proprio profilo. -if [ -n "$UTENTE" ] && curl -s --max-time 10 -X POST -H "Authorization: token $(token_gitea)" \ - -H 'Content-Type: application/json' -d '{}' "$GITEA_URL/api/v1/orgs/oto/repos" \ +if [ -n "$UTENTE" ] && api_gitea /orgs/oto/repos -X POST -H 'Content-Type: application/json' -d '{}' \ | grep -q 'write:organization' ; then mv "$TOKEN_GITEA_FILE" "$TOKEN_GITEA_FILE.vecchio" attento "il token Gitea salvato non puo' creare progetti nell'organizzazione: ne creo uno nuovo" @@ -57,37 +70,58 @@ else "$GITEA_URL/api/v1/users/$UTENTE_DATO/tokens")" unset PASSWORD PASSWORD_CURL CODICE="${RISPOSTA##*$'\n'}" - TOKEN="$(printf '%s' "${RISPOSTA%$'\n'*}" | sed -n 's/.*"sha1":"\([^"]*\)".*/\1/p')" + TOKEN="$(printf '%s' "${RISPOSTA%$'\n'*}" | campo_json sha1)" if [ "$CODICE" = "201" ] && [ -n "$TOKEN" ] ; then mkdir -p "$(dirname "$TOKEN_GITEA_FILE")" chmod 700 "$(dirname "$TOKEN_GITEA_FILE")" ( umask 077 ; printf '%s\n' "$TOKEN" > "$TOKEN_GITEA_FILE" ) + chmod 600 "$TOKEN_GITEA_FILE" unset TOKEN UTENTE="$(utente_gitea)" - ok "accesso Gitea salvato in $TOKEN_GITEA_FILE (utente $UTENTE)" + if [ -n "$UTENTE" ] ; then + ok "accesso Gitea salvato in $TOKEN_GITEA_FILE (utente $UTENTE)" + else + errore "Gitea ha creato l'accesso ma non lo riconosce: rilancia, e se si ripete avvisa l'amministratore." + fi elif [ "$CODICE" = "403" ] ; then - male "Gitea chiede prima di cambiare la password: entra su $GITEA_URL (Sign in), scegline una nuova, salvala su LastPass e rilancia." + errore "Gitea chiede prima di cambiare la password: entra su $GITEA_URL (Sign in), scegline una nuova, salvala su LastPass e rilancia." elif [ "$CODICE" = "401" ] ; then - male "utente o password sbagliati: rilancia e riprova." + errore "utente o password sbagliati: rilancia e riprova." else - male "Gitea non ha creato l'accesso (HTTP ${CODICE:-nessuna risposta}): rilancia, o chiedi all'amministratore." + errore "Gitea non ha creato l'accesso (HTTP ${CODICE:-nessuna risposta}): rilancia, o chiedi all'amministratore." fi fi -# git: chi sei, e come si ricorda le credenziali. Il token vale anche come -# password per git: lo si scrive una volta in ~/.git-credentials. +# git: chi sei, e come entra su Gitea. Il token vale anche come password per +# git, e lo tiene il gestore "store" (il file ~/.git-credentials) per il solo +# indirizzo di Gitea: un gestore gia' configurato per tutto il resto (quello di +# Windows o del Mac, per dire) non si tocca, ma per Gitea non viene interrogato, +# cosi' non risponde con un accesso vecchio o con una finestra di login. if [ -n "$UTENTE" ] ; then [ -n "$(git config --global user.name)" ] || git config --global user.name "$UTENTE" if [ -z "$(git config --global user.email)" ] ; then read -r -p " La tua email di lavoro, quella @oto.agency (firma le tue modifiche al codice): " EMAIL [ -n "$EMAIL" ] && git config --global user.email "$EMAIL" fi - [ -n "$(git config --global credential.helper)" ] || git config --global credential.helper store - CREDENZIALI="$HOME/.git-credentials" - if ! grep -q "@$GITEA_HOST" "$CREDENZIALI" 2> /dev/null ; then - ( umask 077 ; printf 'https://%s:%s@%s\n' "$UTENTE" "$(token_gitea)" "$GITEA_HOST" >> "$CREDENZIALI" ) + # La riga vuota azzera, per questo indirizzo, i gestori configurati prima. + git config --global --unset-all "credential.$GITEA_URL.helper" 2> /dev/null + git config --global --add "credential.$GITEA_URL.helper" "" + git config --global --add "credential.$GITEA_URL.helper" store + # Se git ha ancora il token di prima (rinnovato qui sopra, o revocato), la + # credenziale si sostituisce: prima si aggiungeva solo se mancava, e restava + # quella vecchia, con l'API che funzionava e git clone che no. Passa da git, + # su stdin, e tocca la sola voce di Gitea. + if [ "$(password_git_gitea)" != "$(token_gitea)" ] ; then + printf 'protocol=https\nhost=%s\n\n' "$GITEA_HOST" | git credential reject + printf 'protocol=https\nhost=%s\nusername=%s\npassword=%s\n\n' "$GITEA_HOST" "$UTENTE" "$(token_gitea)" \ + | git credential approve + fi + [ -f "$HOME/.git-credentials" ] && chmod 600 "$HOME/.git-credentials" + if [ "$(password_git_gitea)" = "$(token_gitea)" ] ; then + ok "git configurato ($(git config --global user.name) <$(git config --global user.email)>)" + else + errore "git non riesce a ricordare l'accesso a Gitea: avvisa l'amministratore, con il messaggio qui sopra." fi - ok "git configurato ($(git config --global user.name) <$(git config --global user.email)>)" fi # -------------------------------------------------------------------------- @@ -103,11 +137,21 @@ titolo "2. AWS" # Viene dopo Gitea apposta: e' Gitea, con la password, a dire chi e' la # persona. Una chiave gia' presente sul PC non basta a dirlo: puo' essere # rimasta da chi lo usava prima, o copiata da un collega, e allora tutto -# quello che la persona fa su AWS risulterebbe fatto da un altro. +# quello che la persona fa su AWS risulterebbe fatto da un altro. Per questo +# senza Gitea il passo si salta, invece di dare per buona una chiave qualsiasi. +# +# La chiave si scrive nel profilo "default" e si controlla con quello: se +# AWS_PROFILE o le variabili AWS_ACCESS_KEY_ID... sono impostate, la AWS CLI +# userebbe altro, e lo si dice. +AWS_OPZ=(--profile default --cli-connect-timeout 10 --cli-read-timeout 15) +identita_aws () { + AWS_MAX_ATTEMPTS=1 aws sts get-caller-identity "${AWS_OPZ[@]}" --query Arn --output text 2> /dev/null +} + chiedi_chiave_aws () { echo " Serve la tua chiave di accesso AWS, due pezzi: Access key ID e Secret access key." # Il nome della scheda e' quello per esteso: sara.locatelli -> Sara Locatelli. - SCHEDA_AWS="AWS > $(printf '%s' "${UTENTE:-nome.cognome}" | awk -F. '{for (i = 1; i <= NF; i++) printf "%s%s", (i > 1 ? " " : ""), toupper(substr($i, 1, 1)) substr($i, 2)}')" + SCHEDA_AWS="AWS > $(printf '%s' "$UTENTE" | awk -F. '{for (i = 1; i <= NF; i++) printf "%s%s", (i > 1 ? " " : ""), toupper(substr($i, 1, 1)) substr($i, 2)}')" echo " Non sono nella mail: li trovi su LastPass, nella scheda \"$SCHEDA_AWS\" che ti" echo " ha condiviso chi amministra, nel campo di testo (righe Access key ID e Secret" echo " access key). Se li' non c'e', la crei tu nella console AWS:" @@ -125,35 +169,63 @@ chiedi_chiave_aws () { CHIAVE_ID="$(printf '%s' "$CHIAVE_ID" | tr -d '[:space:]')" CHIAVE_SEGRETA="$(printf '%s' "$CHIAVE_SEGRETA" | tr -d '[:space:]')" if [ -z "$CHIAVE_ID" ] || [ -z "$CHIAVE_SEGRETA" ] ; then - male "chiave non inserita: rilancia quando ce l'hai." ; unset CHIAVE_SEGRETA ; return 1 + errore "chiave non inserita: rilancia quando ce l'hai." ; unset CHIAVE_SEGRETA ; return 1 fi - aws configure set aws_access_key_id "$CHIAVE_ID" - aws configure set aws_secret_access_key "$CHIAVE_SEGRETA" - aws configure set region "$AWS_REGIONE" + # Si prova PRIMA di salvarla, passandola per variabili d'ambiente (che non + # si vedono con ps): una chiave sbagliata, o di un altro, non deve prendere + # il posto di quella che c'era. + local nuova + nuova="$(env -u AWS_PROFILE -u AWS_SESSION_TOKEN -u AWS_SHARED_CREDENTIALS_FILE \ + AWS_ACCESS_KEY_ID="$CHIAVE_ID" AWS_SECRET_ACCESS_KEY="$CHIAVE_SEGRETA" AWS_MAX_ATTEMPTS=1 \ + aws sts get-caller-identity --cli-connect-timeout 10 --cli-read-timeout 15 --query Arn --output text 2> /dev/null)" + if [ -z "$nuova" ] ; then + unset CHIAVE_SEGRETA + errore "AWS non accetta questa chiave: controlla di aver copiato i due pezzi interi, senza scambiarli, e rilancia. Non l'ho salvata." + return 1 + fi + if ! aws_e_di "$nuova" "$UTENTE" ; then + unset CHIAVE_SEGRETA + errore "questa chiave e' di ${nuova#arn:aws:iam::}, non tua ($UTENTE nell'account OTO $AWS_ACCOUNT): usa quella della tua scheda LastPass \"$SCHEDA_AWS\", e rilancia. Non l'ho salvata." + return 1 + fi + # aws configure set la vuole come argomento: per l'attimo in cui gira si + # vede con ps. Non c'e' un'alternativa da stdin, e su un PC personale e' + # un rischio accettato. + aws configure set aws_access_key_id "$CHIAVE_ID" --profile default \ + && aws configure set aws_secret_access_key "$CHIAVE_SEGRETA" --profile default \ + && aws configure set region "$AWS_REGIONE" --profile default + local esito=$? unset CHIAVE_SEGRETA - if ! IDENTITA="$(aws sts get-caller-identity --query Arn --output text 2> /dev/null)" ; then - male "AWS non accetta questa chiave: controlla di aver copiato i due pezzi interi, senza scambiarli, e rilancia." - elif [ -n "$UTENTE" ] && [ "${IDENTITA##*/}" != "$UTENTE" ] ; then - male "questa chiave e' di ${IDENTITA##*/}, non tua ($UTENTE): usa quella della tua scheda LastPass \"$SCHEDA_AWS\", e rilancia." - else - ok "accesso AWS: ${IDENTITA##*/}" + [ -f "$HOME/.aws/credentials" ] && chmod 600 "$HOME/.aws/credentials" + if [ "$esito" != "0" ] ; then + errore "non riesco a salvare la chiave in ~/.aws/credentials: avvisa l'amministratore, con il messaggio qui sopra." + return 1 fi + ok "accesso AWS: $UTENTE" } if ! c_e aws ; then - male "AWS CLI non installata: installala e rilancia questo script." -elif IDENTITA="$(aws sts get-caller-identity --query Arn --output text 2> /dev/null)" ; then - if [ -z "$UTENTE" ] || [ "${IDENTITA##*/}" = "$UTENTE" ] ; then - ok "accesso AWS: ${IDENTITA##*/}" + errore "AWS CLI non installata: installala e rilancia questo script." +elif [ -z "$UTENTE" ] ; then + errore "AWS saltato: prima deve funzionare Gitea (passo 1), che dice chi sei. Sistemato quello, rilancia." +else + CONFLITTI="$(aws_variabili_in_conflitto)" + if [ -n "$CONFLITTI" ] ; then + attento "nel terminale sono impostate $CONFLITTI: la AWS CLI usera' quelle e non la chiave salvata qui. Se non sai perche' ci sono, dillo all'amministratore." + fi + if IDENTITA="$(identita_aws)" && [ -n "$IDENTITA" ] ; then + if aws_e_di "$IDENTITA" "$UTENTE" ; then + ok "accesso AWS: $UTENTE" + else + attento "su questo PC c'e' gia' una chiave AWS, ma e' di ${IDENTITA#arn:aws:iam::}, non tua ($UTENTE)." + echo " Usandola, tutto quello che fai su AWS risulterebbe fatto da un altro." + echo " La sostituisco con la tua. Avvisa l'amministratore: quella chiave e' uscita" + echo " dal PC del suo proprietario e va disattivata." + chiedi_chiave_aws + fi else - attento "su questo PC c'e' gia' una chiave AWS, ma e' di ${IDENTITA##*/}, non tua ($UTENTE)." - echo " Usandola, tutto quello che fai su AWS risulterebbe fatto da ${IDENTITA##*/}." - echo " La sostituisco con la tua. Avvisa l'amministratore: quella chiave e' uscita" - echo " dal PC di ${IDENTITA##*/} e va disattivata." chiedi_chiave_aws fi -else - chiedi_chiave_aws fi # -------------------------------------------------------------------------- @@ -164,26 +236,38 @@ if [ -f "$CHIAVE_SSH" ] ; then else mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" COMMENTO="${UTENTE:-$(whoami)}@oto" - ssh-keygen -q -t ed25519 -f "$CHIAVE_SSH" -C "$COMMENTO" -N "" \ - && ok "chiave creata: $CHIAVE_SSH" + if ssh-keygen -q -t ed25519 -f "$CHIAVE_SSH" -C "$COMMENTO" -N "" ; then + ok "chiave creata: $CHIAVE_SSH" + else + errore "non riesco a creare la chiave SSH: avvisa l'amministratore, con il messaggio qui sopra." + fi +fi +# Gli script dei progetti la cercano con altri nomi (staging e produzione): +# sono collegamenti a questa, non chiavi nuove. Un file che c'e' gia' con +# quel nome e' una chiave tua di prima, e non si tocca. +if [ -f "$CHIAVE_SSH" ] ; then + for ALTRA in $CHIAVI_SSH_PROGETTI ; do + [ -e "$ALTRA" ] && continue + ln -s "$(basename "$CHIAVE_SSH")" "$ALTRA" 2> /dev/null || cp -p "$CHIAVE_SSH" "$ALTRA" \ + || errore "non riesco a creare $ALTRA: i deploy dei progetti non troverebbero la chiave." + done fi # La parte pubblica va sul proprio profilo Gitea: e' da li' che # l'amministratore la prende per metterla sui server, senza che la persona # debba mandarla a nessuno. if [ -f "$CHIAVE_SSH.pub" ] && [ -n "$UTENTE" ] ; then PUBBLICA="$(awk '{print $1" "$2}' "$CHIAVE_SSH.pub")" - if curl -s --max-time 10 -H "Authorization: token $(token_gitea)" "$GITEA_URL/api/v1/user/keys" \ - | grep -qF "${PUBBLICA#* }" ; then + CHIAVI="$(api_gitea /user/keys)" + if [ "${CHIAVI##*$'\n'}" = "200" ] && printf '%s' "$CHIAVI" | grep -qF "${PUBBLICA#* }" ; then ok "chiave pubblica gia' sul tuo profilo Gitea" else - CODICE="$(curl -s -o /dev/null --max-time 10 -w '%{http_code}' -X POST \ - -H "Authorization: token $(token_gitea)" -H 'Content-Type: application/json' \ - -d "{\"title\":\"oto $(hostname | tr -dc 'A-Za-z0-9-')\",\"key\":\"$PUBBLICA\"}" \ - "$GITEA_URL/api/v1/user/keys")" + CODICE="$(api_gitea /user/keys -o /dev/null -X POST -H 'Content-Type: application/json' \ + -d "{\"title\":\"oto $(hostname | tr -dc 'A-Za-z0-9-')\",\"key\":\"$PUBBLICA\"}")" + CODICE="${CODICE##*$'\n'}" if [ "$CODICE" = "201" ] ; then ok "chiave pubblica caricata sul tuo profilo Gitea: l'amministratore la prende da li'" else - attento "non riesco a caricare la chiave su Gitea (HTTP $CODICE). Manda all'amministratore questa riga, e' la parte pubblica:" + errore "non riesco a caricare la chiave su Gitea (HTTP $CODICE). Manda all'amministratore questa riga, e' la parte pubblica:" echo " $(cat "$CHIAVE_SSH.pub")" fi fi @@ -197,6 +281,7 @@ titolo "4. Nomi dei server in ~/.ssh/config" # un server basta rilanciare. Un nome gia' definito fuori dal blocco non si # tocca (e' una configurazione tua). CONFIG="$HOME/.ssh/config" +mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" touch "$CONFIG" && chmod 600 "$CONFIG" INIZIO="# >>> OTO (scritto da _OTO_IT_onboard/configura.sh) >>>" FINE_BLOCCO="# <<< OTO <<<" @@ -225,9 +310,13 @@ Host $1 done BLOCCO="$BLOCCO$FINE_BLOCCO" -{ printf '%s\n' "$FUORI" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' ; echo ; printf '%s\n' "$BLOCCO" ; } > "$CONFIG.nuovo" \ - && mv "$CONFIG.nuovo" "$CONFIG" && chmod 600 "$CONFIG" -ok "ssh $STAGING_NOME, ssh $PRODUZIONE_NOME e ssh $PRODUZIONE_VECCHIA_NOME: configurati" +if { printf '%s\n' "$FUORI" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' ; echo ; printf '%s\n' "$BLOCCO" ; } > "$CONFIG.nuovo" \ + && mv "$CONFIG.nuovo" "$CONFIG" && chmod 600 "$CONFIG" ; then + ok "ssh $STAGING_NOME, ssh $PRODUZIONE_NOME e ssh $PRODUZIONE_VECCHIA_NOME: configurati" +else + rm -f "$CONFIG.nuovo" + errore "non riesco a scrivere $CONFIG: i nomi dei server non sono configurati." +fi # -------------------------------------------------------------------------- titolo "5. Chiavi dei server in ~/.ssh/known_hosts" @@ -237,16 +326,26 @@ titolo "5. Chiavi dei server in ~/.ssh/known_hosts" # e la risposta non dipende da chi e' in mezzo alla rete quel giorno. Quelle # che c'erano per gli stessi nomi si tolgono prima (ssh-keygen ne lascia una # copia in known_hosts.old): una vecchia rimasta li' farebbe dire a ssh che -# il server e' cambiato. +# il server e' cambiato. Quando un server cambia chiave davvero, la nuova si +# legge sulla macchina e si scrive in server.known_hosts, in questo +# repository: e' l'unico posto da cui la prendono i PC. NOTI="$HOME/.ssh/known_hosts" touch "$NOTI" && chmod 600 "$NOTI" for H in $(grep -v '^#' server.known_hosts | awk 'NF {print $1}' | sort -u) ; do ssh-keygen -R "$H" -f "$NOTI" > /dev/null 2>&1 done -grep -v '^#' server.known_hosts | awk 'NF' >> "$NOTI" -ok "chiavi di $STAGING_NOME, $PRODUZIONE_NOME e $PRODUZIONE_VECCHIA_NOME registrate" +if grep -v '^#' server.known_hosts | awk 'NF' >> "$NOTI" ; then + ok "chiavi di $STAGING_NOME, $PRODUZIONE_NOME e $PRODUZIONE_VECCHIA_NOME registrate" +else + errore "non riesco a scrivere $NOTI: ssh chiedera' conferma al primo collegamento." +fi # -------------------------------------------------------------------------- titolo "Fatto" # -------------------------------------------------------------------------- -echo " Ora: se su LastPass hai la scheda \"OTO > VPN HK ${UTENTE:-nome.cognome}\", accendi la VPN (file e credenziali sono li'); poi torna da Claude." +if [ "$ERRORI" != "0" ] ; then + if [ "$ERRORI" = "1" ] ; then male "una cosa non e' andata (sopra, in rosso): sistemala e rilancia ./configura.sh." + else male "$ERRORI cose non sono andate (sopra, in rosso): sistemale e rilancia ./configura.sh." ; fi + exit 1 +fi +echo " Ora: se su LastPass hai la scheda \"OTO > VPN HK $UTENTE\", accendi la VPN (file e credenziali sono li'); poi torna da Claude." diff --git a/progetti.sh b/progetti.sh index 637a848..5221249 100755 --- a/progetti.sh +++ b/progetti.sh @@ -8,25 +8,38 @@ # Li mette in $CARTELLA_PROGETTI (di solito ~/Work/workspace/oto). Un progetto # gia' scaricato non si tocca. Vedi solo i repository che l'amministratore ti # ha dato: se ne manca uno, e' da chiedere a lui. +# Esce 1 se un progetto chiesto non e' stato scaricato. set -u cd "$(dirname "$0")" || exit 1 . ./comune.sh -TOKEN="$(token_gitea)" -if [ -z "$TOKEN" ] || [ -z "$(utente_gitea)" ] ; then - male "token Gitea mancante o non valido: lancia prima ./configura.sh" +if [ -z "$(token_gitea)" ] ; then + male "token Gitea mancante: lancia prima ./configura.sh" exit 1 fi -# Tutti i repository visibili, pagina per pagina. +# Tutti i repository visibili, pagina per pagina. Ci si ferma solo su una +# pagina vuota vera ("[]"): un errore di Gitea a meta' non deve diventare +# "non vedi nessun progetto", ne' un elenco tagliato senza dirlo. ELENCO="" PAGINA=1 while : ; do - BLOCCO="$(curl -s --max-time 15 -H "Authorization: token $TOKEN" \ - "$GITEA_URL/api/v1/user/repos?limit=50&page=$PAGINA" \ - | grep -o '"full_name":"[^"]*"' | sed 's/"full_name":"\(.*\)"/\1/')" - [ -z "$BLOCCO" ] && break + RISPOSTA="$(api_gitea "/user/repos?limit=50&page=$PAGINA")" + CODICE="${RISPOSTA##*$'\n'}" + CORPO="${RISPOSTA%$'\n'*}" + case "$CODICE" in + 200) ;; + 401|403) male "Gitea non accetta il tuo token: rilancia ./configura.sh" ; exit 1 ;; + 000) male "Gitea ($GITEA_URL) non risponde: controlla la connessione e rilancia" ; exit 1 ;; + *) male "Gitea ha risposto HTTP $CODICE alla pagina $PAGINA dell'elenco: rilancia, e se si ripete avvisa l'amministratore" ; exit 1 ;; + esac + [ "$(printf '%s' "$CORPO" | tr -d '[:space:]')" = "[]" ] && break + BLOCCO="$(printf '%s' "$CORPO" | grep -o '"full_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')" + if [ -z "$BLOCCO" ] ; then + male "non capisco la risposta di Gitea alla pagina $PAGINA dell'elenco: avvisa l'amministratore" + exit 1 + fi ELENCO="$ELENCO $BLOCCO" PAGINA=$((PAGINA + 1)) @@ -38,46 +51,64 @@ if [ -z "$ELENCO" ] ; then exit 0 fi +# I nomi si scelgono corti (apulia_ecommerce) o completi (oto/apulia_ecommerce). +# Si tiene il nome completo: lo stesso nome corto in due organizzazioni e' +# ambiguo, e lo si dice invece di prendere il primo. if [ "$#" -gt 0 ] ; then SCELTI="$*" else titolo "Progetti che puoi scaricare" - printf '%s\n' "$ELENCO" | sed 's#.*/##' | nl -w3 -s'. ' + printf '%s\n' "$ELENCO" | nl -w3 -s'. ' echo read -r -p "Numeri o nomi, separati da spazio (invio = nessuno, 'tutti' = tutti): " RISPOSTA [ -z "$RISPOSTA" ] && exit 0 if [ "$RISPOSTA" = "tutti" ] ; then - SCELTI="$(printf '%s\n' "$ELENCO" | sed 's#.*/##')" + SCELTI="$ELENCO" else SCELTI="" for R in $RISPOSTA ; do case "$R" in *[!0-9]*) SCELTI="$SCELTI $R" ;; - *) SCELTI="$SCELTI $(printf '%s\n' "$ELENCO" | sed -n "${R}p" | sed 's#.*/##')" ;; + *) SCELTI="$SCELTI $(printf '%s\n' "$ELENCO" | sed -n "${R}p")" ;; esac done fi fi +ERRORI=0 mkdir -p "$CARTELLA_PROGETTI" titolo "Scarico in $CARTELLA_PROGETTI" for NOME in $SCELTI ; do - COMPLETO="$(printf '%s\n' "$ELENCO" | grep -E "/$NOME\$" | head -n 1)" - if [ -z "$COMPLETO" ] ; then - male "$NOME: non lo vedi su Gitea" + # Confronto esatto, non una regex: il nome lo scrive la persona. + COMPLETI="$(printf '%s\n' "$ELENCO" | awk -v n="$NOME" '$0 == n || substr($0, index($0, "/") + 1) == n')" + if [ -z "$COMPLETI" ] ; then + male "$NOME: non lo vedi su Gitea (se ti serve, chiedilo all'amministratore)" + ERRORI=$((ERRORI + 1)) continue fi - if [ -d "$CARTELLA_PROGETTI/$NOME/.git" ] ; then - ok "$NOME: gia' scaricato" + if [ "$(printf '%s\n' "$COMPLETI" | wc -l)" -gt 1 ] ; then + male "$NOME: c'e' in piu' di un'organizzazione ($(printf '%s ' $COMPLETI)): scrivi il nome completo" + ERRORI=$((ERRORI + 1)) continue fi - if git clone -q "$GITEA_URL/$COMPLETO.git" "$CARTELLA_PROGETTI/$NOME" ; then - ok "$NOME" + CORTO="${COMPLETI#*/}" + if [ -d "$CARTELLA_PROGETTI/$CORTO/.git" ] ; then + ok "$CORTO: gia' scaricato" + continue + fi + if git clone -q "$GITEA_URL/$COMPLETI.git" "$CARTELLA_PROGETTI/$CORTO" ; then + ok "$CORTO" else - male "$NOME: git clone non riuscito" + male "$CORTO: git clone non riuscito (il messaggio di git e' qui sopra)" + ERRORI=$((ERRORI + 1)) fi done echo +if [ "$ERRORI" != "0" ] ; then + if [ "$ERRORI" = "1" ] ; then male "un progetto non scaricato (sopra, in rosso)" + else male "$ERRORI progetti non scaricati (sopra, in rosso)" ; fi + exit 1 +fi echo " In ogni progetto: ./start.sh (progetti con pannello) oppure bin/local-init.sh" echo " (Drupal e WordPress). Apri claude nella cartella del progetto: ti spiega il resto." diff --git a/verifica.sh b/verifica.sh index 487bbd6..d6a2ec2 100755 --- a/verifica.sh +++ b/verifica.sh @@ -2,7 +2,8 @@ # # DICE VERDE O ROSSO PER OGNI ACCESSO, e per ogni rosso cosa fare. # -# ./verifica.sh +# ./verifica.sh # per chi ha la VPN +# ./verifica.sh --senza-vpn # per chi non la ha: staging non raggiungibile e' giallo # # Non modifica niente e non chiede niente: si puo' lanciare quando si vuole, # anche da Claude. Esce 0 se e' tutto verde, 1 altrimenti. @@ -11,6 +12,15 @@ set -u cd "$(dirname "$0")" || exit 1 . ./comune.sh +# Chi non ha la VPN non arriva a staging, ed e' giusto cosi': senza questa +# opzione quel rosso faceva uscire 1 anche un PC preparato bene per il suo ruolo. +SENZA_VPN=0 +case "${1:-}" in + --senza-vpn) SENZA_VPN=1 ;; + "") ;; + *) echo "Uso: ./verifica.sh [--senza-vpn]" >&2 ; exit 2 ;; +esac + ROSSI=0 rosso () { male "$@" ; ROSSI=$((ROSSI + 1)) ; } @@ -24,17 +34,44 @@ if c_e docker ; then fi if c_e claude ; then ok "Claude Code" ; else attento "Claude Code non installato (non obbligatorio, ma ti guida)" ; fi +titolo "Gitea" +UTENTE="$(utente_gitea)" ; ESITO_GITEA=$? +if [ -n "$UTENTE" ] ; then + ok "token valido (utente $UTENTE)" + # Si chiede a git la credenziale che userebbe davvero: un gestore qualsiasi + # configurato non vuol dire che abbia quella giusta, e un token vecchio + # rimasto li' fa funzionare l'API ma non git clone. + GIT_PASSWORD="$(password_git_gitea)" + if [ "$GIT_PASSWORD" = "$(token_gitea)" ] ; then + ok "git entra su $GITEA_HOST con il tuo token" + elif [ -n "$GIT_PASSWORD" ] ; then + rosso "git usa un accesso a Gitea diverso dal tuo token (vecchio o di un altro): rilancia ./configura.sh, che lo sostituisce" + else + rosso "git non ha le credenziali di Gitea: rilancia ./configura.sh" + fi + unset GIT_PASSWORD +elif [ "$ESITO_GITEA" = "2" ] ; then + rosso "Gitea ($GITEA_URL) non risponde: controlla la connessione a internet e rilancia" +else + rosso "token Gitea mancante o non valido: rilancia ./configura.sh" +fi + titolo "AWS" # Chi e' la persona lo dice Gitea: una chiave AWS di un altro sul PC vuol dire -# che tutto quello che fa su AWS risulta fatto da lui. -UTENTE="$(utente_gitea)" -if c_e aws && IDENTITA="$(aws sts get-caller-identity --query Arn --output text 2> /dev/null)" ; then - if [ -n "$UTENTE" ] && [ "${IDENTITA##*/}" != "$UTENTE" ] ; then - rosso "la chiave AWS di questo PC e' di ${IDENTITA##*/}, non tua ($UTENTE): rilancia ./configura.sh, che la sostituisce" +# che tutto quello che fa su AWS risulta fatto da lui. Senza Gitea non si puo' +# dire che la chiave sia sua, e quindi non e' verde. +AWS_OPZ=(--cli-connect-timeout 10 --cli-read-timeout 15) +CONFLITTI="$(aws_variabili_in_conflitto)" +[ -n "$CONFLITTI" ] && attento "nel terminale sono impostate $CONFLITTI: la AWS CLI usa quelle, non la chiave di ~/.aws/credentials" +if c_e aws && IDENTITA="$(AWS_MAX_ATTEMPTS=1 aws sts get-caller-identity "${AWS_OPZ[@]}" --query Arn --output text 2> /dev/null)" && [ -n "$IDENTITA" ] ; then + if [ -z "$UTENTE" ] ; then + rosso "c'e' una chiave AWS (${IDENTITA#arn:aws:iam::}), ma finche' Gitea non va non posso dire che sia tua" + elif aws_e_di "$IDENTITA" "$UTENTE" ; then + ok "accesso: $UTENTE" else - ok "accesso: ${IDENTITA##*/}" + rosso "la chiave AWS di questo PC e' di ${IDENTITA#arn:aws:iam::}, non tua ($UTENTE nell'account OTO $AWS_ACCOUNT): rilancia ./configura.sh, che la sostituisce" fi - if aws ssm get-parameter --region "$AWS_REGIONE" --name "$SSM_PROVA" --query Parameter.Name --output text > /dev/null 2>&1 ; then + if AWS_MAX_ATTEMPTS=1 aws ssm get-parameter "${AWS_OPZ[@]}" --region "$AWS_REGIONE" --name "$SSM_PROVA" --query Parameter.Name --output text > /dev/null 2>&1 ; then ok "legge i segreti comuni su SSM" else rosso "non legge SSM: all'amministratore serve darti il ruolo di sviluppo" @@ -43,20 +80,12 @@ else rosso "AWS non configurato: rilancia ./configura.sh, che ti dice dove crearti la chiave nella console ($AWS_CONSOLE)" fi -titolo "Gitea" -if [ -n "$UTENTE" ] ; then - ok "token valido (utente $UTENTE)" - if grep -q "@$GITEA_HOST" "$HOME/.git-credentials" 2> /dev/null || [ -n "$(git config --global credential.helper)" ] ; then - ok "git sa come entrare su $GITEA_HOST" - else - rosso "git non ha le credenziali di Gitea: rilancia ./configura.sh" - fi -else - rosso "token Gitea mancante o non valido: rilancia ./configura.sh" -fi - titolo "Chiave SSH" if [ -f "$CHIAVE_SSH" ] ; then ok "$CHIAVE_SSH" ; else rosso "chiave mancante: rilancia ./configura.sh" ; fi +# I nomi con cui la cercano i deploy dei progetti +for ALTRA in $CHIAVI_SSH_PROGETTI ; do + [ -e "$ALTRA" ] || rosso "manca $ALTRA, che usano i deploy dei progetti: rilancia ./configura.sh" +done titolo "VPN e server" # La VPN si riconosce da una cosa sola: staging, che sta su rete privata, @@ -85,11 +114,13 @@ entra () { return 2 } if porta_aperta "$STAGING_HOST" 22 ; then - ok "VPN attiva (staging raggiungibile)" + ok "staging raggiungibile (la VPN e' accesa)" entra "$STAGING_NOME" ; ESITO=$? if [ "$ESITO" = "0" ] ; then ok "entri su staging ($STAGING_NOME)" elif [ "$ESITO" = "1" ] ; then rosso "staging non accetta la tua chiave: l'amministratore deve ancora aggiungerla" else rosso "staging ($STAGING_NOME): $MOTIVO" ; fi +elif [ "$SENZA_VPN" = "1" ] ; then + attento "staging non raggiungibile: senza VPN e' normale" else rosso "staging non raggiungibile: la VPN e' accesa?" fi