X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=chiark-utils.git;a=blobdiff_plain;f=sync-accounts%2Fsync-accounts;h=5e26a988e4371e60e075bda8b52b3f94dbd68823;hp=38822505decc56e59d5e4ce01e1577dde06a8936;hb=d9d8b1a736946ae91c521133dc32d5e1d6b5e506;hpb=f586956c8ecf9fe477aa682ac69b0490dfc25e49 diff --git a/sync-accounts/sync-accounts b/sync-accounts/sync-accounts index 3882250..5e26a98 100755 --- a/sync-accounts/sync-accounts +++ b/sync-accounts/sync-accounts @@ -1,123 +1,109 @@ #!/usr/bin/perl -# usage: sync-accounts [-n] [-C] [ ...] -# options: -# -n do not really do anything -# -C alternative config file (default is /etc/sync-accounts) -# if no host(s) specified, does all +# This is part of sync-accounts, a tool for synchronising UN*X password data. # -# The config file consists of directives, one per line. Leading and -# trailing whitespace, blank lines and lines starting # are ignored. +# sync-accounts is +# Copyright 1999-2000,2002 Ian Jackson +# Copyright 2000-2001 nCipher Corporation Ltd # -# Some config file directives apply globally and should appear first: +# 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. # -# lockpasswd -# lockgroup -# 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 `/'. +# 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. # -# logfile -# Append log messages to instead of stdout. -# Errors still go to stderr. +# 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. # -# 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 -# uidmax -# homebase -# 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 - are ignored and will -# never be used). The default home directory location is -# /. -# -# 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. -# -# createuser -# createuser -# 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. -# -# groups [!] ... -# 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. -# -# Some config file directives are per-host, and should appear before -# any directives which actually modify accounts: -# -# host -# 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 -# getgroup -# 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 -# 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. -# -# 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: -# -# user [remote=] -# Specifies that account data should be copied for local user -# from the remote account (assumed to -# be the same as if not specified). The account -# password, comment field, and shell will be copied -# unconditionally. If sameuid is specified the uid will be -# checked. -# -# users - -# Specifies that all remote users whose uid is in the given range -# are to be copied to corresponding local user accounts. +# $Id: sync-accounts,v 1.24 2007-09-21 20:59:02 ianmdlvl Exp $ use POSIX; $configfile= '/etc/sync-accounts'; $def_createuser= 'sync-accounts-createuser'; $ch_homebase= '/home'; +$ch_defaultshell= '/bin/sh'; +$defaultgid= -1; # -1 => usergroups; -2 => nousergroups +@groupglobs= [ '.*', 0 ]; +regroupglobs(); + +$file{'passwd','std'}= 'passwd'; +$file{'shadow','std'}= 'shadow'; +$file{'group','std'}= 'group'; + +$file{'passwd','bsd'}= 'master.passwd'; +$file{'shadow','bsd'}= 'shadow-non-existent'; +$file{'group','bsd'}= 'group'; + +@fields_pw_std= qw(USER PW UID GID COMMENT HOME SHELL); +@fields_pw_bsd= qw(USER PW UID GID CLASS CHANGE EXPIRE COMMENT HOME SHELL); +fields_fmt('PW','std'); +fields('GR',qw(GROUP PW GID USERS)); +fields('SP',qw(USER PW DAYSCHGD DAYSFIX DAYSEXP DAYSWARN DAYSEXPDIS DAYSDISD RESERVED)); +# The name field had better always be field 0 ! END { - for $x (@unlocks) { - unlink $x or warn "unable to unlock by removing $x: $!\n"; + foreach $x (@unlocks) { + ($fn, $style, $arg) = @$x; + &{ "unlock_$style" } ( $fn,$arg ); } } +sub fields { + my ($pfx,@l) = @_; + my ($i, $v, $vn); + foreach $v (@l) { $vn= "${pfx}_$v"; $$vn = $i++; } + $vn= "${pfx}_fields"; $$vn= $i; +} + +sub fields_fmt ($$) { + my ($pfx,$fmt) = @_; + my ($vn); + $vn= "fields_pw_$fmt"; + die "unknown format $fmt\n" unless defined @$vn; + fields($pfx,@$vn); + $vn= "${pfx}_format"; + $$vn= $fmt; +} + +sub newentry { + my ($pfx,$name,@field_val_list) = @_; + my (@rv, $vn, $fn, $v, $i); + @rv= (); + $vn= "${pfx}_fields"; + for ($i=0; $i<$$vn; $i++) { $rv[$i]= ''; } + die "@field_val_list ?" if @field_val_list % 2; + $rv[0] = $name; + while (@field_val_list) { + ($fn,$v,@field_val_list) = @field_val_list; + $vn= "${pfx}_$fn"; +#print STDERR ">$fn|$v|$vn|$$vn<\n"; + $rv[$$vn]= $v; + } + return @rv; +} + $|=1; $cdays= int(time/86400); -@envs_passwd= qw(USER x UID GID COMMENT HOME SHELL); +@envs_createuser= qw(USER UID GID COMMENT HOME SHELL); + +if (@ARGV == 1 && length $ENV{'SYNC_ACCOUNTS_RUNVIA_INFO'}) { + @na= map { + s/\%([0-9a-f][0-9a-f])/ pack("C", hex $1) /ge; + $_; + } split(/\:/, $ENV{'SYNC_ACCOUNTS_RUNVIA_INFO'}); + delete $ENV{'SYNC_ACCOUNTS_RUNVIA_INFO'}; + $ta= shift @na; + $ENV{"SYNC_ACCOUNTS_RUNVIA_LOCKEDGOT_$ta"} = $ARGV[0]; + @ARGV= @na; +} + +@orgargv= @ARGV; while ($ARGV[0] =~ m/^-/) { $_= shift @ARGV; @@ -126,11 +112,19 @@ 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"; } } +die "hosts must not be specified with -q\n" if @ARGV && $display; + +for $h (@ARGV) { $wanthost{$h}= 1; } + open CF,"< $configfile" or die "$configfile: $!"; sub fetchfile (\%$) { @@ -146,43 +140,112 @@ sub fetchfile (\%$) { close G; $? and die "$ch_name: $get_str: exit code $?\n"; } -sub fetchownfile (\@$$) { - my ($ary_ref,$fn_str,$lock_str) = @_; - die "$configfile:$.: lockfile name for $fn_str not". - " defined (use lockpasswd/lockgroup)\n" unless length $lock_str; - if ($lock_str ne '/dev/null' && !$no_act) { - $lock_str= "$fn_str$lock_str" unless $lock_str =~ m,^/,; - link $fn_str,$lock_str or die "cannot lock $fn_str by creating $lock_str: $!\n"; - push @unlocks,$lock_str; +sub lockstyle_ ($$) { + my ($fn,$lock) = @_; + + die "$configfile:$.: locking mechanism for $fn not". + " defined (use lockpasswd/lockgroup)\n"; +} + +sub abslock (@) { + my ($fn,$lock) = @_; + + $fn= "/etc/$fn"; + $lock= "$fn$lock" unless $lock =~ m,^/,; + return ($fn,$lock); +} + +sub lock_none ($$) { return (abslock(@_))[0]; } +sub unlock_none ($$) { } + +sub lock_link ($$) { + my ($fn,$lock) = abslock(@_); + link $fn,$lock or die "cannot lock $fn by creating $lock: $!\n"; + return $fn; +} +sub unlock_link ($$) { + my ($fn,$lock) = abslock(@_); + unlink $lock or warn "unable to unlock by removing $lock: $!\n"; +} + +sub lock_runvia ($$) { + my ($fn,$lock) = @_; + my ($evn); + $evn= "SYNC_ACCOUNTS_RUNVIA_LOCKEDGOT_$fn"; + return $ENV{$evn} if exists $ENV{$evn}; + + @na= map { + s/\W/ sprintf("%%%02x", unpack("C", $&)) /ge; + $_; + } ($fn,@orgargv); + $ENV{'SYNC_ACCOUNTS_RUNVIA_INFO'}= join(":", @na); + $ENV{'EDITOR'}= $0; + delete $ENV{'VISUAL'}; + exec $lock; die "cannot lock $fn by executing $lock: $!\n"; +} +sub unlock_runvia ($$) { } + +sub fetchownfile (\@$$$$) { + my ($ary_ref,$fn_str,$nfields,$style,$lock_arg) = @_; + my ($fn_use, $record, $fn_emsg); + $fn_emsg= $fn_str; + if (!$no_act) { + $fn_use= &{ "lock_$style" } ($fn_str, $lock_arg); + push @unlocks, [ $fn_str, $style, $lock_arg ]; + $savebackto{$fn_str}= $fn_use; + } else { + $fn_use= $fn_emsg= "/etc/".$file{$fn_str,$PW_format}; } - open O,"$fn_str" or die "$fn_str: $!"; + open O,"$fn_use" or die "$fn_use ($fn_str): $!"; while () { chomp; - push @$ary_ref, [ split(/\:/,$_,-1) ]; + 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_str: $!"; + close O or die "$fn_use ($fn_str): $!"; } 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 $g (@groupglobs) { $ggfunc.= " m/^$g->[0]\$/ ? $g->[1] :\n"; } + $ggfunc.= " die;\n};\n1;\n"; +#print STDERR "$ggfunc\n"; + must_eval($ggfunc); +} + sub fetchown () { if (!$own_fetchedpasswd) { - fetchownfile(@ownpasswd,'/etc/passwd',$ch_lockpasswd); - if (defined stat('/etc/shadow')) { +#print STDERR ">$PW_fields<\n"; + fetchownfile(@ownpasswd,'passwd', + $PW_fields, $ch_lockstyle_passwd, $ch_lock_passwd); + $shadowfile= $file{'shadow',$PW_format}; + if (stat("/etc/$shadowfile")) { $own_haveshadow= 1; $own_fetchedshadow= 1; - fetchownfile(@ownshadow,'/etc/shadow','/dev/null'); + fetchownfile(@ownshadow,'shadow',$SP_fields,'none',''); } elsif ($! == &ENOENT) { $own_haveshadow= 0; } else { - die "unable to check for /etc/shadow: $!\n"; + die "unable to check for /etc/$shadowfile: $!\n"; } $own_fetchedpasswd= 1; } if (!$own_fetchedgroup) { - fetchownfile(@owngroup,'/etc/group',$ch_lockgroup); + fetchownfile(@owngroup,'group',$GR_fields, + $ch_lockstyle_group, $ch_lock_group); $own_fetchedgroup= 1; } #print STDERR "fetchown() -> $#ownpasswd $#owngroup\n"; @@ -191,24 +254,29 @@ sub fetchown () { sub checkuid ($$) { my ($useuid,$foruser) = @_; for $e (@ownpasswd) { - if ($e->[0] ne $foruser && $e->[2] == $useuid) { - diag("uid clash with $e->[0] (uid $e->[2])"); + 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; } } 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;"); } } @@ -221,55 +289,135 @@ sub fetchpasswd () { if (length $ch_getshadow) { fetchfile(%remshadow,"$ch_getshadow |"); for $k (keys %rempasswd) { - $rempasswd{$k}->[1]= 'xx' unless length $rempasswd{$k}->[1]; + $rempasswd{$k}->[$REM_PW]= 'xx' unless length $rempasswd{$k}->[$REM_PW]; } for $k (keys %remshadow) { next unless exists $rempasswd{$k}; - $rempasswd{$k}->[1]= $remshadow{$k}->[1]; + $rempasswd{$k}->[$REM_PW]= $remshadow{$k}->[$SP_PW]; } } + $ch_fetchedpasswd= 1; +} + +sub fetchgroup () { + return if $ch_fetchedgroup; + die "$configfile:$.: getgroup not specified for host $ch_name\n" + unless length $ch_getgroup; + fetchfile(%remgroup,"$ch_getgroup |"); + $ch_fetchedgroup= 1; +} + +sub syncusergroup ($$) { + my ($lu,$luid) = @_; + + return 1 if $defaultgid != -1; +#print STDERR "syncusergroup($lu,$luid)\n"; + $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; + if (!$samename || !$sameid) { + diag("local group $e->[$GR_GROUP] ($e->[$GR_GID]) 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, [ newentry('GR', $lu, + 'PW', 'x', + 'GID', $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) = @_; + my ($vn); +#print STDERR "syncuser($lu,$ru)\n"; + return if $doneuser{$lu}++; next unless $ch_doinghost; - $diagstr= "user $lu from $ch_name!$ru"; + return if !length $ru; fetchown(); -#print STDERR "syncuser($lu,$ru)\n"; + + if ($display) { + for $e (@ownpasswd) { + next unless ref $e && $e->[$PW_USER] eq $lu; + hosthead("from $ch_name"); + print ($lu eq $ru ? " $lu" : " $lu($ru)") or die $!; + print "" 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; } - if (length $ch_getshadow && exists $remshadow{$ru} && length $remshadow{$ru}->[7]) { + if (length $ch_getshadow && exists $remshadow{$ru} && + length $remshadow{$ru}->[$SP_DAYSDISD]) { diag("remote account disabled in shadow"); return; } - - if (!grep($_->[0] 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; } if ($opt_sameuid) { - $useuid= $rempasswd{$ru}->[2]; + $useuid= $rempasswd{$ru}->[$REM_UID]; + $usegid= $rempasswd{$ru}->[$REM_GID]; } 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) { - $tuid= $e->[2]; next if $tuid<$useuid || $tuid>$ch_uidmax; + 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/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], - "$ch_homebase/$lu",$rempasswd{$ru}->[6]); + @newpwent= newentry('PW', $lu, + 'PW', 'x', + 'UID', $useuid, + 'GID', $usegid, + 'COMMENT', $rempasswd{$ru}->[$REM_COMMENT], + 'HOME', "$ch_homebase/$lu", + 'SHELL', $ch_defaultshell); defined($c= open CU,"-|") or die $!; if (!$c) { @@ -279,9 +427,10 @@ sub syncuser ($$) { print STDOUT join(':',@newpwent),"\n" or die $!; exit 0; } - for ($i=0; $i<@envs_passwd; $i++) { - next if $envs_passwd[$i] eq 'x'; - $ENV{"SYNCUSER_CREATE_$envs_passwd[$i]"}= $newpwent[$i]; + for ($i=0; $i<@envs_createuser; $i++) { + $vn= "PW_$envs_createuser[$i]"; +#print STDERR ">$i|$vn|$$vn|$newpwent[$$vn]<\n"; + $ENV{"SYNCUSER_CREATE_$envs_createuser[$i]"}= $newpwent[$$vn]; } exec $opt_createuser; die "$configfile:$.: ($lu): $opt_createuser: $!\n"; } @@ -292,33 +441,78 @@ sub syncuser ($$) { if ($newpwent !~ m/\:/) { diag("creation script demurred"); return; } @newpwent= split(/\:/,$newpwent,-1); } - die "$opt_createuser: bad output: $_\n" if @newpwent!=7 or $newpwent[0] ne $lu; - checkuid($newpwent[2],$lu) or return; + die "$opt_createuser: bad result: \`".join(':',@newpwent)."\'\n" + if @newpwent != $PW_fields or $newpwent[$PW_USER] ne $lu; + checkuid($newpwent[$PW_UID],$lu) or return; if ($own_haveshadow) { - push(@ownshadow,[ $lu, $newpwent[1], $cdays, 0,99999,7,'','' ]); - $newpwent[1]= 'x'; + push @ownshadow, [ newentry('SP', $lu, + 'PW', 'x', + 'DAYSCHGD', $cdays, + 'DAYSFIX', 0, + 'DAYSEXP', 99999, + 'DAYSEXPDIS', 7) ]; $modifiedshadow= 1; } + syncusergroup($lu,$newpwent[$PW_UID]) or return; push @ownpasswd,[ @newpwent ]; $modifiedpasswd= 1; } + + for $e (@ownpasswd) { + next unless ref $e && $e->[$PW_USER] eq $lu; + syncusergroup($lu,$e->[$PW_UID]) or return; + } + + $ruid= $rempasswd{$ru}->[$REM_UID]; + $rgid= $rempasswd{$ru}->[$REM_GID]; + if ($opt_sameuid && checkuid($ruid,$lu)) { + for $e (@ownpasswd) { + 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; + } + } + #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"; - copyfield('shadow',$lu,1, $rempasswd{$ru}->[1]); + 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 { -#print STDERR "syncuser($lu,$ru) passwd $rempasswd{$ru}->[1]\n"; - copyfield('passwd',$lu,1, $rempasswd{$ru}->[1]); +#print STDERR "syncuser($lu,$ru) passwd $rempasswd{$ru}->[$REM_PW]\n"; + copyfield('passwd',$lu,$PW_PW, $rempasswd{$ru}->[$REM_PW]); } - 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; + copyfield('passwd',$lu,$PW_COMMENT, $rempasswd{$ru}->[$REM_COMMENT]); + + $newsh= $rempasswd{$ru}->[$REM_SHELL]; + $oksh= $checkedshell{$newsh}; + if (!length $oksh) { $checkedshell{$newsh}= $oksh= (-x $newsh) ? 1 : 0; } + copyfield('passwd',$lu,$PW_SHELL, $newsh) if $oksh; + + 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); +#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}->[$GR_USERS])); + $cusers= $e->[$GR_USERS]; $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->[$GR_USERS]= $cusers; + $modifiedgroup= 1; } } } @@ -330,23 +524,69 @@ sub banner () { } sub finish () { + my ($record); + + for $h (keys %wanthost) { + die "host $h not in config file\n" if $wanthost{$h}; + } + + 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 ref $e2 && $e2->[$SP_USER] eq $tu; + $tpw= $e2->[$SP_PW]; last; + } + $tpw= length($tpw)>=13 ? 1 : length($tpw) ? -1 : 0; + $head= ($tpw == 1 ? "unsynched" : + $tpw == 0 ? "unsynched, passwordless" : + "unsynched, no-logins"). + ($tuid < 100 ? " system account" : " normal user"); + $unsynch_type{$head} .= " $e->[$PW_USER]"; + } + foreach $head (sort keys %unsynch_type) { + hosthead($head); + print $unsynch_type{$head} or die $!; + } + print "\n\n" or die $!; + 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 $@; + $realfile= $file{$file,$PW_format}; + must_eval("\$modified= \$modified$file; \$data_ref= \\\@own$file;". + " \$fetched= \$own_fetched$file; 1;"); next if !$modified; die $file unless $fetched; banner(); - $newfile= $no_act ? "$file.new" : "/etc/$file.new"; - open NF,"> $newfile"; + if ($no_act) { + $newfileinst= "/etc/$realfile"; + $newfile= "$realfile.new"; + } else { + $newfileinst= $savebackto{$file}; + $newfile= "$newfileinst.new"; + } + 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 /etc/$file $newfile"; $?==256 or die $?; + system 'diff','-U0','--label',$realfile,$newfileinst, + '--label',"$realfile.new",$newfile; + $?==256 or die $?; if (!$no_act) { - chown 0,0, $newfile or die $!; - chmod 0644, $newfile or die $!; - rename $newfile, "/etc/$file" or die $!; + (@stats= stat $newfileinst) or die "$file: $!"; + chown $stats[4],$stats[5], $newfile or die $!; + chmod $stats[2] & 07777, $newfile or die $!; + rename $newfile, $newfileinst or die $!; } } exit 0; @@ -354,22 +594,36 @@ sub finish () { while () { 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; $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; + } + fields_fmt('REM','std'); } elsif (m/^(getpasswd|getshadow|getgroup)\s+(.*\S)$/) { - eval "\$ch_$1= \$2; 1;" or die $@; - } elsif (m/^(lockpasswd|lockgroup)\s+(\S+)$/) { - eval "\$ch_$1= \$2; 1;" or die $@; - } elsif (m,^(homebase)\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+)$/) { + must_eval("\$ch_lock_$1= \$3; \$ch_lockstyle_$1= \$2; 1;"); + } elsif (m/^lock(passwd|group)\s+(none)$/) { + must_eval("\$ch_lockstyle_$1= \$2; 1;"); + } elsif (m,^(homebase|defaultshell)\s+(/\S+)$,) { + 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$/) { @@ -377,26 +631,44 @@ while () { } 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; + $!=0; system 'date'; $? and die "date: $! $?\n"; + } 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 $@; + must_eval("\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;"); + } 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/[-+._]/\\$&/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"; }