Sposób zgodny z POSIX, aby uzyskać nazwę użytkownika powiązaną z identyfikatorem użytkownika

23

Często chcę uzyskać nazwę logowania powiązaną z identyfikatorem użytkownika, a ponieważ okazało się, że jest to częsty przypadek użycia, postanowiłem napisać funkcję powłoki, aby to zrobić. Chociaż głównie używam dystrybucji GNU / Linux, staram się pisać skrypty, aby były jak najbardziej przenośne i sprawdzać, czy to, co robię, jest zgodne z POSIX.

Analizować /etc/passwd

Pierwsze podejście, które próbowałem, to parsowanie /etc/passwd(używanie awk).

awk -v uid="$uid" -F: '$3 == uid {print $1}' /etc/passwd

Jednak problem z tym podejściem polega na tym, że logowanie może nie być lokalne, np. Uwierzytelnianie użytkownika może odbywać się przez NIS lub LDAP.

Użyj getentpolecenia

Użycie getent passwdjest bardziej przenośne niż parsowanie, /etc/passwdponieważ wysyła również zapytania do nielokalnych baz danych NIS lub LDAP.

getent passwd "$uid" | cut -d: -f1

Niestety getentnarzędzie nie wydaje się być określane przez POSIX.

Użyj idpolecenia

id jest standardowym narzędziem POSIX do uzyskiwania danych o tożsamości użytkownika.

Implementacje BSD i GNU akceptują identyfikator użytkownika jako operand:

Oznacza to, że można go użyć do wydrukowania nazwy logowania powiązanej z identyfikatorem użytkownika:

id -nu "$uid"

Jednak podanie ID użytkownika jako argumentu operacji nie jest określone w POSIX; opisuje tylko użycie nazwy logowania jako operandu.

Łącząc wszystkie powyższe

Rozważałem połączenie powyższych trzech podejść w coś takiego:

get_username(){
    uid="$1"
    # First try using getent
    getent passwd "$uid" | cut -d: -f1 ||
        # Next try using the UID as an operand to id.
        id -nu "$uid" ||
        # As a last resort, parse `/etc/passwd`.
        awk -v uid="$uid" -F: '$3 == uid {print $1}' /etc/passwd
}

Jest to jednak niezgrabne, nieeleganckie i - co ważniejsze - nietrwałe; kończy działanie ze statusem niezerowym, jeśli identyfikator użytkownika jest nieprawidłowy lub nie istnieje. Zanim napisałem dłuższy i bardziej niezgrabny skrypt powłoki, który analizuje i przechowuje status wyjścia każdego wywołania polecenia, pomyślałem, że zapytam tutaj:

Czy istnieje bardziej elegancki i przenośny (zgodny z POSIX) sposób uzyskiwania nazwy logowania powiązanej z identyfikatorem użytkownika?

Anthony G - sprawiedliwość dla Moniki
źródło
10
Dla dodatkowej zabawy weź pod uwagę, że wiele nazw użytkowników może być mapowanych na ten sam identyfikator ...
Stephen Kitt
Problem z wieloma nazwami użytkowników odwzorowanymi na ten sam identyfikator w kontekście tego pytania polega na tym, że ani getentnie idzwróci niczego po pierwszym dopasowaniu; jedynym sposobem na znalezienie ich wszystkich jest wyliczenie wszystkich użytkowników, jeśli baza danych użytkowników na to pozwala. ( /etc/passwdOczywiście poszukuje prac dla zdefiniowanych tam użytkowników).
Stephen Kitt
1
Dzięki @StephenKitt Stworzyłem taki wpis w moim /etc/passwdi /etc/shadowdo przetestowania tego scenariusza i zweryfikowałem, że oba idi getent passwdzachowują się jak opisano. Jeśli w pewnym momencie skończę z systemem, w którym użytkownik ma wiele nazw, zrobię to samo co te narzędzia systemowe i po prostu traktuję pierwsze wystąpienie jako kanoniczną nazwę tego użytkownika.
Anthony G - sprawiedliwość dla Moniki
1
Czy POSIX w ogóle wymaga, aby identyfikator użytkownika był powiązany z nazwą użytkownika ? Każdy program działający jako root może wywoływać setuid(some_id)i nie ma wymagań, które some_idmogą być częścią bazy danych użytkowników. Przy takich rzeczach, jak przestrzenie nazw użytkowników w systemie Linux, może to okazać się paraliżującym założeniem dla twoich skryptów.
mosvy
1
@Filippos, który wydaje się być drogim sposobem wywoływania getpwuid()funkcji, która lsużywa do tłumaczenia identyfikatorów UID na nazwy logowania. Odpowiedź Gillesa jest bardziej bezpośrednim i skutecznym sposobem na osiągnięcie tego.
Anthony G - sprawiedliwość dla Moniki

