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