Moyen compatible POSIX pour obtenir le nom d'utilisateur associé à un ID utilisateur

23

Je souhaite souvent obtenir le nom de connexion associé à un ID utilisateur et comme il s'est avéré qu'il s'agit d'un cas d'utilisation courant, j'ai décidé d'écrire une fonction shell pour ce faire. Bien que j'utilise principalement des distributions GNU / Linux, j'essaie d'écrire mes scripts pour être aussi portables que possible et de vérifier que ce que je fais est compatible POSIX.

Analyser /etc/passwd

La première approche que j'ai essayée a été d'analyser /etc/passwd(en utilisant awk).

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

Cependant, le problème avec cette approche est que les connexions peuvent ne pas être locales, par exemple, l'authentification des utilisateurs pourrait être via NIS ou LDAP.

Utilisez la getentcommande

L'utilisation getent passwdest plus portable que l'analyse /etc/passwdcar elle interroge également les bases de données NIS ou LDAP non locales.

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

Malheureusement, l' getentutilitaire ne semble pas être spécifié par POSIX.

Utilisez la idcommande

id est l'utilitaire normalisé POSIX pour obtenir des données sur l'identité d'un utilisateur.

Les implémentations BSD et GNU acceptent un ID utilisateur comme opérande:

Cela signifie qu'il peut être utilisé pour imprimer le nom de connexion associé à un ID utilisateur:

id -nu "$uid"

Cependant, fournir des ID utilisateur en tant qu'opérande n'est pas spécifié dans POSIX; il décrit uniquement l'utilisation d'un nom de connexion comme opérande.

Combiner tout ce qui précède

J'ai envisagé de combiner les trois approches ci-dessus en quelque chose comme ce qui suit:

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
}

Cependant, c'est maladroit, inélégant et - plus important encore - pas robuste; il se termine avec un état différent de zéro si l'ID utilisateur n'est pas valide ou n'existe pas. Avant d'écrire un script shell plus long et plus maladroit qui analyse et stocke l'état de sortie de chaque appel de commande, j'ai pensé demander ici:

Existe-t-il un moyen plus élégant et portable (compatible POSIX) d'obtenir le nom de connexion associé à un ID utilisateur?

Anthony G - justice pour Monica
la source
10
Pour plus de plaisir, considérez que plusieurs noms d'utilisateur peuvent correspondre au même identifiant ...
Stephen Kitt
Le problème avec plusieurs noms d'utilisateurs mappés sur le même identifiant dans le contexte de cette question est que getentni idne retournera quoi que ce soit après la première correspondance; la seule façon de les trouver tous est d'énumérer tous les utilisateurs, si la base de données des utilisateurs le permet. (Recherche dans les /etc/passwdœuvres pour les utilisateurs définis là, évidemment.)
Stephen Kitt
1
Merci @StephenKitt j'ai créé une telle entrée dans mon /etc/passwdet /etc/shadowde tester ce scénario et vérifié que les deux idet getent passwdse comportent comme vous le décrivez. Si, à un moment donné, je finis par utiliser un système où un utilisateur a plusieurs noms, je ferai la même chose que ces utilitaires système et traiterai simplement la première occurrence comme le nom canonique de cet utilisateur.
Anthony G - justice pour Monica le
1
Est-ce que nécessite un Posix ID utilisateur d'être associé à un nom d'utilisateur du tout ? Tout programme s'exécutant en tant que root peut appeler setuid(some_id), et aucune exigence ne some_idpeut faire partie d'une base de données utilisateur. Avec des choses comme les espaces de noms d'utilisateurs sous Linux, cela peut devenir une hypothèse paralysante pour vos scripts.
mosvy
1
@Philippos qui semble être un moyen coûteux d'appeler la getpwuid()fonction qui lsutilise pour traduire les UID en noms de connexion. La réponse de Gilles est une manière plus directe et efficace d'y parvenir.
Anthony G - justice pour Monica

Réponses:

14

Une façon courante de le faire est de tester si le programme souhaité existe et est disponible depuis votre PATH. Par exemple:

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
}

Comme POSIX idne prend pas en charge les arguments UID, la elifclause for iddoit tester non seulement si elle se idtrouve dans le PATH, mais également si elle s'exécutera sans erreur. Cela signifie qu'il peut s'exécuter iddeux fois, ce qui, heureusement, n'aura pas d'impact notable sur les performances. Il est également possible que les deux idet awksoient exécutés, avec les mêmes performances négligeables.

BTW, avec cette méthode, il n'est pas nécessaire de stocker la sortie. Un seul d'entre eux sera exécuté, donc un seul imprimera la sortie pour la fonction à retourner.

