Git Credential Helper

Git ciągle mnie zaskakuje modularnością i rozszerzalnością swojej architektury. W pracy mam standardowe zablokowane porty SSH, które są używane przez większość popularnych serwisów hostujących serwery gita (Giltab, Github); również mój prywatny serwer stoi na niestandardowym porcie, który padł ofiarą działu IT. W związku z tym, nie mogę korzystać z logowania przy pomocy kluczy SSH i od kilku lat, kiedy tylko chcę uzyskać dostęp do prywatnego repozytorium lub spushować jakąś zmianę, muszę korzystać z komunikacji przez HTTPS i wpisywać swoją nazwę użytkownika i hasło.

Słabe rozwiązanie: cache

Jakiś czas temu nieco ułatwiłem sobie to żmudne zadanie ustawiając cache’owanie haseł:

$ git config --global credential.helper 'cache --timeout=3600'

Okazuje się jednak, że nie do końca pojąłem pełną złożoność tego ustawienia. cache jest bowiem jedynie programem rozprowadzanym wraz z gitem (git-credential-cache), który jest wywoływany z tego powodu, że został ustawiony jako wartość pola helper sekcji [credential] w pliku konfiguracyjnym gita. Może to być jednak dowolne inne polecenie, które wypisze na standardowe wyjście odpowiednio sformatowane dane logowania.

Dobre rozwiązanie: własny pomocnik

Na co dzień do zarządzania hasłami korzystam z programu pass, który szyfrowanie haseł deleguje do gpg, a sam jedynie zarządza zaszyfrowanymi plikami z hasłami. Jest to niezwykle wygodne narzędzie z tego względu, że w naturalny sposób integruje się z systemem operacyjnym – dla każdego programu, który jest w stanie wywołać dowolne polecenie shella możemy w prosty sposób napisać manager haseł. Dzięki temu, że pass korzysta bezpośrednio z gpg, możemy użyć gpg-agenta, który, w zależności od konfiguracji, będzie w odpowiedni sposób pytał i przechowywał hasła kluczy PGP.

Wobec powyższego, naturalnym dla mnie przykładem będzie napisanie prostego skryptu-pomocnika konwertującego wyjście programu pass na format akceptowany przez gita.

W skrócie: git akceptuje listę klucz=wartość, gdzie każda para zakończona jest znakiem nowej linii, zaś pole klucz przyjmuje jedną z następujących wartości: protocol, host, path, username, password. Lista par nie musi zawierać wszystkich wartości, szczególnie że nie zawsze mają one sens. W naszym przypadku zwrócenie pól username i password w zupełności wystarcza.

Przykład owej listy, którą powinien zwracać helper:

username=johnny
password=haxorpass

Format passa zaś jest wymuszony architekturą samego programu; pierwsza linijka to hasło, pozostałe dane to metadane, które generalnie są ignorowane przez passa jako takie, jednak można je wypisać na ekran przy pomocy komendy pass show <nazwa>:

haxorpass
username: johnny

Wobec bijącego po oczach podobieństwa, zamiana jednego formatu na drugi to prosta transformacja, którą można wykonać przy pomocy jednego obrotu seda. Zanim jednak podam kod programu, musimy się zaznajomić ze sposobem, w jaki git będzie go uruchamiał.

credential.helper

Jak już na początku wspomniałem, git wykona komendę zapisaną w polu helper w sekcji [credential] konfiguracji1. Nie jest to jednak pełna informacja, gdyż przed wykonaniem git wykonuje pewną transformację wpisu:

  1. jeśli wpis rozpoczyna się wykrzyknikiem, to wszystko po nim jest traktowane jako kod shella i zostaje użyte tak jak zostało wpisane; ini helper = !f() { echo "password=abc" }; f

  2. jeśli wpis jest ścieżką bezwzględną, zostanie on użyty jako wywoływane polecenie tak, jak został wpisany; ini helper = /usr/local/bin/foobar

  3. w przeciwnym wypadku (gdy wpis jest ścieżką względną), zostanie on wywołany jako git credential-$NAME (czyli w $PATH musi być dostępny program o nazwie “git-credential-$NAME”). ini helper = foobar

Do wynikowego polecenia git następnie dopisuje, jako ostatni argument, typ operacji:

  • get – prośba o zwrócenie danych logowania
  • store – prośba o zapisanie danych logowania wewnątrz programu
  • erase – prośba o usunięcie zapisanych danych logowania

Na przykład, dla helper = foo git wywoła polecenie git credential-foo get, ale dla helper = !foo wywoła foo get.

Skrypt

Uzbrojeni w tę wiedzę możemy stworzyć skrypt i odpowiednio skonfigurować gita. Zacznijmy od konfiguracji gita. Jeśli skrypt nazwiemy git-credential-pass i umieścimy go w $PATH, to dla interesujących nas serwisów możemy dodać następujące pola konfiuracyjne:

[credential "https://gitlab.com"]
    helper = pass my-credentials/gitlab.com

[credential "https://github.com"]
    helper = pass my-credentials/github.com

Sam skrypt zaś to proste filtrowanie wyjścia passa, o którym mówiłem już wcześniej. Należy umieścić go gdzieś w $PATH lub $GIT_EXEC_PATH:

#!/bin/sh

test "$2" = "get" || exit 0
pass show "$1" | sed -e '1 s/^\(.*\)$/password=\1/' \
                     -e '2,$ s/username: \?/username=/'

Więcej szczegółów, jak zwykle, w dokumentacji2.


  1. Git sprawdzi najpierw lokalną konfigurację beżącego repozytorium zapisaną w pliku .git/config, a następnie konfigurację globalną w ~/.gitconfig

  2. Dokumentację znalazłem jedynie na serwerze Linuksa – ta z git-scm.com jest niedostępna.