chiark / gitweb /
Group management sorted ?
[chiark-utils.git] / sync-accounts / sync-accounts
index 38822505decc56e59d5e4ce01e1577dde06a8936..d83f6f7002050213b834ca6a7c36856177c5a9ab 100755 (executable)
 #      account, and new local accounts will get the remote accounts'
 #      ids.
 #
+#  usergroups
+#  nousergroups
+#      Specifies whether local accounts are supposed to have
+#      corresponding groups.  If this is set then 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.  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> ...
+#  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:
@@ -86,9 +98,9 @@
 #      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'.
 
 use POSIX;
 
 $configfile= '/etc/sync-accounts';
 $def_createuser= 'sync-accounts-createuser';
 $ch_homebase= '/home';
+$opt_usergroups= 1;
+@groupglobs= [ '.*', 0 ];
+regroupglobs();
 
 END {
     for $x (@unlocks) {
@@ -131,6 +160,8 @@ while ($ARGV[0] =~ m/^-/) {
     }
 }
 
+for $h (@ARGV) { $wanthost{$h}= 1; }    
+
 open CF,"< $configfile" or die "$configfile: $!";
 
 sub fetchfile (\%$) {
@@ -167,6 +198,14 @@ sub diag ($) {
     print "$diagstr: $_[0]\n" or die $!;
 }
 
+sub regroupglobs () {
+    $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 +269,57 @@ 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 !$opt_usergroups;
+#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 syncuser ($$) {
     my ($lu,$ru) = @_;
 
+#print STDERR "syncuser($lu,$ru)\n";
     next unless $ch_doinghost;
+    return if $doneuser{$lu}++;
+    return if !length $ru;
     $diagstr= "user $lu from $ch_name!$ru";
 
     fetchown();
-#print STDERR "syncuser($lu,$ru)\n";
+#print STDERR "syncuser($lu,$ru) doing\n";
     fetchpasswd();
 
     if (!$rempasswd{$ru}) { diag("no remote entry"); return; }
@@ -245,7 +327,7 @@ 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; }
@@ -295,13 +377,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,14 +412,29 @@ 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;
+
+    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 +445,11 @@ sub banner () {
 }
 
 sub finish () {
+    for $h (keys %wanthost) {
+       die "host $h not in config file\n" if $wanthost{$h};
+    }
+    
+    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 +464,13 @@ 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 $!;
+           if ($file eq 'shadow') {
+               system "chown root.shadow $newfile"; $? and die $?;
+               chmod 0640, $newfile or die $!;
+           } else {
+               chown 0,0, $newfile or die $!;
+               chmod 0644, $newfile or die $!;
+           }
            rename $newfile, "/etc/$file" or die $!;
        }
     }
@@ -361,7 +486,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+)$/) {
@@ -382,15 +514,25 @@ while (<CF>) {
        } else {
            open STDOUT,">> $1" or die "$1: $!"; $|=1;
        }
-    } elsif (m/^(no|)(sameuid)$/) {
+    } elsif (m/^(no|)(sameuid|usergroups)$/) {
        eval "\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;" or die $@;
+    } elsif (m/^(no|)group\s+([-+.0-9a-zA-Z*?]+)$/) {
+       $yes= $1 ne 'no';
+       $_= $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];