Forma compatible con POSIX para obtener el nombre de usuario asociado con una ID de usuario

23

A menudo quiero obtener el nombre de inicio de sesión asociado con una ID de usuario y, debido a que se ha demostrado que es un caso de uso común, decidí escribir una función de shell para hacer esto. Si bien uso principalmente distribuciones GNU / Linux, trato de escribir mis scripts para que sean lo más portátiles posible y verificar que lo que estoy haciendo sea compatible con POSIX.

Analizar gramaticalmente /etc/passwd

El primer enfoque que intenté fue analizar /etc/passwd(usando awk).

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

Sin embargo, el problema con este enfoque es que los inicios de sesión pueden no ser locales, por ejemplo, la autenticación del usuario podría ser a través de NIS o LDAP.

Usa el getentcomando

El uso getent passwdes más portátil que el análisis, /etc/passwdya que también consulta bases de datos NIS o LDAP no locales.

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

Desafortunadamente, la getentutilidad no parece estar especificada por POSIX.

Usa el idcomando

id es la utilidad estandarizada POSIX para obtener datos sobre la identidad de un usuario.

Las implementaciones BSD y GNU aceptan una ID de usuario como un operando:

Esto significa que se puede usar para imprimir el nombre de inicio de sesión asociado con una ID de usuario:

id -nu "$uid"

Sin embargo, proporcionar ID de usuario como el operando no se especifica en POSIX; solo describe el uso de un nombre de inicio de sesión como operando.

Combinando todo lo anterior

Pensé en combinar los tres enfoques anteriores en algo como lo siguiente:

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
}

Sin embargo, esto es torpe, poco elegante y, lo que es más importante, no es robusto; sale con un estado distinto de cero si la ID de usuario no es válida o no existe. Antes de escribir un script de shell más largo y más grueso que analiza y almacena el estado de salida de cada invocación de comando, pensé en preguntar aquí:

¿Existe una forma más elegante y portátil (compatible con POSIX) de obtener el nombre de inicio de sesión asociado con una ID de usuario?

Anthony G - justicia para Monica
fuente
10
Para mayor diversión, considere que varios nombres de usuario pueden asignarse a la misma identificación ...
Stephen Kitt
El problema con los múltiples nombres de usuario asignados a la misma identificación en el contexto de esta pregunta es que getentni iddevolverá nada después de la primera coincidencia; la única forma de encontrarlos todos es enumerar a todos los usuarios, si la base de datos de usuarios lo permite. (Buscando /etc/passwdobras para usuarios definidos allí, obviamente.)
Stephen Kitt
1
Gracias @StephenKitt. Creé tal entrada en mi /etc/passwd y /etc/shadowpara probar este escenario y verifiqué ambas cosas idygetent passwd como usted describe. Si, en algún momento, termino usando un sistema en el que un usuario tiene varios nombres, haré lo mismo que estas utilidades del sistema y simplemente trataré la primera aparición como el nombre canónico para ese usuario.
Anthony G - justicia para Monica
1
¿POSIX requiere que se asocie una identificación de usuario con un nombre de usuario? en absoluto ? Cualquier programa que se ejecute como root puede llamar setuid(some_id), y no hay ningún requisito que some_idpueda ser parte de una base de datos de usuarios. Con cosas como los espacios de nombres de usuario en Linux, esto puede convertirse en una suposición paralizante para sus scripts.
mosvy
1
@Philippos que parece una forma costosa de llamar a la getpwuid()función que se lsutiliza para traducir UID a nombres de inicio de sesión. La respuesta de Gilles es una forma más directa y eficiente de lograr esto.
Anthony G - justicia para Monica

Respuestas:

14

Una forma común de hacer esto es probar si el programa que desea existe y está disponible en su PATH. Por ejemplo:

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
}

Debido a que POSIX idno admite argumentos de UID, la elifcláusula idtiene que probar no solo si idestá en la RUTA, sino también si se ejecutará sin error. Esto significa que puede ejecutarse iddos veces, lo que afortunadamente no tendrá un impacto notable en el rendimiento. También es posible que tanto idy awkse ejecutará, con la misma insignificante impacto en el rendimiento.

Por cierto, con este método, no hay necesidad de almacenar la salida. Solo se ejecutará uno de ellos, por lo que solo uno imprimirá la salida para que la función regrese.