cas
la source
pour faire face à la possibilité de multiples noms d' utilisateur ayant le même uid, envelopper tout de ifà fidans { ... } | head -n 1. c'est-à-dire supprimer tout sauf le premier match uid. mais cela signifie que vous devrez capturer le code de sortie de tout programme exécuté.
ČAS
Merci d'avoir répondu. J'espérais qu'il pourrait y avoir un autre utilitaire que je n'avais pas rencontré mais c'est utile. Parce que je n'ai pas accès à une implémentation idqui n'accepte pas un ID comme opérande, j'ai pensé que tester son état de sortie pourrait être problématique - comment faire la différence entre un nom de connexion qui n'existe pas ou un UID qui n'existe pas. Il est possible qu'un nom de connexion ne soit composé que de caractères numériques: gnu.org/software/coreutils/manual/html_node/…
Anthony G - justice pour Monica
1
À chaque édition, la fonction devient plus robuste. :) Sur cette note, j'utiliserais probablement if command -v getent >/dev/null;au lieu de if [ -x /usr/bin/getent ] ;la chance que ces utilitaires aient un chemin différent.
Anthony G - justice pour Monica
3
Oui. J'utilise régulièrement command -và cette fin: pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html (bien que je ne l'ai jamais testé qu'avec le dashshell intégré).
Anthony G - justice pour Monica
1
@AnthonyGeoghegan Si vous devez travailler sur des systèmes anciens, type foo >/dev/null 2>/dev/nullfonctionne sur tous les sh que j'ai jamais vus. commandest relativement moderne.
Gilles 'SO- arrête d'être méchant' le
6

Il n'y a rien dans POSIX qui pourrait aider à part ça id. Essayer idet se replier sur l'analyse /etc/passwdest probablement aussi portable que possible dans la pratique.

BusyBox idn'accepte pas les ID utilisateur, mais les systèmes avec BusyBox sont généralement des systèmes intégrés autonomes où l'analyse /etc/passwdest suffisante.

Si vous rencontrez un système non GNU qui idn'accepte pas les ID utilisateur, vous pouvez également essayer d'appeler getpwuidvia Perl, au cas où il serait disponible:

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

Ou Python:

if python -c 'import pwd, sys; print(pwd.getpwuid(int(sys.argv[1]))).pw_name' 2>/dev/null; then return; fi
Gilles 'SO- arrête d'être méchant'
la source
2
L'analyse /etc/passwdn'est pas du tout portable et ne fonctionnera pas pour les backends non-passwd-file comme LDAP.
R ..
J'aime ça, je vais le voler
cas
1
@R .. Le demandeur en est conscient, cette réponse ne prétend pas le contraire, alors quel est l'intérêt de votre commentaire?
Gilles 'SO- arrête d'être méchant'
Merci pour cette réponse. Je suis rassuré qu'il n'y a pas d'autre utilité que je ne connaissais pas. Il semble que POSIX spécifie une fonction C standard pour traduire l'UID en un nom de connexion mais pas nécessairement une commande correspondante (autre que id).
Anthony G - justice pour Monica
2
En dernier recours, vérifiez s'il y a un compilateur alternatif sur le système, puis compilez un wrapper getpwuid () fourni ...
rackandboneman
5

POSIX spécifie en getpwuidtant que fonction C standard pour rechercher dans la base de données utilisateur un ID utilisateur permettant de traduire l'ID en un nom de connexion. J'ai téléchargé le code source de GNU coreutils et je peux voir que cette fonction est utilisée dans leur implémentation d'utilitaires tels que idet ls.

Comme exercice d'apprentissage, j'ai écrit ce programme C rapide et sale pour simplement agir comme un wrapper pour cette fonction. Gardez à l'esprit que je n'ai pas programmé en C depuis l'université (il y a de nombreuses années) et je n'ai pas l'intention de l'utiliser en production mais j'ai pensé que je le posterais ici comme preuve de concept (si quelqu'un veut le modifier) , N'hésitez pas):

#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;
    }
}

Je n'ai pas eu la chance de le tester avec NIS / LDAP mais j'ai remarqué que s'il y a plusieurs entrées pour le même utilisateur /etc/passwd, il ignore tout sauf le premier.

Exemple d'utilisation:

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

$ ./get_user 99
Invalid user ID: 99
Anthony G - justice pour Monica
la source
3

En général, je déconseille de faire cela. Le mappage des noms d'utilisateur aux UID n'est pas un à un, et les hypothèses de codage que vous pouvez convertir à partir d' un UID pour obtenir un nom d'utilisateur vont casser les choses. Par exemple, j'exécute souvent des conteneurs d'espace de noms d'utilisateurs entièrement sans racine en faisant en sorte que les fichiers passwdet groupdans le conteneur mappent tous les noms d'utilisateurs et de groupes à l'id 0; cela permet d'installer des packages pour fonctionner sans chownéchec. Mais si quelque chose essaie de reconvertir 0 en uid et n'obtient pas ce qu'il attend, il se cassera gratuitement. Donc, dans cet exemple, au lieu de reconvertir et de comparer les noms d'utilisateur, vous devez convertir en uids et comparer dans cet espace.

Si vous avez vraiment besoin de faire cette opération, il peut être possible de le faire de manière semi-portative si vous êtes root, en créant un fichier temporaire, chownen l'intégrant à l'UID, puis en utilisant lspour lire et analyser le nom du propriétaire. Mais j'utiliserais simplement une approche bien connue qui n'est pas standardisée mais "portable dans la pratique", comme l'une de celles que vous avez déjà trouvées.

Mais encore une fois, ne faites pas ça. Parfois, il est difficile de vous envoyer un message.

R ..
la source
1
Commentaires purgés. Je voudrais rappeler aux deux participants que les commentaires doivent être civils.
terdon