chiark / gitweb /
Really keep perms.
[chiark-utils.git] / sync-accounts / sync-accounts
index 38822505decc56e59d5e4ce01e1577dde06a8936..1cbb452ef88b9f614b67a1570b875d2c25b7d2a3 100755 (executable)
@@ -1,8 +1,10 @@
 #!/usr/bin/perl
+# $Id: sync-accounts,v 1.11 1999-01-03 02:19:27 ian Exp $
 # 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
 #
 # The config file consists of directives, one per line.  Leading and
@@ -14,7 +16,7 @@
 #  lockgroup <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 `/'.
+#      not start with `/'.  If set to /dev/null no locking is done.
 #
 #  logfile <filename>
 #      Append log messages to <filename> instead of stdout.
 #      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
 #      the PATH if necessary.  Either sameuid, or both uidmin and
 #      uidmax, must be specified, if accounts are to be created.
 #
-#  groups [!]<glob-pattern> ...
+#      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:
+#        SYNCACCOUNT_CREATE_USER
+#        SYNCACCOUNT_CREATE_UID
+#        SYNCACCOUNT_CREATE_GID
+#        SYNCACCOUNT_CREATE_COMMENT
+#        SYNCACCOUNT_CREATE_HOME
+#        SYNCACCOUNT_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 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 specification is read from left to right,
-#      until a match on a glob pattern is found.  Then, the membership
-#      is adjusted iff the glob pattern was not preceded by `!'.
-#      THIS FEATURE IS NOT YET IMPLEMENTED.
+#      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 *'.
 #
 # Some config file directives are per-host, and should appear before
 # any directives which actually modify accounts:
 #      shadow(5) on Linux.  getshadow should not be specified without
 #      getpasswd.
 #
-# Some configuration file directives specify that account or group
-# data is to transferred from the current host.  They should appear as
-# the last thing(s) in a host section:
+# 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
 #  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
 
 use POSIX;
 
 $configfile= '/etc/sync-accounts';
 $def_createuser= 'sync-accounts-createuser';
 $ch_homebase= '/home';
