2 # $Id: sync-accounts,v 1.14 1999-01-03 15:52:58 ian Exp $
3 # usage: sync-accounts [-n] [-C<config-file>] [<host> ...]
5 # -n do not really do anything
6 # -C alternative config file (default is /etc/sync-accounts)
7 # -q display accounts synched, not synched, etc.
8 # if no host(s) specified, does all
9 # host(s) may not be specified with -q
11 # The config file consists of directives, one per line. Leading and
12 # trailing whitespace, blank lines and lines starting # are ignored.
14 # Some config file directives apply globally and should appear first:
16 # lockpasswd <suffix/filename>
17 # lockgroup <suffix/filename>
18 # Specifies the lockfile suffix or pathname to use when editing
19 # the passwd and group files. The value is a suffix if it does
20 # not start with `/'. If set to /dev/null no locking is done.
23 # Append log messages to <filename> instead of stdout.
24 # Errors still go to stderr.
26 # Some config file directives set options which may be different at
27 # different points in the file. The most-recently-seen value is used
33 # When an account is to be created, a uid/gid will be chosen
34 # which is one higher than the highest currently in use (except
35 # that ids outside the range <min>-<max> are ignored and will
36 # never be used). The default home directory location is
37 # <pathname>/<username>.
41 # Specifies whether uids are supposed to match. The default is
42 # nosameuid. When sameuid is on, it is an error for the uid or
43 # gid of a local account not to match the corresponding remote
44 # account, and new local accounts will get the remote accounts'
50 # Specifies whether local accounts are supposed to have
51 # corresponding groups, or all be part of a particular group. If
52 # usergroups is set, when a new account is created, the
53 # corresponding per-user group will be created as well, and
54 # per-user groups are created for existing accounts if necessary
55 # (if account creation is enabled). If the gid or group name for
56 # a per-user group is already taken for a different group name or
57 # gid this will be logged, and processing of that account will be
58 # inhibited, but it is not a fatal error. If defaultgid is used,
59 # then newly-created accounts will be made a part of that group,
60 # and the groups of existing accounts will be left alone. If
61 # nousergroups is specified then no new accounts can be created,
62 # and existing accounts' groups will be left alone. The default
66 # createuser <commandname>
68 # Specifies whether accounts found on the remote host should be
69 # created if necessary, and what command to run to do the
70 # creation (eg, setup of home directory). The default is
71 # nocreateuser. If createuser is specified without a commandname
72 # then sync-accounts-createuser is used. The command is found on
73 # the PATH if necessary. Either sameuid, or both uidmin and
74 # uidmax, must be specified, if accounts are to be created.
76 # The command (which will be run with sh -c) must at least create
77 # the new account's home directory. The passwd and group entries
78 # will not have been set up. The following environment variables
79 # will be set, giving details about the account to be created:
80 # SYNCACCOUNT_CREATE_USER
81 # SYNCACCOUNT_CREATE_UID
82 # SYNCACCOUNT_CREATE_GID
83 # SYNCACCOUNT_CREATE_COMMENT
84 # SYNCACCOUNT_CREATE_HOME
85 # SYNCACCOUNT_CREATE_SHELL
86 # If it chooses, the script may modify the password entry which
87 # will be added to the system, by outputting a replacement
88 # password file entry. (The password field of that is ignored.)
89 # If the script outputs a line which does not contain a : then
90 # the account will not be created after all.
92 # group <glob-pattern>
93 # nogroup <glob-pattern>
94 # Specifies that the membership of the local groups specified
95 # should be adjusted or not adjusted whenever account data for a
96 # particular user is copied, so that the account will be a member
97 # of the affected group locally iff it is a member of the same
98 # group on the remote host. The most recently-encountered
99 # glob-pattern for a particular group takes effect. The default
102 # defaultshell <pathname>
103 # If, when creating an account, the remote account's shell is not
104 # available on the local system, this value will be used. The
105 # default is /bin/sh.
107 # Some config file directives are per-host, and should appear before
108 # any directives which actually modify accounts:
110 # host <shorthostname>
111 # Starts a host's section. This resets the per-host parameters
112 # to the defaults. The shorthostname need not be the host's
113 # official name in any sense. If sync-accounts is invoked with
114 # host names on the command line they are compared with the
117 # getpasswd <command>
119 # Commands to run on the local host to get the passwd, shadow and
120 # group data for the host in question. getpasswd must be
121 # specified if user data is to be transferred; getgroup must be
122 # specified if group data is to be transferred.
124 # getshadow <command>
125 # Specifies that shadow file data is to be used (by default,
126 # password information is found from the output of getpasswd).
127 # The command should emit shadow data in the format specified by
128 # shadow(5) on Linux. getshadow should not be specified without
131 # Some configuration file directives specify that account data is to
132 # transferred from the current host. They should appear as the last
133 # thing(s) in a host section:
135 # user <username> [remote=<remoteusername>]
136 # Specifies that account data should be copied for local user
137 # <username> from the remote account <remoteusername> (assumed to
138 # be the same as <username> if not specified). The account
139 # password, comment field, and shell will be copied
140 # unconditionally. If sameuid is specified the uid will be
143 # users <ruidmin>-<ruidmax>
144 # Specifies that all remote users whose uid is in the given range
145 # are to be copied to corresponding local user accounts.
148 # Specifies that data for <username> is _not_ to be copied, even
149 # if subsequent user or users directives suggest that it should
152 # (A note is made when a `user', `users' or `nouser' directive is
153 # encountered for a particular account, and no subsequent directives
154 # for that account will take effect.)
157 # This directive has no effect on `sync-accounts'. However, it
158 # is used as a placeholder by `grab-account': new accounts for
159 # creation are inserted just before `addhere'.
161 # Finally, the config file must finish with:
167 $configfile= '/etc/sync-accounts';
168 $def_createuser= 'sync-accounts-createuser';
169 $ch_homebase= '/home';
170 $ch_defaultshell= '/bin/sh';
171 $defaultgid= -1; # -1 => usergroups; -2 => nousergroups
172 @groupglobs= [ '.*', 0 ];
177 unlink $x or warn "unable to unlock by removing $x: $!\n";
182 $cdays= int(time/86400);
184 @envs_passwd= qw(USER x UID GID COMMENT HOME SHELL);
186 while ($ARGV[0] =~ m/^-/) {
198 die "unknown option $_\n";
202 die "hosts must not be specified with -q\n" if @ARGV && $display;
204 for $h (@ARGV) { $wanthost{$h}= 1; }
206 open CF,"< $configfile" or die "$configfile: $!";
208 sub fetchfile (\%$) {
209 my ($ary_ref,$get_str) = @_;
212 open G,"$get_str" or die "$get_str: $!";
215 m/^([^:]+)\:/ or die "$ch_name: $get_str:$.: $_ ?\n";
216 $ary_ref->{$1}= [ split(/\:/,$_,-1) ];
218 close G; $? and die "$ch_name: $get_str: exit code $?\n";
221 sub fetchownfile (\@$$) {
222 my ($ary_ref,$fn_str,$lock_str) = @_;
223 die "$configfile:$.: lockfile name for $fn_str not".
224 " defined (use lockpasswd/lockgroup)\n" unless length $lock_str;
225 if ($lock_str ne '/dev/null' && !$no_act) {
226 $lock_str= "$fn_str$lock_str" unless $lock_str =~ m,^/,;
227 link $fn_str,$lock_str or die "cannot lock $fn_str by creating $lock_str: $!\n";
228 push @unlocks,$lock_str;
230 open O,"$fn_str" or die "$fn_str: $!";
233 push @$ary_ref, [ split(/\:/,$_,-1) ];
235 close O or die "$fn_str: $!";
239 print "$diagstr: $_[0]\n" or die $!;
242 sub regroupglobs () {
243 $nogroups= (@groupglobs == 1 &&
244 $groupglobs[0]->[0] eq '.*' &&
245 !$groupglobs[0]->[1]);
246 $ggfunc= "sub wantsyncgroup {\n \$_= \$_[0];\n return\n";
247 for $e (@groupglobs) { $ggfunc.= " m/^$e->[0]\$/ ? $e->[1] :\n"; }
248 $ggfunc.= " die;\n};\n1;\n";
249 #print STDERR "$ggfunc\n";
250 eval $ggfunc or die "$ggfunc // $@";
254 if (!$own_fetchedpasswd) {
255 fetchownfile(@ownpasswd,'/etc/passwd',$ch_lockpasswd);
256 if (defined stat('/etc/shadow')) {
258 $own_fetchedshadow= 1;
259 fetchownfile(@ownshadow,'/etc/shadow','/dev/null');
260 } elsif ($! == &ENOENT) {
263 die "unable to check for /etc/shadow: $!\n";
265 $own_fetchedpasswd= 1;
267 if (!$own_fetchedgroup) {
268 fetchownfile(@owngroup,'/etc/group',$ch_lockgroup);
269 $own_fetchedgroup= 1;
271 #print STDERR "fetchown() -> $#ownpasswd $#owngroup\n";
275 my ($useuid,$foruser) = @_;
276 for $e (@ownpasswd) {
277 if ($e->[0] ne $foruser && $e->[2] == $useuid) {
278 diag("uid clash with $e->[0] (uid $e->[2])");
285 sub copyfield ($$$$) {
286 my ($file,$entry,$field,$value) = @_;
287 eval "\$ary_ref= \\\@own$file; 1;" or die $@;
288 #print STDERR "copyfield($file,$entry,$field,$value)\n";
290 #print STDERR "copyfield($file,$entry,$field,$value) $e->[0] $e->[field] ".join(':',@$e)."\n";
291 next unless $e->[0] eq $entry;
292 next if $e->[$field] eq $value;
293 $e->[$field]= $value;
294 eval "\$modified$file= 1; 1;" or die $@;
299 return if $ch_fetchedpasswd;
300 die "$configfile:$.: getpasswd not specified for host $ch_name\n"
301 unless length $ch_getpasswd;
303 fetchfile(%rempasswd,"$ch_getpasswd |");
304 if (length $ch_getshadow) {
305 fetchfile(%remshadow,"$ch_getshadow |");
306 for $k (keys %rempasswd) {
307 $rempasswd{$k}->[1]= 'xx' unless length $rempasswd{$k}->[1];
309 for $k (keys %remshadow) {
310 next unless exists $rempasswd{$k};
311 $rempasswd{$k}->[1]= $remshadow{$k}->[1];
317 return if $ch_fetchedgroup;
318 die "$configfile:$.: getgroup not specified for host $ch_name\n"
319 unless length $ch_getgroup;
320 fetchfile(%remgroup,"$ch_getgroup |");
323 sub syncusergroup ($$) {
326 return 1 if $defaultgid != -1;
327 #print STDERR "syncusergroup($lu,$luid)\n";
331 $samename= $e->[0] eq $lu;
332 $sameid= $e->[2] eq $luid;
333 next unless $samename || $sameid;
334 if (!$samename || !$sameid) {
335 diag("local group $e->[0] ($e->[2]) mismatch vs. local user $lu ($luid)");
339 diag("per-user group $lu ($luid) duplicated");
345 return 1 if $ugfound;
347 if (!length $opt_createuser) {
348 diag("account creation not enabled, not creating per-user group");
351 push @owngroup, [ $lu,'x',$luid,'' ];
358 return if $hostheaddone eq $th;
359 print "\n\n" or die $! if length $hostheaddone;
360 print "==== $th ====\n" or die $!;
367 #print STDERR "syncuser($lu,$ru)\n";
368 return if $doneuser{$lu}++;
369 next unless $ch_doinghost;
370 return if !length $ru;
375 for $e (@ownpasswd) {
376 next unless $e->[0] eq $lu;
377 hosthead("from $ch_name");
378 print ($lu eq $ru ? " $lu" : " $lu($ru)") or die $!;
379 print "<DUPLICATE>" if $displaydone{$lu}++;
384 $diagstr= "user $lu from $ch_name!$ru";
386 #print STDERR "syncuser($lu,$ru) doing\n";
389 if (!$rempasswd{$ru}) { diag("no remote entry"); return; }
390 if (length $ch_getshadow && exists $remshadow{$ru} && length $remshadow{$ru}->[7]) {
391 diag("remote account disabled in shadow");
395 if (!grep($_->[0] eq $lu, @ownpasswd)) {
396 if (!length $opt_createuser) { diag("account creation not enabled"); return; }
397 if ($no_act) { diag("-n specified; not creating account"); return; }
400 $useuid= $rempasswd{$ru}->[2];
401 $usegid= $rempasswd{$ru}->[3];
403 die "nousergroups specified, cannot create users\n" if $defaultgid==-2;
404 length $ch_uidmin or die "no uidmin specified, cannot create users\n";
405 length $ch_uidmax or die "no uidmax specified, cannot create users\n";
406 $ch_uidmin<$ch_uidmax or die "uidmin>=uidmax, cannot create users\n";
409 for $e ($defaultgid==-1 ? (@ownpasswd, @owngroup) : (@ownpasswd)) {
410 $tuid= $e->[2]; next if $tuid<$useuid || $tuid>$ch_uidmax;
411 if ($tuid==$ch_uidmax) {
412 diag("uid (or gid?) $ch_uidmax used, cannot create users");
417 $usegid= $defaultgid==-1 ? $useuid : $defaultgid;
420 @newpwent= ($lu,'x',$useuid,$usegid,$rempasswd{$ru}->[4],
421 "$ch_homebase/$lu",$ch_defaultshell);
423 defined($c= open CU,"-|") or die $!;
426 defined($c2= open STDIN,"-|") or die $!;
428 print STDOUT join(':',@newpwent),"\n" or die $!;
431 for ($i=0; $i<@envs_passwd; $i++) {
432 next if $envs_passwd[$i] eq 'x';
433 $ENV{"SYNCUSER_CREATE_$envs_passwd[$i]"}= $newpwent[$i];
435 exec $opt_createuser; die "$configfile:$.: ($lu): $opt_createuser: $!\n";
438 close CU; $? and die "$configfile:$.: ($lu): $opt_createuser: code $?\n";
440 if (length $newpwent) {
441 if ($newpwent !~ m/\:/) { diag("creation script demurred"); return; }
442 @newpwent= split(/\:/,$newpwent,-1);
444 die "$opt_createuser: bad output: $_\n" if @newpwent!=7 or $newpwent[0] ne $lu;
445 checkuid($newpwent[2],$lu) or return;
446 if ($own_haveshadow) {
447 push(@ownshadow,[ $lu, $newpwent[1], $cdays, 0,99999,7,'','','' ]);
451 syncusergroup($lu,$newpwent[2]) or return;
452 push @ownpasswd,[ @newpwent ];
456 for $e (@ownpasswd) {
457 next unless $e->[0] eq $lu;
458 syncusergroup($lu,$e->[2]) or return;
461 $ruid= $rempasswd{$ru}->[2];
462 $rgid= $rempasswd{$ru}->[3];
463 if ($opt_sameuid && checkuid($ruid,$lu)) {
464 for $e (@ownpasswd) {
465 next unless $e->[0] eq $lu;
466 $luid= $e->[2]; $lgid= $e->[3];
467 die "$diagstr: local uid $luid, remote uid $ruid\n" if $luid ne $ruid;
468 die "$diagstr: local gid $lgid, remote gid $rgid\n" if $lgid ne $rgid;
472 #print STDERR "syncuser($lu,$ru) exists $own_haveshadow\n";
473 if ($own_haveshadow && grep($_->[0] eq $lu, @ownshadow)) {
474 #print STDERR "syncuser($lu,$ru) shadow $rempasswd{$ru}->[1]\n";
475 copyfield('shadow',$lu,1, $rempasswd{$ru}->[1]);
477 #print STDERR "syncuser($lu,$ru) passwd $rempasswd{$ru}->[1]\n";
478 copyfield('passwd',$lu,1, $rempasswd{$ru}->[1]);
480 copyfield('passwd',$lu,4, $rempasswd{$ru}->[4]); # comment
482 $newsh= $rempasswd{$ru}->[6];
483 $oksh= $checkedshell{$newsh};
484 if (!length $oksh) { $checkedshell{$newsh}= $oksh= (-x $newsh) ? 1 : 0; }
485 copyfield('passwd',$lu,6, $newsh) if $oksh;
490 #print STDERR "syncuser($lu,$ru) group $tgroup\n";
491 next unless &wantsyncgroup($tgroup);
492 #print STDERR "syncuser($lu,$ru) group $tgroup yes\n";
494 if (!exists $remgroup{$tgroup}) {
495 diag("group $tgroup: not on remote host");
498 $inremote= grep($_ eq $ru, split(/\,/,$remgroup{$tgroup}->[3]));
499 $cusers= $e->[3]; $inlocal= grep($_ eq $lu, split(/\,/,$cusers));
500 if ($inremote && !$inlocal) {
501 $cusers.= ',' if length $cusers;
503 } elsif ($inlocal && !$inremote) {
504 $cusers= join(',', grep($_ ne $lu, split(/\,/, $cusers)));
515 return if $bannerdone;
516 print "\n" or die $!; system 'date'; $? and die $?;
521 for $h (keys %wanthost) {
522 die "host $h not in config file\n" if $wanthost{$h};
527 #print STDERR "\n\nfinish display=$display pw=$pw\n\n";
528 for $e (@ownpasswd) {
530 next if $displaydone{$tu};
532 for $e2 (@ownshadow) {
533 next unless $e2->[0] eq $tu;
534 $tpw= $e2->[1]; last;
536 $tpw= length($tpw)==13 ? 1 : length($tpw) ? -1 : 0;
537 next unless $pw == $tpw;
538 hosthead($pw == 1 ? "unsynched normal account" :
539 $pw == 0 ? "unsynched, passwordless" :
540 "unsynched, no logins");
541 print " $e->[0]" or die $!;
544 print "\n\n" or die $! if $hostheaddone;
549 for $file (qw(passwd shadow group)) {
550 eval "\$modified= \$modified$file; \$data_ref= \\\@own$file;".
551 " \$fetched= \$own_fetched$file; 1;" or die $@;
553 die $file unless $fetched;
555 $newfile= $no_act ? "$file.new" : "/etc/$file.new";
556 open NF,"> $newfile" or die "$newfile: $!";
557 for $e (@$data_ref) {
558 print NF join(':',@$e),"\n" or die $!;
561 system "diff -U0 /etc/$file $newfile"; $?==256 or die $?;
563 (@stats= stat "/etc/$file") or die "$file: $!";
564 chown $stats[4],$stats[5], $newfile or die $!;
565 chmod $stats[2] & 07777, $newfile or die $!;
566 rename $newfile, "/etc/$file" or die $!;
574 next if m/^\#/ || !m/\S/;
576 finish() if m/^end$/;
577 if (m/^host\s+(\S+)$/) {
579 $ch_getpasswd= $ch_getgroup= $ch_getshadow= '';
580 $ch_fetchedpasswd= $ch_fetchedgroup;
583 } elsif (exists $wanthost{$ch_name}) {
584 $wanthost{$ch_name}= 0;
589 } elsif (m/^(getpasswd|getshadow|getgroup)\s+(.*\S)$/) {
590 eval "\$ch_$1= \$2; 1;" or die $@;
591 } elsif (m/^(lockpasswd|lockgroup)\s+(\S+)$/) {
592 eval "\$ch_$1= \$2; 1;" or die $@;
593 } elsif (m,^(homebase|defaultshell)\s+(/\S+)$,) {
594 eval "\$ch_$1= \$2; 1;" or die $@;
595 } elsif (m/^(uidmin|uidmax)\s+(\d+)$/ && $2>0) {
596 eval "\$ch_$1= \$2; 1;" or die $@;
597 } elsif (m/^createuser$/) {
598 $opt_createuser= $def_createuser;
599 } elsif (m/^nocreateuser$/) {
601 } elsif (m/^createuser\s+(\S+)$/) {
603 } elsif (m/^logfile\s+(.*\S)$/) {
605 open STDOUT,">> $1" or die "$1: $!"; $|=1;
606 } elsif (!$display) {
607 print "would log to $1\n" or die $!;
609 } elsif (m/^(no|)(sameuid)$/) {
610 eval "\$opt_$2= ".($1 eq 'no' ? 0 : 1)."; 1;" or die $@;
611 } elsif (m/^usergroups$/) {
613 } elsif (m/^nousergroups$/) {
615 } elsif (m/^defaultgid\s+(\d+)$/) {
617 } elsif (m/^(no|)group\s+([-+.0-9a-zA-Z*?]+)$/) {
618 $yes= $1 eq 'no' ? 0 : 1;
620 @groupglobs=() if $_ eq '*';
624 unshift @groupglobs, [ $_, $yes ];
626 } elsif (m/^user\s+(\S+)$/) {
628 } elsif (m/^user\s+(\S+)\s+remote\=(\S+)$/) {
630 } elsif (m/^nouser\s+(\S+)$/) {
632 } elsif (m/^users\s+(\d+)\-(\d+)$/) {
633 $tmin= $1; $tmax= $2; $except= $3;
635 for $k (keys %rempasswd) {
636 $tuid= $rempasswd{$k}->[2];
637 next if $tuid<$1 or $tuid>$2;
640 } elsif (m/^addhere$/) {
642 die "$configfile:$.: unknown directive\n";
646 die "$configfile:$.: missing \`end', or read error\n";