From: ian Date: Sat, 2 Jan 1999 21:33:49 +0000 (+0000) Subject: Initial checkin, seems to sort of work. X-Git-Tag: branchpoint-2001-05-11-withlockex-old~20 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;ds=sidebyside;h=f586956c8ecf9fe477aa682ac69b0490dfc25e49;p=chiark-utils.git Initial checkin, seems to sort of work. --- f586956c8ecf9fe477aa682ac69b0490dfc25e49 diff --git a/sync-accounts/sync-accounts b/sync-accounts/sync-accounts new file mode 100755 index 0000000..3882250 --- /dev/null +++ b/sync-accounts/sync-accounts @@ -0,0 +1,405 @@ +#!/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 +# +# 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 +# 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 `/'. +# +# logfile +# Append log messages to instead of stdout. +# Errors still go to stderr. +# +# 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. + +use POSIX; + +$configfile= '/etc/sync-accounts'; +$def_createuser= 'sync-accounts-createuser'; +$ch_homebase= '/home'; + +END { + for $x (@unlocks) { + unlink $x or warn "unable to unlock by removing $x: $!\n"; + } +} + +$|=1; +$cdays= int(time/86400); + +@envs_passwd= qw(USER x UID GID COMMENT HOME SHELL); + +while ($ARGV[0] =~ m/^-/) { + $_= shift @ARGV; + last if m/^--$/; + if (m/^-C/) { + $configfile= $'; + } elsif (m/^-n$/) { + $no_act= 1; + } else { + die "unknown option $_\n"; + } +} + +open CF,"< $configfile" or die "$configfile: $!"; + +sub fetchfile (\%$) { + my ($ary_ref,$get_str) = @_; + + undef %$ary_ref; + open G,"$get_str" or die "$get_str: $!"; + while () { + chomp; + m/^([^:]+)\:/ or die "$ch_name: $get_str:$.: $_ ?\n"; + $ary_ref->{$1}= [ split(/\:/,$_,-1) ]; + } + 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; + } + open O,"$fn_str" or die "$fn_str: $!"; + while () { + chomp; + push @$ary_ref, [ split(/\:/,$_,-1) ]; + } + close O or die "$fn_str: $!"; +} + +sub diag ($) { + print "$diagstr: $_[0]\n" or die $!; +} + +sub fetchown () { + if (!$own_fetchedpasswd) { + fetchownfile(@ownpasswd,'/etc/passwd',$ch_lockpasswd); + if (defined stat('/etc/shadow')) { + $own_haveshadow= 1; + $own_fetchedshadow= 1; + fetchownfile(@ownshadow,'/etc/shadow','/dev/null'); + } elsif ($! == &ENOENT) { + $own_haveshadow= 0; + } else { + die "unable to check for /etc/shadow: $!\n"; + } + $own_fetchedpasswd= 1; + } + if (!$own_fetchedgroup) { + fetchownfile(@owngroup,'/etc/group',$ch_lockgroup); + $own_fetchedgroup= 1; + } +#print STDERR "fetchown() -> $#ownpasswd $#owngroup\n"; +} + +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])"); + return 0; + } + } + return 1; +} + +sub copyfield ($$$$) { + my ($file,$entry,$field,$value) = @_; + eval "\$ary_ref= \\\@own$file; 1;" or die $@; +#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 $@; + } +} + +sub fetchpasswd () { + return if $ch_fetchedpasswd; + die "$configfile:$.: getpasswd not specified for host $ch_name\n" + unless length $ch_getpasswd; + undef %remshadow; + fetchfile(%rempasswd,"$ch_getpasswd |"); + if (length $ch_getshadow) { + fetchfile(%remshadow,"$ch_getshadow |"); + for $k (keys %rempasswd) { + $rempasswd{$k}->[1]= 'xx' unless length $rempasswd{$k}->[1]; + } + for $k (keys %remshadow) { + next unless exists $rempasswd{$k}; + $rempasswd{$k}->[1]= $remshadow{$k}->[1]; + } + } +} + +sub syncuser ($$) { + my ($lu,$ru) = @_; + + next unless $ch_doinghost; + $diagstr= "user $lu from $ch_name!$ru"; + + fetchown(); +#print STDERR "syncuser($lu,$ru)\n"; + fetchpasswd(); + + if (!$rempasswd{$ru}) { diag("no remote entry"); return; } + if (length $ch_getshadow && exists $remshadow{$ru} && length $remshadow{$ru}->[7]) { + 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]; + } 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"; + + $useuid= $ch_uidmin; + for $e (@ownpasswd, @owngroup) { + $tuid= $e->[2]; next if $tuid<$useuid || $tuid>$ch_uidmax; + if ($tuid==$ch_uidmax) { + diag("uid/gid $ch_uidmax used, cannot create users"); + return; + } + $useuid= $tuid+1; + } + } + + @newpwent= ($lu,'x',$useuid,$useuid,$rempasswd{$ru}->[4], + "$ch_homebase/$lu",$rempasswd{$ru}->[6]); + + defined($c= open CU,"-|") or die $!; + if (!$c) { + @unlocks= (); + defined($c2= open STDIN,"-|") or die $!; + if (!$c2) { + 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]; + } + exec $opt_createuser; die "$configfile:$.: ($lu): $opt_createuser: $!\n"; + } + $newpwent= ; + close CU; $? and die "$configfile:$.: ($lu): $opt_createuser: code $?\n"; + chomp $newpwent; + if (length $newpwent) { + 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; + if ($own_haveshadow) { + push(@ownshadow,[ $lu, $newpwent[1], $cdays, 0,99999,7,'','' ]); + $newpwent[1]= 'x'; + $modifiedshadow= 1; + } + push @ownpasswd,[ @newpwent ]; + $modifiedpasswd= 1; + } +#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]); + } else { +#print STDERR "syncuser($lu,$ru) passwd $rempasswd{$ru}->[1]\n"; + copyfield('passwd',$lu,1, $rempasswd{$ru}->[1]); + } + 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; + } + } +} + +sub banner () { + return if $bannerdone; + print "\n" or die $!; system 'date'; $? and die $?; + $bannerdone= 1; +} + +sub finish () { + for $file (qw(passwd shadow group)) { + eval "\$modified= \$modified$file; \$data_ref= \\\@own$file;". + " \$fetched= \$own_fetched$file; 1;" or die $@; + next if !$modified; + die $file unless $fetched; + banner(); + $newfile= $no_act ? "$file.new" : "/etc/$file.new"; + open NF,"> $newfile"; + for $e (@$data_ref) { + print NF join(':',@$e),"\n" or die $!; + } + 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 $!; + rename $newfile, "/etc/$file" or die $!; + } + } + exit 0; +} + +while () { + chomp; + next if m/^\#/ || !m/\S/; + s/^\s*//; s/\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); + } 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 $@; + } elsif (m/^(uidmin|uidmax)\s+(\d+)$/ && $2>0) { + eval "\$ch_$1= \$2; 1;" or die $@; + } elsif (m/^createuser$/) { + $opt_createuser= $def_createuser; + } elsif (m/^nocreateuser$/) { + $opt_createuser= ''; + } 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 { + open STDOUT,">> $1" or die "$1: $!"; $|=1; + } + } elsif (m/^(no|)(sameuid)$/) { + eval "\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;" or die $@; + } 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/^users\s+(\d+)\-(\d+)$/) { + $tmin= $1; $tmax= $2; + fetchpasswd(); + for $k (keys %rempasswd) { + $tuid= $rempasswd{$k}->[2]; + next if $tuid<$1 or $tuid>$2; + syncuser($k,$k); + } + } else { + die "$configfile:$.: unknown directive\n"; + } +} + +die "$configfile:$.: missing \`end', or read error\n";