chiark / gitweb /
really: Document need to be in the "root" group as well. (This is better than removi...
[chiark-utils.git] / sync-accounts / sync-accounts
index 80d22269605bced7ffba50bdda7c5a2edd783d0f..cef131c78b8b7d3aa0e5d7b5a5cdec943a1bdc8d 100755 (executable)
 #!/usr/bin/perl
-# $Id: sync-accounts,v 1.17 2001-03-06 18:35:09 ian Exp $
+# This is part of sync-accounts, a tool for synchronising UN*X password data.
 #
-# Copyright (C)1999-2000 Ian Jackson <ian@davenant.greenend.org.uk>
-# Copyright (C)2000-2001 nCipher Corporation Ltd
+# sync-accounts is
+#  Copyright 1999-2000,2002 Ian Jackson <ian@davenant.greenend.org.uk>
+#  Copyright 2000-2001 nCipher Corporation Ltd
 #
-#  This is free software; you can redistribute it and/or modify it under
-#  the terms of the GNU General Public License as published by the Free
-#  Software Foundation; either version 2, or (at your option) any later
-#  version.
+#  sync-accounts is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License as
+#  published by the Free Software Foundation; either version 3, or (at
+#  your option) any later version.
 #
-#  This is distributed in the hope that it will be useful, but WITHOUT
-#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
-#  for more details.
+#  sync-accounts is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  General Public License for more details.
 #
 #  You should already have a copy of the GNU General Public License.
-#  If not, write to the Free Software Foundation, Inc., 59 Temple
-#  Place - Suite 330, Boston, MA 02111-1307, USA.
+#  If not, consult the Free Software Foundation's website at
+#  www.fsf.org, or the GNU Project website at www.gnu.org.
 #
-# usage: sync-accounts [-n] [-C<config-file>] [<host> ...]
-# options:
-#   -n     do not really do anything
-#   -C     alternative config file (default is /etc/sync-accounts)
-#   -q     display accounts synched, not synched, etc.
-# if no host(s) specified, does all
-# host(s) may not be specified with -q
-#
-# The config file consists of directives, one per line.  Leading and
-# trailing whitespace, blank lines and lines starting # are ignored.
-#
-# Some config file directives apply globally and should appear first:
-#
-#  lockpasswd link <suffix/filename>
-#  lockgroup link <suffix/filename>
-#      Specifies the lockfile suffix or pathname to use when editing
-#      the passwd and group files.  The value is a suffix if it does
-#      not start with `/'.  If set to /dev/null no locking is done.
-#
-#  lockpasswd runvia <program>
-#  lockgroup runvia <program>
-#      Lock by reinvoking ourselves via a program as EDITOR.
-#      (<program> would typically be vipw or vigr.)
-#
-#  lockpasswd none
-#  lockgroup none
-#      Do not lock.
-#
-#  logfile <filename>
-#      Append log messages to <filename> instead of stdout.
-#      Errors still go to stderr.
-#
-#  localformat bsd|std
-#      Specifies the local password file is in the relevant format:
-#      `std' is the standard V7 password file (with a SysV- style
-#      /etc/shadow if one is detected at run-time); `bsd' is the weird
-#      BSD4.4 master.passwd format, and should be used only with
-#      `lockpasswd runvia vipw'.  The default is `std'.
-#
-# Some config file directives set options which may be different at
-# different points in the file.  The most-recently-seen value is used
-# at each point:
-#
-#  uidmin <min>
-#  uidmax <max>
-#  homebase <pathname>
-#      When an account is to be created, a uid/gid will be chosen
-#      which is one higher than the highest currently in use (except
-#      that ids outside the range <min>-<max> are ignored and will
-#      never be used).  The default home directory location is
-#      <pathname>/<username>.
-#
-#  sameuid
-#  nosameuid
-#      Specifies whether uids are supposed to match.  The default is
-#      nosameuid.  When sameuid is on, it is an error for the uid or
-#      gid of a local account not to match the corresponding remote
-#      account, and new local accounts will get the remote accounts'
-#      ids.
-#
-#  usergroups
-#  nousergroups
-#  defaultgid <gid>
-#      Specifies whether local accounts are supposed to have
-#      corresponding groups, or all be part of a particular group.  If
-#      usergroups is set, when a new account is created, the
-#      corresponding per-user group will be created as well, and
-#      per-user groups are created for existing accounts if necessary
-#      (if account creation is enabled).  If the gid or group name for
-#      a per-user group is already taken for a different group name or
-#      gid this will be logged, and processing of that account will be
-#      inhibited, but it is not a fatal error.  If defaultgid is used,
-#      then newly-created accounts will be made a part of that group,
-#      and the groups of existing accounts will be left alone.  If
-#      nousergroups is specified then no new accounts can be created,
-#      and existing accounts' groups will be left alone.  The default
-#      is `usergroups'.
-#
-#  createuser
-#  createuser <commandname>
-#  nocreateuser
-#      Specifies whether accounts found on the remote host should be
-#      created if necessary, and what command to run to do the
-#      creation (eg, setup of home directory).  The default is
-#      nocreateuser.  If createuser is specified without a commandname
-#      then sync-accounts-createuser is used.  The command is found on
-#      the PATH if necessary.  Either sameuid, or both uidmin and
-#      uidmax, must be specified, if accounts are to be created.
-#
-#      The command (which will be run with sh -c) must at least create
-#      the new account's home directory.  The passwd and group entries
-#      will not have been set up.  The following environment variables
-#      will be set, giving details about the account to be created:
-#        SYNCUSER_CREATE_USER
-#        SYNCUSER_CREATE_UID
-#        SYNCUSER_CREATE_GID
-#        SYNCUSER_CREATE_COMMENT
-#        SYNCUSER_CREATE_HOME
-#        SYNCUSER_CREATE_SHELL
-#      If it chooses, the script may modify the password entry which
-#      will be added to the system, by outputting a replacement
-#      password file entry.  (The password field of that is ignored.)
-#      If the script outputs a line which does not contain a : then
-#      the account will not be created after all.
-#
-#  group <glob-pattern>
-#  nogroup <glob-pattern>
-#      Specifies that the membership of the local groups specified
-#      should be adjusted or not adjusted whenever account data for a
-#      particular user is copied, so that the account will be a member
-#      of the affected group locally iff it is a member of the same
-#      group on the remote host.  The most recently-encountered
-#      glob-pattern for a particular group takes effect.  The default
-#      is `nogroups *'.
-#
-#  defaultshell <pathname>
-#      If, when creating an account, the remote account's shell is not
-#      available on the local system, this value will be used.  The
-#      default is /bin/sh.
-#
-# Some config file directives are per-host, and should appear before
-# any directives which actually modify accounts:
-#
-#  host <shorthostname>
-#      Starts a host's section.  This resets the per-host parameters
-#      to the defaults.  The shorthostname need not be the host's
-#      official name in any sense.  If sync-accounts is invoked with
-#      host names on the command line they are compared with the
-#      shorthostnames.
-#
-#  getpasswd <command>
-#  getgroup <command>
-#      Commands to run on the local host to get the passwd, shadow and
-#      group data for the host in question.  getpasswd must be
-#      specified if user data is to be transferred; getgroup must be
-#      specified if group data is to be transferred.
-#
-#  getshadow <command>
-#      Specifies that shadow file data is to be used (by default,
-#      password information is found from the output of getpasswd).
-#      The command should emit shadow data in the format specified by
-#      shadow(5) on Linux.  getshadow should not be specified without
-#      getpasswd.
-#
-#  remoteformat std|bsd
-#      Specifies the format of the output of `getpasswd'.  `std' is
-#      standard V7 passwd file format (optionally augmented by the use
-#      of a shadow file fetched with getshadow).  `bsd' is the weird
-#      BSD4.4 master.passwd format (and getshadow should not normally
-#      be used with `remoteformat bsd').  The default is `std'.
-#
-# Some configuration file directives specify that account data is to
-# transferred from the current host.  They should appear as the last
-# thing(s) in a host section:
-#
-#  user <username> [remote=<remoteusername>]
-#      Specifies that account data should be copied for local user
-#      <username> from the remote account <remoteusername> (assumed to
-#      be the same as <username> if not specified).  The account
-#      password, comment field, and shell will be copied
-#      unconditionally.  If sameuid is specified the uid will be
-#      checked.
-#
-#  users <ruidmin>-<ruidmax>
-#      Specifies that all remote users whose uid is in the given range
-#      are to be copied to corresponding local user accounts.
-#
-#  nouser <username>
-#      Specifies that data for <username> is _not_ to be copied, even
-#      if subsequent user or users directives suggest that it should
-#      be.
-#
-#   (A note is made when a `user', `users' or `nouser' directive is
-#   encountered for a particular account, and no subsequent directives
-#   for that account will take effect.)
-#
-#  addhere
-#      This directive has no effect on `sync-accounts'.  However, it
-#      is used as a placeholder by `grab-account': new accounts for
-#      creation are inserted just before `addhere'.
-#
-# Finally, the config file must finish with:
-# 
-#  end
+# $Id: sync-accounts,v 1.25 2007-09-21 21:21:15 ianmdlvl Exp $
 
 use POSIX;
 
@@ -381,9 +199,13 @@ sub fetchownfile (\@$$$$) {
     open O,"$fn_use" or die "$fn_use ($fn_str): $!";
     while (<O>) {
        chomp;
-       $record= [ split(/\:/,$_,-1) ];
-       die "$fn_emsg:$.:wrong number of fields:\`$_'\n"
-           unless @$record == $nfields;
+       if (m/^\#/ || !m/\S/) {
+           $record= $_;
+       } else {
+           $record= [ split(/\:/,$_,-1) ];
+           die "$fn_emsg:$.:wrong number of fields:\`$_'\n"
+               unless @$record == $nfields;
+       }
        push @$ary_ref, $record;
     }
     close O or die "$fn_use ($fn_str): $!";
@@ -401,7 +223,7 @@ sub regroupglobs () {
     for $g (@groupglobs) { $ggfunc.= "    m/^$g->[0]\$/ ? $g->[1] :\n"; }
     $ggfunc.= "    die;\n};\n1;\n";
 #print STDERR "$ggfunc\n";
-    eval $ggfunc or die "$ggfunc // $@";
+    must_eval($ggfunc);
 }
 
 sub fetchown () {
@@ -432,6 +254,7 @@ sub fetchown () {
 sub checkuid ($$) {
     my ($useuid,$foruser) = @_;
     for $e (@ownpasswd) {
+       next unless ref $e;
        if ($e->[$PW_USER] ne $foruser && $e->[$PW_UID] == $useuid) {
            diag("uid clash with $e->[$PW_USER] (uid $e->[$PW_UID])");
            return 0;
@@ -440,16 +263,20 @@ sub checkuid ($$) {
     return 1;
 }
 
+sub must_eval ($) {
+    eval $_[0] or die "$_[0] // $@";
+}
+
 sub copyfield ($$$$) {
     my ($file,$entry,$field,$value) = @_;
-    eval "\$ary_ref= \\\@own$file; 1;" or die $@;
+    must_eval("\$ary_ref= \\\@own$file; 1;");
 #print STDERR "copyfield($file,$entry,$field,$value)\n";
     for $e (@$ary_ref) {
 #print STDERR "copyfield($file,$entry,$field,$value) $e->[0] $e->[field] ".join(':',@$e)."\n";
        next unless $e->[0] eq $entry;
        next if $e->[$field] eq $value;
        $e->[$field]= $value;
-       eval "\$modified$file= 1; 1;" or die $@;
+       must_eval("\$modified$file= 1; 1;");
     }
 }
 
@@ -488,6 +315,7 @@ sub syncusergroup ($$) {
     $ugfound=0;
     
     for $e (@owngroup) {
+       next unless ref $e;
        $samename= $e->[$GR_GROUP] eq $lu;
        $sameid= $e->[$GR_GID] eq $luid;
        next unless $samename || $sameid;
@@ -537,7 +365,7 @@ sub syncuser ($$) {
 
     if ($display) {
        for $e (@ownpasswd) {
-           next unless $e->[$PW_USER] eq $lu;
+           next unless ref $e && $e->[$PW_USER] eq $lu;
            hosthead("from $ch_name");
            print ($lu eq $ru ? " $lu" : " $lu($ru)") or die $!;
            print "<DUPLICATE>" if $displaydone{$lu}++;
@@ -557,7 +385,7 @@ sub syncuser ($$) {
        return;
     }
 
-    if (!grep($_->[$PW_USER] eq $lu, @ownpasswd)) {
+    if (!grep(ref $_ && $_->[$PW_USER] eq $lu, @ownpasswd)) {
        if (!length $opt_createuser) { diag("account creation not enabled"); return; }
        if ($no_act) { diag("-n specified; not creating account"); return; }
 
@@ -572,6 +400,7 @@ sub syncuser ($$) {
        
            $useuid= $ch_uidmin;
            for $e ($defaultgid==-1 ? (@ownpasswd, @owngroup) : (@ownpasswd)) {
+               next unless ref $e;
                $tuid= $e->[$PW_UID]; next if $tuid<$useuid || $tuid>$ch_uidmax;
                if ($tuid==$ch_uidmax) {
                    diag("uid (or gid?) $ch_uidmax used, cannot create users");
@@ -630,7 +459,7 @@ sub syncuser ($$) {
     }
 
     for $e (@ownpasswd) {
-       next unless $e->[$PW_USER] eq $lu;
+       next unless ref $e && $e->[$PW_USER] eq $lu;
        syncusergroup($lu,$e->[$PW_UID]) or return;
     }
 
@@ -638,7 +467,7 @@ sub syncuser ($$) {
     $rgid= $rempasswd{$ru}->[$REM_GID];
     if ($opt_sameuid && checkuid($ruid,$lu)) {
        for $e (@ownpasswd) {
-           next unless $e->[$PW_USER] eq $lu;
+           next unless ref $e && $e->[$PW_USER] eq $lu;
            $luid= $e->[$PW_UID]; $lgid= $e->[$PW_GID];
            die "$diagstr: local uid $luid, remote uid $ruid\n" if $luid ne $ruid;
            die "$diagstr: local gid $lgid, remote gid $rgid\n" if $lgid ne $rgid;
@@ -646,7 +475,7 @@ sub syncuser ($$) {
     }
 
 #print STDERR "syncuser($lu,$ru) exists $own_haveshadow\n";
-    if ($own_haveshadow && grep($_->[$PW_USER] eq $lu, @ownshadow)) {
+    if ($own_haveshadow && grep(ref $_ && $_->[$PW_USER] eq $lu, @ownshadow)) {
 #print STDERR "syncuser($lu,$ru) shadow $rempasswd{$ru}->[$REM_PW]\n";
        copyfield('shadow',$lu,$SP_PW, $rempasswd{$ru}->[$REM_PW]);
     } else {
@@ -662,6 +491,7 @@ sub syncuser ($$) {
 
     if (!$nogroups) {
        for $e (@owngroup) {
+           next unless ref $e;
            $tgroup= $e->[$GR_GROUP];
 #print STDERR "syncuser($lu,$ru) group $tgroup\n";
            next unless &wantsyncgroup($tgroup);
@@ -694,6 +524,8 @@ sub banner () {
 }
 
 sub finish () {
+    my ($record);
+
     for $h (keys %wanthost) {
        die "host $h not in config file\n" if $wanthost{$h};
     }
@@ -701,13 +533,14 @@ sub finish () {
     if ($display) {
 #print STDERR "\n\nfinish display=$display pw=$pw\n\n";
        for $e (@ownpasswd) {
+           next unless ref $e;
            $tu= $e->[$PW_USER];
            $tuid= $e->[$PW_UID];
            next if $displaydone{$tu};
            $tpw= $e->[$PW_PW];
 #print STDERR ">$tu|$tpw<\n";
            for $e2 (@ownshadow) {
-               next unless $e2->[$SP_USER] eq $tu;
+               next unless ref $e2 && $e2->[$SP_USER] eq $tu;
                $tpw= $e2->[$SP_PW]; last;
            }
            $tpw= length($tpw)>=13 ? 1 : length($tpw) ? -1 : 0;
@@ -728,8 +561,8 @@ sub finish () {
     umask 077;
     for $file (qw(passwd shadow group)) {
        $realfile= $file{$file,$PW_format};
-       eval "\$modified= \$modified$file; \$data_ref= \\\@own$file;".
-           " \$fetched= \$own_fetched$file; 1;" or die $@;
+       must_eval("\$modified= \$modified$file; \$data_ref= \\\@own$file;".
+                 " \$fetched= \$own_fetched$file; 1;");
        next if !$modified;
        die $file unless $fetched;
        banner();
@@ -742,7 +575,8 @@ sub finish () {
        }
        open NF,"> $newfile" or die "$newfile: $!";
        for $e (@$data_ref) {
-           print NF join(':',@$e),"\n" or die $!;
+           $record= ref $e ? join(':',@$e) : $e;
+           print NF $record,"\n" or die $!;
        }
        close NF or die $!;
        system 'diff','-U0','--label',$realfile,$newfileinst,
@@ -760,8 +594,8 @@ sub finish () {
 
 while (<CF>) {
     chomp;
-    next if m/^\#/ || !m/\S/;
     s/^\s*//; s/\s*$//;
+    next if m/^\#/ || !m/\S/;
     finish() if m/^end$/;
     if (m/^host\s+(\S+)$/) {
        $ch_name= $1;
@@ -777,19 +611,19 @@ while (<CF>) {
        }
        fields_fmt('REM','std');
     } elsif (m/^(getpasswd|getshadow|getgroup)\s+(.*\S)$/) {
-       eval "\$ch_$1= \$2; 1;" or die $@;
+       must_eval("\$ch_$1= \$2; 1;");
     } elsif (m/^(local|remote)format\s+(\w+)$/) {
        fields_fmt($1 eq 'local' ? 'PW' :
                   $1 eq 'remote' ? 'REM' : die,
                   $2);
     } elsif (m/^lock(passwd|group)\s+(runvia|link)\s+(\S+)$/) {
-       eval "\$ch_lock_$1= \$3; \$ch_lockstyle_$1= \$2; 1;" or die $@;
+       must_eval("\$ch_lock_$1= \$3; \$ch_lockstyle_$1= \$2; 1;");
     } elsif (m/^lock(passwd|group)\s+(none)$/) {
-       eval "\$ch_lockstyle_$1= \$2; 1;" or die $@;
+       must_eval("\$ch_lockstyle_$1= \$2; 1;");
     } elsif (m,^(homebase|defaultshell)\s+(/\S+)$,) {
-       eval "\$ch_$1= \$2; 1;" or die $@;
+       must_eval("\$ch_$1= \$2; 1;");
     } elsif (m/^(uidmin|uidmax)\s+(\d+)$/ && $2>0) {
-       eval "\$ch_$1= \$2; 1;" or die $@;
+       must_eval("\$ch_$1= \$2; 1;");
     } elsif (m/^createuser$/) {
        $opt_createuser= $def_createuser;
     } elsif (m/^nocreateuser$/) {
@@ -804,7 +638,7 @@ while (<CF>) {
            print "would log to $1\n" or die $!;
        }
     } elsif (m/^(no|)(sameuid)$/) {
-       eval "\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;" or die $@;
+       must_eval("\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;");
     } elsif (m/^usergroups$/) {
        $defaultgid= -1;
     } elsif (m/^nousergroups$/) {
@@ -815,7 +649,7 @@ while (<CF>) {
        $yes= $1 eq 'no' ? 0 : 1;
        $_= $2;
        @groupglobs=() if $_ eq '*';
-       s/[-+._]/\\$1/g;
+       s/[-+._]/\\$&/g;
        s/\*/\.\*/g;
        s/\?/\./g;
        unshift @groupglobs, [ $_, $yes ];