+$defaultgid= -1; # -1 => usergroups; -2 => nousergroups
+@groupglobs= [ '.*', 0 ];
+regroupglobs();
 
 END {
     for $x (@unlocks) {
@@ -126,11 +183,17 @@ while ($ARGV[0] =~ m/^-/) {
        $configfile= $';
     } elsif (m/^-n$/) {
        $no_act= 1;
+       $display= 0;
+    } elsif (m/^-q$/) {
+       $no_act= 1;
+       $display= 1;
     } else {
        die "unknown option $_\n";
     }
 }
 
+for $h (@ARGV) { $wanthost{$h}= 1; }    
+
 open CF,"< $configfile" or die "$configfile: $!";
 
 sub fetchfile (\%$) {
@@ -167,6 +230,17 @@ sub diag ($) {
     print "$diagstr: $_[0]\n" or die $!;
 }
 
+sub regroupglobs () {
+    $nogroups= (@groupglobs == 1 &&
+               $groupglobs[0]->[0] eq '.*' &&
+               !$groupglobs[0]->[1]);
+    $ggfunc= "sub wantsyncgroup {\n  \$_= \$_[0];\n  return\n";
+    for $e (@groupglobs) { $ggfunc.= "    m/^$e->[0]\$/ ? $e->[1] :\n"; }
+    $ggfunc.= "    die;\n};\n1;\n";
+#print STDERR "$ggfunc\n";
+    eval $ggfunc or die "$ggfunc // $@";
+}
+
 sub fetchown () {
     if (!$own_fetchedpasswd) {
        fetchownfile(@ownpasswd,'/etc/passwd',$ch_lockpasswd);
@@ -230,14 +304,77 @@ sub fetchpasswd () {
     }
 }
 
+sub fetchgroup () {
+    return if $ch_fetchedgroup;
+    die "$configfile:$.: getgroup not specified for host $ch_name\n"
+       unless length $ch_getgroup;
+    fetchfile(%remgroup,"$ch_getgroup |");
+}
+
+sub syncusergroup ($$) {
+    my ($lu,$luid) = @_;
+
+    return 1 if $defaultgid != -1;
+#print STDERR "syncusergroup($lu,$luid)\n";
+    $ugfound=0;
+    
+    for $e (@owngroup) {
+       $samename= $e->[0] eq $lu;
+       $sameid= $e->[2] eq $luid;
+       next unless $samename || $sameid;
+       if (!$samename || !$sameid) {
+           diag("local group $e->[0] ($e->[2]) mismatch vs. local user $lu ($luid)");
+           return 0;
+       }
+       if ($ugfound) {
+           diag("per-user group $lu ($luid) duplicated");
+           return 0;
+       }
+       $ugfound=1;
+    }
+
+    return 1 if $ugfound;
+
+    if (!length $opt_createuser) {
+       diag("account creation not enabled, not creating per-user group");
+       return 0;
+    }
+    push @owngroup, [ $lu,'x',$luid,'' ];
+    $modifiedgroup= 1;
+    return 1;
+}
+
+sub hosthead ($) {
+    my ($th) = @_;
+    return if $hostheaddone eq $th;
+    print "\n\n" or die $! if length $hostheaddone;
+    print "==== $th ====\n" or die $!;
+    $hostheaddone= $th;
+}
+
 sub syncuser ($$) {
     my ($lu,$ru) = @_;
 
+#print STDERR "syncuser($lu,$ru)\n";
     next unless $ch_doinghost;
-    $diagstr= "user $lu from $ch_name!$ru";
+    return if $doneuser{$lu}++;
+    return if !length $ru;
 
     fetchown();
-#print STDERR "syncuser($lu,$ru)\n";
+
+    if ($display) {
+       for $e (@ownpasswd) {
+           next unless $e->[0] eq $lu;
+           hosthead("from $ch_name");
+           print ($lu eq $ru ? " $lu" : " $lu($ru)") or die $!;
+           print "<DUPLICATE>" if $displaydone{$lu}++;
+       }
+       return;
+    }
+    
+    $diagstr= "user $lu from $ch_name!$ru";
+
+#print STDERR "syncuser($lu,$ru) doing\n";
     fetchpasswd();
 
     if (!$rempasswd{$ru}) { diag("no remote entry"); return; }
@@ -245,30 +382,33 @@ sub syncuser ($$) {
        diag("remote account disabled in shadow");
        return;
     }
-    
+
     if (!grep($_->[0] eq $lu, @ownpasswd)) {
        if (!length $opt_createuser) { diag("account creation not enabled"); return; }
        if ($no_act) { diag("-n specified; not creating account"); return; }
 
        if ($opt_sameuid) {
            $useuid= $rempasswd{$ru}->[2];
+           $usegid= $rempasswd{$ru}->[3];
        } else {
-           length $ch_uidmin or die "no uidmin specified, cannot create users";
-           length $ch_uidmax or die "no uidmax specified, cannot create users";
-           $ch_uidmin<$ch_uidmax or die "uidmin>=uidmax specified, cannot create users";
+           die "nousergroups specified, cannot create users\n" if $defaultgid==-2;
+           length $ch_uidmin or die "no uidmin specified, cannot create users\n";
+           length $ch_uidmax or die "no uidmax specified, cannot create users\n";
+           $ch_uidmin<$ch_uidmax or die "uidmin>=uidmax, cannot create users\n";
        
            $useuid= $ch_uidmin;
-           for $e (@ownpasswd, @owngroup) {
+           for $e ($defaultgid==-1 ? (@ownpasswd, @owngroup) : (@ownpasswd)) {
                $tuid= $e->[2]; next if $tuid<$useuid || $tuid>$ch_uidmax;
                if ($tuid==$ch_uidmax) {
-                   diag("uid/gid $ch_uidmax used, cannot create users");
+                   diag("uid (or gid?) $ch_uidmax used, cannot create users");
                    return;
                }
                $useuid= $tuid+1;
            }
+           $usegid= $defaultgid==-1 ? $useuid : $defaultgid;
        }
        
-       @newpwent= ($lu,'x',$useuid,$useuid,$rempasswd{$ru}->[4],
+       @newpwent= ($lu,'x',$useuid,$usegid,$rempasswd{$ru}->[4],
                    "$ch_homebase/$lu",$rempasswd{$ru}->[6]);
        
        defined($c= open CU,"-|") or die $!;
@@ -295,13 +435,31 @@ sub syncuser ($$) {
        die "$opt_createuser: bad output: $_\n" if @newpwent!=7 or $newpwent[0] ne $lu;
        checkuid($newpwent[2],$lu) or return;
        if ($own_haveshadow) {
-           push(@ownshadow,[ $lu, $newpwent[1], $cdays, 0,99999,7,'','' ]);
+           push(@ownshadow,[ $lu, $newpwent[1], $cdays, 0,99999,7,'','','' ]);
            $newpwent[1]= 'x';
            $modifiedshadow= 1;
        }
+       syncusergroup($lu,$newpwent[2]) or return;
        push @ownpasswd,[ @newpwent ];
        $modifiedpasswd= 1;
     }
+
+    for $e (@ownpasswd) {
+       next unless $e->[0] eq $lu;
+       syncusergroup($lu,$e->[2]) or return;
+    }
+
+    $ruid= $rempasswd{$ru}->[2];
+    $rgid= $rempasswd{$ru}->[3];
+    if ($opt_sameuid && checkuid($ruid,$lu)) {
+       for $e (@ownpasswd) {
+           next unless $e->[0] eq $lu;
+           $luid= $e->[2]; $lgid= $e->[3];
+           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;
+       }
+    }
+
 #print STDERR "syncuser($lu,$ru) exists $own_haveshadow\n";
     if ($own_haveshadow && grep($_->[0] eq $lu, @ownshadow)) {
 #print STDERR "syncuser($lu,$ru) shadow $rempasswd{$ru}->[1]\n";
@@ -312,13 +470,30 @@ sub syncuser ($$) {
     }
     copyfield('passwd',$lu,4, $rempasswd{$ru}->[4]); # comment
     copyfield('passwd',$lu,6, $rempasswd{$ru}->[6]); # shell
-    if ($opt_sameuid && checkuid($rempasswd{$ru}->[2],$lu)) {
-       for $e (@ownpasswd) {
-           next unless $e->[0] eq $lu;
-           $lid= $e->[2]; $rid= $rempasswd{$ru}->[2];
-           die "$diagstr: local uid $lid, remote uid $rid\n" if $lid ne $rid;
-           $lid= $e->[3]; $rid= $rempasswd{$ru}->[3];
-           die "$diagstr: local gid $lid, remote gid $rid\n" if $lid ne $rid;
+
+    if (!$nogroups) {
+       for $e (@owngroup) {
+           $tgroup= $e->[0];
+#print STDERR "syncuser($lu,$ru) group $tgroup\n";
+           next unless &wantsyncgroup($tgroup);
+#print STDERR "syncuser($lu,$ru) group $tgroup yes\n";
+           fetchgroup();
+           if (!exists $remgroup{$tgroup}) {
+               diag("group $tgroup: not on remote host");
+               next;
+           }
+           $inremote= grep($_ eq $ru, split(/\,/,$remgroup{$tgroup}->[3]));
+           $cusers= $e->[3]; $inlocal= grep($_ eq $lu, split(/\,/,$cusers));
+           if ($inremote && !$inlocal) {
+               $cusers.= ',' if length $cusers;
+               $cusers.= $lu;
+           } elsif ($inlocal && !$inremote) {
+               $cusers= join(',', grep($_ ne $lu, split(/\,/, $cusers)));
+           } else {
+               next;
+           }
+           $e->[3]= $cusers;
+           $modifiedgroup= 1;
        }
     }
 }
@@ -330,6 +505,34 @@ sub banner () {
 }
 
 sub finish () {
+    for $h (keys %wanthost) {
+       die "host $h not in config file\n" if $wanthost{$h};
+    }
+
+    if ($display) {
+       for $pw (1,-1,0) {
+#print STDERR "\n\nfinish display=$display pw=$pw\n\n";
+           for $e (@ownpasswd) {
+               $tu= $e->[0];
+               next if $displaydone{$tu};
+               $tpw= $e->[1];
+               for $e2 (@ownshadow) {
+                   next unless $e2->[0] eq $tu;
+                   $tpw= $e2->[1]; last;
+               }
+               $tpw= length($tpw)==13 ? 1 : length($tpw) ? -1 : 0;
+               next unless $pw == $tpw;
+               hosthead($pw == 1 ? "unsynched normal account" :
+                        $pw == 0 ? "unsynched, passwordless" :
+                        "unsynched, no logins");
+               print " $e->[0]" or die $!;
+           }
+       }
+       print "\n\n" or die $! if $hostheaddone;
+       exit 0;
+    }
+    
+    umask 077;
     for $file (qw(passwd shadow group)) {
        eval "\$modified= \$modified$file; \$data_ref= \\\@own$file;".
            " \$fetched= \$own_fetched$file; 1;" or die $@;
@@ -344,8 +547,9 @@ sub finish () {
        close NF or die $!;
        system "diff -U0 /etc/$file $newfile"; $?==256 or die $?;
        if (!$no_act) {
-           chown 0,0, $newfile or die $!;
-           chmod 0644, $newfile or die $!;
+           (@stats= stat "/etc/$file") or die "$file: $!";
+           chown $stats[4],$stats[5], $newfile or die $!;
+           chmod $stats[2] & 07777, $newfile or die $!;
            rename $newfile, "/etc/$file" or die $!;
        }
     }
@@ -361,7 +565,14 @@ while (<CF>) {
        $ch_name= $1;
        $ch_getpasswd= $ch_getgroup= $ch_getshadow= '';
        $ch_fetchedpasswd= $ch_fetchedgroup;
-       $ch_doinghost= !@ARGV || grep($_ eq $ch_name, @ARGV);
+       if (!@ARGV) {
+           $ch_doinghost= 1;
+       } elsif (exists $wanthost{$ch_name}) {
+           $wanthost{$ch_name}= 0;
+           $ch_doinghost= 1;
+       } else {
+           $ch_doinghost= 0;
+       }
     } elsif (m/^(getpasswd|getshadow|getgroup)\s+(.*\S)$/) {
        eval "\$ch_$1= \$2; 1;" or die $@;
     } elsif (m/^(lockpasswd|lockgroup)\s+(\S+)$/) {
@@ -377,26 +588,43 @@ while (<CF>) {
     } elsif (m/^createuser\s+(\S+)$/) {
        $opt_createuser= $1;
     } elsif (m/^logfile\s+(.*\S)$/) {
-       if ($no_act) {
-           print "would log to $1\n" or die $!;
-       } else {
+       if (!$no_act) {
            open STDOUT,">> $1" or die "$1: $!"; $|=1;
+       } elsif (!$display) {
+           print "would log to $1\n" or die $!;
        }
     } elsif (m/^(no|)(sameuid)$/) {
        eval "\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;" or die $@;
+    } elsif (m/^usergroups$/) {
+       $defaultgid= -1;
+    } elsif (m/^nousergroups$/) {
+       $defaultgid= -2;
+    } elsif (m/^defaultgid\s+(\d+)$/) {
+       $defaultgid= $1;
+    } elsif (m/^(no|)group\s+([-+.0-9a-zA-Z*?]+)$/) {
+       $yes= $1 eq 'no' ? 0 : 1;
+       $_= $2;
+       @groupglobs=() if $_ eq '*';
+       s/[-+._]/\\$1/g;
+       s/\*/\.\*/g;
+       s/\?/\./g;
+       unshift @groupglobs, [ $_, $yes ];
+       regroupglobs();
     } elsif (m/^user\s+(\S+)$/) {
        syncuser($1,$1);
-    } elsif (m/^groups\s+(.*\S)$/) {
     } elsif (m/^user\s+(\S+)\s+remote\=(\S+)$/) {
        syncuser($1,$2);
+    } elsif (m/^nouser\s+(\S+)$/) {
+       syncuser($1,'');
     } elsif (m/^users\s+(\d+)\-(\d+)$/) {
-       $tmin= $1; $tmax= $2;
+       $tmin= $1; $tmax= $2; $except= $3;
        fetchpasswd();
        for $k (keys %rempasswd) {
            $tuid= $rempasswd{$k}->[2];
            next if $tuid<$1 or $tuid>$2;
            syncuser($k,$k);
        }
+    } elsif (m/^addhere$/) {
     } else {
        die "$configfile:$.: unknown directive\n";
     }