chiark / gitweb /
changes from Cambridge University (Ben Harris) unedited; will edit shortly
[userv-utils.git] / groupmanage / groupmanage
1 #!/usr/bin/perl
2 #
3 # Copyright (C)1995-9 Ian Jackson <ijackson@chiark.greenend.org.uk>
4 # Copyright (C) 1999, 2003
5 #     Chancellor Masters and Scholars of the University of Cambridge
6 #
7 # Hacked by Ben Harris <bjh21@cam.ac.uk> in 1999 and 2003 for Unix
8 # Support's own nefarious purposes.
9 #
10 # This is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2, or (at your option)
13 # any later version.
14 #
15 # It is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # $Id$
21
22 sub usage {
23     &unlock;
24     &p_out;
25     print(<<END) || die "groupmanage: write usage: $!\n";
26 groupmanage: $_[0]
27   usage:
28     groupmanage <groupname> [--info]
29     groupmanage <groupname> <action> [<action> ...]
30     groupmanage <groupname> --create [<action> <action> ...]
31   actions:
32        --clear
33        --add <username> <username> ...
34        --remove <username> <username> ...
35        --manager-clear
36        --manager-add <username> <username> ...
37        --manager-remove <username> <username> ...
38        --title <string>
39        --owner <username>  [root only]
40 groupmanage is Copyright.  It is free software, released under the GNU
41 GPL v2 or later.  There is NO WARRANTY.  See the GPL for details.
42 END
43     exit(1);
44 }
45
46 @ARGV || &usage('too few arguments');
47
48 if ($>) {
49     exec 'userv','root','groupmanage',@ARGV;
50     &quit("unable to execute userv to gain root privilege: $!");
51 }
52
53 chdir("/etc") || die "groupmanage: chdir /etc: $!\n";
54
55 $groupname= shift(@ARGV);
56 $groupname =~ y/\n//d;
57
58 $groupname =~ m/^\w[-0-9A-Za-z]*$/ ||
59     &quit("first argument is invalid - must be group name");
60
61 @ARGV || push(@ARGV,'--info');
62
63 $callinguser= exists $ENV{'USERV_UID'} ? $ENV{'USERV_UID'} : $<;
64 $callingname = exists $ENV{'USERV_USER'} ? $ENV{'USERV_USER'} : getpwuid($<);
65
66 %opt= ('user-create','0',
67        'user-create-minunameu','5',
68        'user-create-min','10000',
69        'user-create-max','19999',
70        'user-create-nameintitle','0',
71        'user-create-maxperu','5',
72        'group-file','group',
73        'gtmp-file','gtmp',
74        'grouplist-file','grouplist',
75        'name-regexp','',
76        'admin-group','',
77        'finish-command','');
78 %ovalid=  ('user-create','boolean',
79            'user-create-minunameu','number',
80            'user-create-min','number',
81            'user-create-max','number',
82            'user-create-nameintitle','boolean',
83            'user-create-maxperu','number',
84            'group-file','string',
85            'gtmp-file','string',
86            'grouplist-file','string',
87            'name-regexp','string',
88            'admin-group','string',
89            'finish-command','string');
90
91 sub ov_boolean {
92     $cov= $_ eq 'yes' ? 1 :
93           $_ eq 'no' ? 0 :
94           &quit("groupmanage.conf:$.: bad boolean value");
95 }
96
97 sub ov_number {
98     m/^[0-9]{1,10}$/ || &quit("groupmanage.conf:$.: bad numerical value");
99 }
100
101 sub ov_string {
102
103 }
104
105 open(GMC,"groupmanage.conf") || &quit("read groupmanage.conf: $!");
106 while (<GMC>) {
107     next if m/^\#/ || !m/\S/;
108     s/\s*\n$//;
109     s/^\s*([-0-9a-z]+)\s*// || &quit("groupmanage.conf:$.: bad option format");
110     $co= $1;
111     defined($opt{$co}) || &quit("groupmanage.conf:$.: unknown option $co");
112     $cov= $_;
113     $ovf= 'ov_'.$ovalid{$co};
114     &$ovf;
115     $opt{$co}= $cov;
116 }
117 close(GMC);
118
119 if ($ARGV[0] eq '--info') {
120     @ARGV == 1 || &usage('no arguments allowed after --info');
121     &p_out;
122     &load;
123     &checkexists;
124     &display;
125     &p_out;
126     exit(0);
127 }
128
129 sub naming {
130     $callinguser || return;
131     &p_out;
132     if ($opt{'user-create-minunameu'}) {
133         print(STDERR <<END) || &quit("write err re name: $!");
134 groupmanage: groups you create must be named after you ...
135     <usernamepart>-<identifier>
136  You must quote at least $opt{'user-create-minunameu'} chars of your username $createby
137  (or all of it if it is shorter).
138 END
139     }
140     if ($opt{'name-regexp'}) {
141         print(STDERR <<END) || &quit("write err re name: $!");
142 groupmanage: groups you create must match a pattern...
143   The pattern is the Perl regular expression /$opt{'name-regexp'}/.
144 END
145     }
146     exit(1);
147 }
148
149 if ($ARGV[0] eq '--create') {
150     $opt{'user-create'} || !$callinguser ||
151         ($opt{'admin-group'} &&
152          (getgrnam($opt{'admin-group'}))[3] =~ /(^| )$callingname( |$)/) ||
153         &quit("group creation by users disabled by administrator");
154     length($groupname) <= 8 || &quit("group names must be 8 chars or fewer");
155     $!=0; (@pw= getpwuid($callinguser))
156         || &quit("cannot get your passwd entry: $!");
157     $createby= $pw[0];
158     if ($opt{'user-create-minunameu'}) {
159         $groupname =~ m/^([-0-9A-Za-z]+)-([0-9a-z]+)$/ || &naming;
160         $upart= $1;
161         $idpart= $2;
162         $upart eq $createby ||
163             (length($upart) >= $opt{'user-create-minunameu'} &&
164              substr($createby,0,length($upart)) eq $upart)
165                 || &naming;
166     } else {
167         $groupname =~ m/${opt{'name-regexp'}}/ || &naming;
168     }
169     $create= 1;
170     shift(@ARGV);
171 }
172
173 &lock;
174 &load;
175
176 if ($create) {
177     $bythisowner < $opt{'user-create-maxperu'} ||
178         &quit("you already have $bythisowner group(s)");
179     $groupfileix==-1 || &quit("group already exists, cannot create it");
180     $grouplistix==-1 || &quit("group is already in grouplist, cannot create it");
181     for ($gid= $opt{'user-create-min'};
182          $gid < $opt{'user-create-max'} && defined(getgrgid($gid));
183          $gid++) { }
184     $gid <= $opt{'user-create-max'} || &quit("out of gids to use, contact admin");
185     $password=''; @members=($createby);
186     $description= "${createby}'s -- user-defined, no title";
187     $owner= $createby; @managers=(); @members= ($createby);
188     $groupfileix=$#groupfile+1;
189     $grouplistix=$#grouplist+1;
190     &p("created group $groupname");
191 } else {
192     &checkexists;
193     &p("modifying group $groupname");
194 }
195
196 &weare($owner) || grep(&weare($_),@managers) || !$callinguser ||
197     ($opt{'admin-group'} &&
198      (getgrnam($opt{'admin-group'}))[3] =~ /(^| )$callingname( |$)/) ||
199     &quit("you may not manage $groupname");
200
201 $action= 'none';
202 while (@ARGV) {
203     $_= shift(@ARGV);
204     if (m/^--(add|remove)$/) {
205         $action= $1; $clist= 'members'; $what= 'member';
206     } elsif (m/^--owner$/) {
207         !$callinguser || &quit("only root may change owner");
208         @ARGV || &usage("no username owner after --owner");
209         $owner= shift(@ARGV);
210         &p("owner set to $owner");
211     } elsif (m/^--manager-(add|remove)$/) {
212         $action= $1; $clist= 'managers'; $what= 'manager';
213     } elsif (m/^--clear$/) {
214         &p('cleared list of members');
215         @members=(); $action='none'; $memc++;
216     } elsif (m/^--manager-clear$/) {
217         &p('cleared list of managers');
218         @managers=(); $action='none';
219     } elsif (m/^--title$/) {
220         &weare($owner) || !$callinguser ||
221             &quit("only group's owner ($owner) may change title");
222         @ARGV || &usage("no title after --title");
223         $_= shift(@ARGV); y/\020-\176//cd; y/:\\//d;
224         if ($opt{'user-create-nameintitle'} &&
225             $gid >= $opt{'user-create-min'} && $gid <= $opt{'user-create-max'}) {
226             $_= "${owner}'s -- $_";
227         }
228         $description= $_;
229         &p("title set to $description");
230     } elsif (m/^-/) {
231         &usage("unknown option $_");
232     } elsif (m/^\w[-0-9A-Za-z]*$/) {
233         y/\n//d;
234         $chgu=$_;
235         getpwnam($chgu) || &quit("username $chgu does not exist");
236         eval "\@l = \@$clist; 1" || &quit("internal error: $@");
237         $already= grep($_ eq $chgu, @l);
238         if ($action eq 'add') {
239             if ($already) {
240                 &p("$chgu already $what");
241             } else {
242                 &p("added $what $chgu");
243                 push(@l,$chgu);
244                 $memc+= ($clist eq 'members');
245             }
246         } elsif ($action eq 'remove') {
247             if ($already) {
248                 &p("removed $what $chgu");
249                 @l= grep($_ ne $chgu, @l);
250                 $memc+= ($clist eq 'members');
251             } else {
252                 &p("$chgu is already not $what");
253             }
254         } else {
255             &usage("username found but no action to take for them");
256         }
257         eval "\@$clist = \@l; 1" || &quit("internal error: $@");
258     } else {
259         &usage("bad username or option $_");
260     }
261 }
262 &p("nb: a change to group membership only takes effect at the user's next login")
263     if $memc;
264 $groupfile[$groupfileix]=
265     "$groupname:$password:$gid:".join(',',@members)."\n";
266 $grouplist[$grouplistix]=
267     "$groupname:$description:$owner:".join(',',@managers).":$homedir\n";
268 &save($opt{'group-file'},@groupfile);
269 &save($opt{'grouplist-file'},@grouplist);
270 if ($opt{'finish-command'}) {
271     !system($opt{'finish-command'}) || &quit("finish-command: $!");
272 }
273 unlink($opt{'gtmp-file'}) || &quit("unlock group (remove gtmp): $!");
274 &p_out;
275 exit(0);
276
277 sub load {
278     open(GF,"< $opt{'group-file'}") || &quit("read group: $!");
279     @groupfile=<GF>; close(GF);
280     $groupfileix=-1;
281     for ($i=0; $i<=$#groupfile; $i++) {
282         $_= $groupfile[$i]; s/\n$//;
283         next if m/^\#/;
284         m/^(\w[-0-9A-Za-z]*):([^:]*):(\d+):([-0-9A-Za-z,]*)$/ ||
285             &quit("bad entry in group: $_");
286         $gname2gid{$1}=$3;
287         next unless $1 eq $groupname;
288         $groupfileix<0 || &quit("duplicate entries in group");
289         $groupfileix= $i;
290         $password= $2;
291         $gid= $3;
292         @members= split(/,/,$4);
293     }
294     open(GL,"< $opt{'grouplist-file'}") || &quit("read grouplist: $!");
295     @grouplist=<GL>; close(GL);
296     $grouplistix=-1;
297     for ($i=0; $i<=$#grouplist; $i++) {
298         $_= $grouplist[$i]; s/\n$//;
299         next if m/^\#/;
300         m/^(\w[-0-9A-Za-z]*):([^:]*):(\w[-0-9A-Za-z]*):([-0-9A-Za-z,]*):([^:]*)$/ ||
301             &quit("bad entry in grouplist: $_");
302         $bythisowner++ if ($create && $3 eq $createby &&
303                            $gname2gid{$1} >= $opt{'user-create-min'} &&
304                            $gname2gid{$1} <= $opt{'user-create-max'});
305         next unless $1 eq $groupname;
306         $grouplistix<0 || &quit("duplicate entries in grouplist");
307         $grouplistix= $i;
308         $description= $2;
309         $owner= $3;
310         $homedir= $5;
311         @managers= split(/,/,$4);
312     }
313 }
314
315 sub checkexists {
316     $grouplistix>=0 || &quit("no entry in grouplist for $groupname");
317     $groupfileix>=0 || &quit("no entry in group for $groupname");
318 }
319
320 sub weare {
321     return 0 if $_[0] eq '';
322     @pw= getpwnam($_[0]);
323     return @pw && $pw[2] == $callinguser ? 1 : 0;
324 }
325
326 sub save {
327     $filename= shift(@_);
328     unlink("$filename~");
329     open(DUMP,"> $filename.new") || &quit("create new $filename: $!");
330     print(DUMP @_) || &quit("write new $filename: $!");
331     close(DUMP) || &quit("close new $filename: $!");
332     link("$filename","$filename~") || &quit("create backup $filename: $!");
333     rename("$filename.new","$filename") || &quit("install new $filename: $!");
334 }
335
336 sub quit {
337     &unlock;
338     &p_out;
339     die "groupmanage: @_\n";
340 }
341
342 sub lock {
343     # NFS-safe Locking per Linux open(2)
344     my($hostname) = `hostname`;
345     chomp($hostname);
346     my($hitching_post) = "$opt{'gtmp-file'}.$hostname.$$";
347     open(LOCK, ">$hitching_post") || die "$hitching_post: $!";
348     close(LOCK);
349     link($hitching_post, $opt{'gtmp-file'});
350     if ((stat($hitching_post))[3] != 2) {
351         close(OUT);
352         unlink($hitching_post);
353         &quit("group file locked -- giving up...");
354     }
355     unlink($hitching_post);
356 #    link($opt{'group-file'},$opt{'gtmp-file'}) || &quit("create gtmp: $!");
357     $locked++;
358 }
359
360 sub unlock {
361     return unless $locked;
362     $locked--;
363     unlink($opt{'gtmp-file'}) || warn("unlock group file (remove gtmp): $!\n");
364 }
365
366 sub display {
367     print(<<END) || &quit("write to stdout: $!\n");
368 group       $groupname
369 gid         $gid
370 description $description
371 owner       $owner
372 managers    @managers
373 members     @members
374 homedir     $homedir
375 END
376 }
377
378 sub p_out {
379     print(STDOUT "$stdout_string") || &quit("write to stdout: $!\n");
380     $stdout_string= '';
381 }
382
383 sub p {
384     $stdout_string.= $_[0]."\n";
385 }