Odpowiedzi:

14

Jednym z powszechnych sposobów jest sprawdzenie, czy żądany program istnieje i jest dostępny z twojego PATH. Na przykład:

get_username(){
  uid="$1"

  # First try using getent
  if command -v getent > /dev/null 2>&1; then 
    getent passwd "$uid" | cut -d: -f1

  # Next try using the UID as an operand to id.
  elif command -v id > /dev/null 2>&1 && \
       id -nu "$uid" > /dev/null 2>&1; then
    id -nu "$uid"

  # Next try perl - perl's getpwuid just calls the system's C library getpwuid
  elif command -v perl >/dev/null 2>&1; then
    perl -e '@u=getpwuid($ARGV[0]);
             if ($u[0]) {print $u[0]} else {exit 2}' "$uid"

  # As a last resort, parse `/etc/passwd`.
  else
      awk -v uid="$uid" -F: '
         BEGIN {ec=2};
         $3 == uid {print $1; ec=0; exit 0};
         END {exit ec}' /etc/passwd
  fi
}

Ponieważ POSIX idnie obsługuje argumentów UID, elifklauzula dla idmusi sprawdzać nie tylko, czy idjest w PATH, ale także czy będzie działać bezbłędnie. Oznacza to, że może działać iddwa razy, co na szczęście nie będzie miało zauważalnego wpływu na wydajność. Jest również możliwe, że oba idi awkbędzie działać z taką samą znikomym przeboju wydajności.

BTW, dzięki tej metodzie nie trzeba przechowywać danych wyjściowych. Uruchomiony zostanie tylko jeden z nich, więc tylko jeden wydrukuje wyjście funkcji.

cas
źródło
poradzić sobie z możliwością wielu nazw użytkowników mających ten sam identyfikator UID owinąć wszystko od ifsię fiw { ... } | head -n 1. tzn. upuść wszystko oprócz pierwszego dopasowania UID. ale to będzie oznaczało, że będziesz musiał przechwycić kod wyjścia dowolnego uruchomionego programu.
cas
Dziękuję za odpowiedź. Miałem nadzieję, że może istnieć jakieś inne narzędzie, z którym się nie spotkałem, ale jest to pomocne. Ponieważ nie mam dostępu do implementacji id, która nie akceptuje identyfikatora jako argumentu, pomyślałem, że testowanie jego statusu wyjścia może być problematyczne - jak odróżnić nieistniejącą nazwę logowania od identyfikatora UID, który nie istnieje Nazwa logowania może składać się wyłącznie ze znaków numerycznych: gnu.org/software/coreutils/manual/html_node/…
Anthony G - sprawiedliwość dla Moniki
1
Z każdą edycją funkcja staje się coraz bardziej niezawodna. :) W tym względzie prawdopodobnie użyłbym if command -v getent >/dev/null;zamiast if [ -x /usr/bin/getent ] ;przypadkowego, że te narzędzia mają inną ścieżkę.
Anthony G - sprawiedliwość dla Moniki
3
Tak. Regularnie używam command -vdo tego celu: pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html (chociaż testowałem go tylko z dashwbudowaną powłoką).
Anthony G - sprawiedliwość dla Moniki
1
@AnthonyGeoghegan Jeśli kiedykolwiek będziesz musiał pracować na starożytnych systemach, type foo >/dev/null 2>/dev/nulldziała na każdym sh, który kiedykolwiek widziałem. commandjest stosunkowo nowoczesny.
Gilles „SO- przestań być zły”
6

W POSIX nie ma nic, co by pomogło poza id. Próbowanie idparsowania i powrót do niego /etc/passwdjest prawdopodobnie tak przenośne, jak w praktyce.

BusyBox idnie akceptuje identyfikatorów użytkowników, ale systemy z BusyBox są zwykle autonomicznymi systemami osadzonymi, w których parsowanie /etc/passwdjest wystarczające.

W przypadku napotkania systemu innego niż GNU, w którym idnie akceptuje się identyfikatorów użytkowników, możesz także spróbować zadzwonić getpwuidprzez Perla, jeśli istnieje taka możliwość:

