Linuxのuseraddコマンドって

ちょいと、お客さんの案件で、数字のみで構成されるユーザIDをWindows,Unix(Solaris)混在環境で使用したいという話があって、LDAP ManagerでUnix連携してみたら数字だけのユーザIDを受け付けてくれなかった。
どうして?って事で、useraddコマンドのソースを覗いてみることにした。
まずは、useraddってどのパッケージに含まれているのか?

# rpm -qf /usr/sbin/useradd
shadow-utils-4.0.3-61.RHEL4

で、CentOSのサイトからソースを入手

# rpm -ivh shadow-utils-4.0.3-60.RHEL4.src.rpm

でインストール(あってるのかな?昔は--rebuildとかしていたみたいだけど)

# cd /usr/src/redhat/SOURCES/
# bunzip2 shadow-4.0.3.tar.bz2
# tar -xvf shadow-4.0.3.tar

でソースを展開
useraddコマンドの本体は、shadow-4.0.3/src/useradd.cにある。
この中で、

 user_name = argv[optind];
 if (!check_user_name (user_name)) {
     fprintf (stderr, _("%s: invalid user name '%s'\n"),
       Prog, user_name);
     exit (E_BAD_ARG);
 }

として、check_user_nameから0が帰ってくるとエラーと判断するらしい。
で、check_user_nameはshadow-4.0.3/libmisc/chkname.cにある。

int
check_user_name(const char *name)
{
#if HAVE_UTMPX_H
        struct utmpx ut;
#else
        struct utmp ut;
#endif

        /*
         * User names are limited by whatever utmp can
         * handle (usually max 8 characters).
         */
        if (strlen(name) > sizeof(ut.ut_user))
                return 0;

        return good_name(name);
}

となっていて、実際の文字種類の確認はgood_nameで実施している。
good_nameでは、

static int
good_name(const char *name)
{
        /*
         * User/group names must match [a-z_][a-z0-9_-]*
         */
        if (!*name || !((*name >= 'a' && *name <= 'z') || *name == '_'))
                return 0;

        while (*++name) {
                if (!((*name >= 'a' && *name <= 'z') ||
                    (*name >= '0' && *name <= '9') ||
                    *name == '_' || *name == '-' ||
                    (*name == '$' && *(name+1) == NULL)))
                        return 0;
        }

        return 1;
}

となっていて、先頭1文字は[a-z_]で、それ以降は[a-z0-9_\-]、最後の1文字になら$を含める仕様になっているようだ。
ちょいプロを書いて確認してみると

#include <stdio.h>
static int
good_name(const char *name)
{
        /*
         * User/group names must match [a-z_][a-z0-9_-]*
         */
        if (!*name || !((*name >= 'a' && *name <= 'z') || *name == '_'))
                return 0;

        while (*++name) {
                if (!((*name >= 'a' && *name <= 'z') ||
                    (*name >= '0' && *name <= '9') ||
                    *name == '_' || *name == '-' ||
                    (*name == '$' && *(name+1) == NULL)))
                        return 0;
        }

        return 1;
}
main(){
char *name1 = "12345";
char *name2 = "a12345";
char *name3 = "1a2345";
char *name4 = "_a12345";
char *name5 = "a12345$";
char *name6 = "a1234$5";
char *name7 = "a1234%5";
char *name8 = "_12345";
    good_name( name1 );
    printf ( "%s;ret %d\n",name1,good_name( name1 ));
    printf ( "%s;ret %d\n",name2,good_name( name2 ));
    printf ( "%s;ret %d\n",name3,good_name( name3 ));
    printf ( "%s;ret %d\n",name4,good_name( name4 ));
    printf ( "%s;ret %d\n",name5,good_name( name5 ));
    printf ( "%s;ret %d\n",name6,good_name( name6 ));
    printf ( "%s;ret %d\n",name7,good_name( name7 ));
    printf ( "%s;ret %d\n",name8,good_name( name8 ));
}
# ./a.out
12345;ret 0
a12345;ret 1
1a2345;ret 0
_a12345;ret 1
a12345$;ret 1
a1234$5;ret 0
a1234%5;ret 0
_12345;ret 1

となる。
が、実際にuseradd 11111とやっても正常に動作するし、ログインも出来ちゃうのはなぜ?
よーっく見てみると
/usr/src/redhat/SOURCES/配下には沢山のパッチが並んでいて、shadow-4.0.3-goodname.patchというのがある。
開いてみると

--- shadow-4.0.3/libmisc/chkname.c.goodname     2002-01-10 08:04:34.000000000 -0500
+++ shadow-4.0.3/libmisc/chkname.c      2004-12-03 16:56:02.931628451 -0500
@@ -22,16 +22,24 @@
 good_name(const char *name)
 {
        /*
-        * User/group names must match [a-z_][a-z0-9_-]*
+        * User/group names must match gnu e-regex:
+         *   [a-zA-Z0-9_.][a-zA-Z0-9_-.]{0,30}[a-zA-Z0-9_.$-]?
+         *
+         * as a non-POSIX, extension, allow "$" as the last char for
+         * sake of Samba 3.x "add machine script"
         */
-       if (!*name || !((*name >= 'a' && *name <= 'z') || *name == '_'))
+       if (!*name || !((*name >= 'a' && *name <= 'z')
+            || (*name >= 'A' && *name <= 'Z')
+            || (*name >= '0' && *name <= '9')
+            || *name == '_' || *name == '.'))
                return 0;

        while (*++name) {
-               if (!((*name >= 'a' && *name <= 'z') ||
-                   (*name >= '0' && *name <= '9') ||
-                   *name == '_' || *name == '-' ||
-                   (*name == '$' && *(name+1) == NULL)))
+               if (!(  (*name >= 'a' && *name <= 'z')
+                    || (*name >= 'A' && *name <= 'Z')
+                    || (*name >= '0' && *name <= '9')
+                    || *name == '_' || *name == '.' || *name == '-'
+                    || (*name == '$' && *(name + 1) == '\0')))
                        return 0;
        }

@@ -48,10 +56,9 @@
 #endif

        /*
-        * User names are limited by whatever utmp can
-        * handle (usually max 8 characters).
+        * User names are limited by whatever utmp can handle.
         */
-       if (strlen(name) > sizeof(ut.ut_user))
+       if (strlen(name) + 1 > sizeof(ut.ut_user))
                return 0;

        return good_name(name);

となっていて、どうやらsamba対応のために、かなり拡張されているようだ。
これで数字だけのユーザが出来てしまう理由がわかったよ。
良かった(^^;