cas
fuente
Para hacer frente a la posibilidad de que varios nombres de usuario tengan el mismo uid, envuelva todo de adentro ifa fiadentro { ... } | head -n 1. es decir, soltar todo menos el primer partido uid. pero eso significará que tendrá que capturar el código de salida de cualquier programa que se ejecute.
cas
Gracias por la respuesta. Esperaba que pudiera haber alguna otra utilidad que no hubiera encontrado, pero esto es útil. Como no tengo acceso a una implementación idque no acepte un ID como operando, pensé que probar su estado de salida podría ser problemático: cómo distinguir entre un nombre de inicio de sesión que no existe o un UID que no existe Es posible que un nombre de inicio de sesión consista solo en caracteres numéricos: gnu.org/software/coreutils/manual/html_node/…
Anthony G - justicia para Monica
1
Con cada edición, la función se vuelve más robusta. :) En esa nota, probablemente usaría en if command -v getent >/dev/null;lugar de if [ -x /usr/bin/getent ] ;la posibilidad de que estas utilidades tengan una ruta diferente.
Anthony G - justicia para Monica
3
Sí. Regularmente uso command -vpara este propósito: pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html (aunque solo lo he probado con el dashshell incorporado).
Anthony G - justicia para Monica
1
@AnthonyGeoghegan Si alguna vez tienes que trabajar en sistemas antiguos, type foo >/dev/null 2>/dev/nulltrabaja en todas las cosas que he visto. commandEs comparativamente moderno.
Gilles 'SO- deja de ser malvado' el
6

No hay nada en POSIX que pueda ayudar más que id. Intentar idy volver a analizar /etc/passwdes probablemente tan portátil como se pone en práctica.

BusyBox idno acepta ID de usuario, pero los sistemas con BusyBox suelen ser sistemas integrados autónomos donde el análisis /etc/passwdes suficiente.

En caso de que encuentre un sistema que idno sea GNU y no acepte ID de usuario, también puede intentar llamargetpwuid través de Perl, en caso de que esté disponible:

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

O Python:

if python -c 'import pwd, sys; print(pwd.getpwuid(int(sys.argv[1]))).pw_name' 2>/dev/null; then return; fi
Gilles 'SO- deja de ser malvado'
fuente
2
El análisis /etc/passwdno es portátil en absoluto, y no funcionará para backends que no sean archivos de contraseña como LDAP.
R ..
Me gusta, lo robaré
cas
1
@R .. El autor de la pregunta lo sabe, esta respuesta no dice lo contrario, entonces, ¿cuál es el punto de tu comentario?
Gilles 'SO- deja de ser malvado' el
Gracias por esta respuesta Estoy seguro de que no hay otra utilidad que no conociera. Parece que POSIX especifica una función C estándar para traducir el UID a un nombre de inicio de sesión, pero no necesariamente un comando correspondiente (que no sea id).
Anthony G - justicia para Monica
2
Como último recurso, verifique si hay un compilador de CA en el sistema, luego compile un contenedor getpwuid () suministrado ...
rackandboneman
5

POSIX especifica getpwuidcomo una función C estándar para buscar en la base de datos de usuario una ID de usuario que permita que la ID se traduzca a un nombre de inicio de sesión. Descargué el código fuente de GNU coreutils y puedo ver esta función siendo utilizada en su implementación de utilidades como idy ls.

Como ejercicio de aprendizaje, escribí este programa de C rápido y sucio para simplemente actuar como una envoltura para esta función. Tenga en cuenta que no he programado en C desde la universidad (hace muchos años) y no tengo la intención de usar esto en la producción, pero pensé en publicarlo aquí como prueba de concepto (si alguien quiere editarlo) , sentirse libre):

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

No tuve la oportunidad de probarlo con NIS / LDAP, pero noté que si hay varias entradas para el mismo usuario /etc/passwd, ignora todas menos la primera.

Ejemplo de uso:

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

$ ./get_user 99
Invalid user ID: 99
Anthony G - justicia para Monica
fuente
3

En general, recomendaría no hacer esto. El mapeo de los nombres de usuario a los uid no es uno a uno, y los supuestos de codificación que puede convertir de un uid para obtener un nombre de usuario van a romper las cosas. Por ejemplo, a menudo ejecuto contenedores de espacio de nombres de usuario completamente libres de raíz haciendo que passwdygroup archivos en el contenedor asignen todos los nombres de usuarios y grupos al id 0; Esto permite instalar paquetes para que funcionen sin chownfallar. Pero si algo intenta convertir 0 de nuevo a uid y no obtiene lo que espera, se romperá de forma gratuita. Entonces, en este ejemplo, en lugar de volver a convertir y comparar nombres de usuario, debe convertir a uids y comparar en ese espacio.

Si realmente necesita hacer esta operación, podría ser posible hacerlo de forma semi-portátil si es root, haciendo un archivo temporal, llevándolo chownal uid y luego usandols para volver a leer y analizar el nombre del propietario. Pero simplemente usaría un enfoque bien conocido que no está estandarizado sino que es "portátil en la práctica", como uno de los que ya encontró.

Pero de nuevo, no hagas esto. A veces, algo difícil de hacer es enviarte un mensaje.

R ..
fuente
1
Comentarios purgados. Me gustaría recordarles a ambos participantes que los comentarios deben ser civiles.
terdon