username=$(perl -e 'print((getpwuid($ARGV[0]))[0])) 2>/dev/null
if [ -n "$username" ]; then echo "$username"; return; fi

Lub Python:

if python -c 'import pwd, sys; print(pwd.getpwuid(int(sys.argv[1]))).pw_name' 2>/dev/null; then return; fi
Gilles „SO- przestań być zły”
źródło
2
Analiza /etc/passwdnie jest w ogóle przenośna i nie będzie działać w przypadku backendów plików innych niż passwd, takich jak LDAP.
R ..
Podoba mi się, ukradnę to
cas
1
@R .. Pytający jest tego świadomy, ta odpowiedź nie twierdzi inaczej, więc jaki jest sens twojego komentarza?
Gilles „SO- przestań być zły”
Dziękuję za tę odpowiedź. Zapewniam cię, że nie ma innego narzędzia, o którym nie wiedziałem. Wygląda na to, że POSIX określa standardową funkcję C do tłumaczenia UID na nazwę logowania, ale niekoniecznie odpowiednią komendę (inną niż id).
Anthony G - sprawiedliwość dla Moniki
2
Jako ostatnią awarię, sprawdź, czy w systemie jest kompilator ac, a następnie skompiluj dostarczoną owijkę getpwuid () ...
rackandboneman
5

POSIX określa getpwuidjako standardową funkcję C wyszukiwanie bazy danych użytkownika w celu znalezienia identyfikatora użytkownika, umożliwiając jego przetłumaczenie na nazwę logowania. Pobrałem kod źródłowy dla GNU coreutils i widzę, że ta funkcja jest używana w ich implementacji narzędzi takich jak idi ls.

Jako ćwiczenie do nauki napisałem ten szybki i brudny program C, który po prostu działa jako opakowanie dla tej funkcji. Pamiętaj, że nie programowałem w C od college'u (wiele lat temu) i nie zamierzam używać tego w produkcji, ale pomyślałem, że opublikuję go tutaj jako dowód koncepcji (jeśli ktoś chce go edytować , nie krępuj się):

#include <stdio.h>
#include <stdlib.h>  /* atoi */
#include <pwd.h>

int main( int argc, char *argv[] ) {
    uid_t uid;
    if ( argc >= 2 ) {
        /* NB: atoi returns 0 (super-user ID) if argument is not a number) */
        uid = atoi(argv[1]);
    }
    /* Ignore any other arguments after the first one. */
    else {
        fprintf(stderr, "One numeric argument must be supplied.\n");
        return 1;
    }

    struct passwd *pwd;
    pwd = getpwuid(uid);
    if (pwd) {
        printf("The login name for %d is: %s\n", uid, pwd->pw_name);
        return 0;
    }
    else {
        fprintf(stderr, "Invalid user ID: %d\n", uid);
        return 1;
    }
}

Nie miałem okazji przetestować go za pomocą NIS / LDAP, ale zauważyłem, że jeśli istnieje wiele wpisów dla tego samego użytkownika /etc/passwd, ignorowane są wszystkie oprócz pierwszego.

Przykładowe użycie:

$ ./get_user ""
The login name for 0 is: root

$ ./get_user 99
Invalid user ID: 99
Anthony G - sprawiedliwość dla Moniki
źródło
3

Ogólnie odradzam to robić. Odwzorowanie nazw użytkowników na identyfikatory użytkownika nie jest jeden do jednego, a założenia kodowania, które można przekonwertować z identyfikatora użytkownika, aby uzyskać nazwę użytkownika, doprowadzą do załamania. Na przykład często uruchamiam całkowicie wolne od rootów kontenery przestrzeni nazw użytkowników, dzięki czemu pliki passwdi groupw kontenerze mapują wszystkie nazwy użytkowników i grup na id 0; pozwala to na instalowanie pakietów do pracy bez chownawarii. Ale jeśli coś spróbuje przekonwertować 0 z powrotem na UID i nie dostanie tego, czego się spodziewa, nieuzasadnione. Dlatego w tym przykładzie zamiast konwertować i porównywać nazwy użytkowników, powinieneś przekonwertować na UID i porównać w tym miejscu.

Jeśli naprawdę potrzebujesz wykonać tę operację, może być możliwe wykonanie częściowo przenośne, jeśli jesteś rootem, tworząc plik tymczasowy, chownzapisując go w UID, a następnie używając lsdo odczytu i analizy nazwy właściciela. Ale po prostu zastosowałbym dobrze znane podejście, które nie jest ustandaryzowane, ale „przenośne w praktyce”, jak jedno z tych, które już znalazłeś.

Ale znowu, nie rób tego. Czasami trudno jest wysłać wiadomość.

R ..
źródło
1
Komentarze zostały usunięte. Chciałbym przypomnieć obu uczestnikom, że komentarze muszą mieć charakter obywatelski.
terdon