Handle hostnames with upper-case letters
[webmin.git] / proftpd / userpermissions_form.cgi
1 #!/usr/bin/perl
2 # userpermissions_form.cgi
3 # Display a the list of users and their permissions
4 # Author: Mattias Gaertner
5 #
6 # Abstract:
7 #   - Allows editing the user permissions for a directory with an 
8 #     .ftpaccess file.
9 #   - It has a select field to easily add a user to the .ftpaccess file.
10 #   - Shows a list of users with their permissions.
11 #   - Provides minimum allowed commands (at the moment hardcoded in
12 #     $MiniumCommands).
13 #     These commands will applied to any new and changed permissions.
14 #   - Shows names instead of the hard to remember FTP abbreviations
15 #     (e.g. PBSZ).
16 #   - Commands can be combined. For example: RNFR and RNTO are shown 
17 #     as only one permission.
18 #   - adds automatically a DenyAll All limit, so the default is to allow
19 #     nothing.
20 #   
21 # ToDos:
22 #   - multi language support 
23 #   - a page to config the minimum commands
24 #   - a page to config the tuples (combined commands)
25 #   - Probably some functions already exists in webmin and can be replaced
26
27 require './proftpd-lib.pl';
28
29 &ReadParse();
30
31 # read .ftpaccess file
32 $file = $in{'file'};
33 $title = &text('ftpindex_header', "<tt>$in{'file'}</tt>");
34 $return = "ftpaccess_index.cgi";
35 $rmsg = $text{'ftpindex_return'};
36
37 &ui_print_header($title, "Edit User Permissions", "",
38         undef, undef, undef, undef, &restart_button());
39
40 #########################################
41 # Navigation parameters
42 foreach $h ('virt', 'idx', 'file', 'limit', 'anon', 'global') {
43     if (defined($in{$h})) {
44         $NavigationData.="<input type=hidden name=$h value='$in{$h}'>\n";
45         push(@args, "$h=$in{$h}");
46     }
47 }
48 $args = join('&', @args);
49
50
51 # These are the FTP Commands, that any user have
52 $MinimumCommands="CWD XCWD CDUP XCUP PORT PASS PASV EPRT EPSV"
53   ." PWD XPWD SIZE HELP NOOP AUTH ABORT USER LIST TYPE PROT QUIT PBSZ MDTM MODE";
54
55 $Commands{"CWD"}="Change working directory";
56 $Commands{"XCWD"}=""; 
57 $Commands{"CDUP"}="";
58 $Commands{"XCUP"}="";
59 $Commands{"PORT"}="";
60 $Commands{"PASV"}="enter passive mode";
61 $Commands{"EPRT"}="";
62 $Commands{"EPSV"}="";
63 $Commands{"RNFR"}="Rename From";
64 $Commands{"RNTO"}="Rename To";
65 $Commands{"DELE"}="Delete File";
66 $Commands{"RMD"}="Remove Directory";
67 $Commands{"XRMD"}="X Remove Directory";
68 $Commands{"MKD"}="Create Directory";
69 $Commands{"XMKD"}="X Create Directory";
70 $Commands{"MODE"}="";
71 $Commands{"PWD"}="";
72 $Commands{"XPWD"}="";
73 $Commands{"SIZE"}="";
74 $Commands{"SITE_CHMOD"}="Change Unix File Permissions";
75 $Commands{"STAT"}="Return Server Status";
76 $Commands{"SYST"}="Prints System Info";
77 $Commands{"HELP"}="";
78 $Commands{"NOOP"}="";
79 $Commands{"AUTH"}="";
80 $Commands{"PBSZ"}="";
81 $Commands{"PROT"}="";
82 $Commands{"TYPE"}="Set Transfer Type";
83 $Commands{"MODE"}="Set Transfer Mode";
84 $Commands{"MDTM"}="List Modification Time";
85 $Commands{"RETR"}="Retrieve (Read)";
86 $Commands{"STOR"}="Store (Write)";
87 $Commands{"STOU"}="Store Unique";
88 $Commands{"APPE"}="Append";
89 $Commands{"REST"}="Restart Write";
90 $Commands{"ABOR"}="Abort";
91 $Commands{"USER"}="";
92 $Commands{"PASS"}="";
93 $Commands{"LIST"}="List remote files";
94 $Commands{"QUIT"}="";
95 $Commands{"TupleRMD"} = "Remove Directory";
96 $Commands{"TupleMKD"} = "Make Directory";
97 $Commands{"TupleRN"} = "Rename";
98 $Commands{"TuplePWD"} = "Print Working Directory";
99
100 # Not implemented by proftpd:
101 #$Commands{"STRU"}="Specify File Structure";
102
103 # Here you can group commands
104 $CommandTuples{"TupleRMD"} = "RMD XRMD";
105 $CommandTuples{"TupleMKD"} = "MKD XMKD";
106 $CommandTuples{"TupleRN"} = "RNFR RNTO";
107 $CommandTuples{"TuplePWD"} = "PWD XPWD";
108
109 # Create CommandToTuple array
110 foreach $TupleName(sort keys %CommandTuples){
111     foreach $Command(split (" ",$CommandTuples{$TupleName})){
112         next unless ($Command);
113         $CommandToTuple{$Command}=$TupleName;
114     } 
115 }
116
117
118 #########################################
119 # Get user list and read old permissions
120 &GetUsers();
121 &GetFTPAccessUserPerms($file);
122
123 #########################################
124 # Parse Input and update .ftpaccess file
125
126 foreach $ParamName(keys %in){
127     #print "Name=\"$ParamName\" Value=\"".$in{$ParamName}."\"<br>\n";
128     if($ParamName eq "AddUser"){
129         $Username=$in{$ParamName};
130         if($Username =~ /^[a-zA-Z0-9_]+$/){
131             &AddUser($Username,$file);
132         }
133     }
134     if($ParamName eq "DeleteUser"){
135         $Username=$in{$ParamName};
136         if($Username =~ /^[a-zA-Z0-9_]+$/){
137             if($in{"Confirm Delete User"} eq "on"){
138                 &DeleteUser($Username,$file);
139                 #print "New used usernames: $UsedUsernames<br>\n";
140             } else {
141                 print "<H2>To really delete a user, please check the confim checkbox.</H2>\n";
142             }
143         }
144     }
145     if($ParamName eq "ChangePermissions"){
146         $Username=$in{$ParamName};
147         if($Username =~ /^[a-zA-Z0-9_]+$/){
148             &ChangePermissions($Username,$file);
149         }
150     }
151 }
152
153
154 #########################################
155 # select box and button for add user
156 print "<form action=userpermissions_form.cgi method=get>\n";
157 print $NavigationData;
158 print "<H3>Add an User to the permission table</H3>\n";
159 print "<select name=\"AddUser\">\n";
160 foreach $Username (sort split(" ",$Usernames)){
161     print "<option value=\"$Username\">$Username</option>\n";
162 }
163 print "</select>\n";
164 print "<input type=submit value=\"Add User\"><br>\n";
165 print "</form>\n";
166
167 #########################################
168 # Print Permissions
169
170 $MaxColumns=4;
171 foreach $Username(sort split (" ",$UsedUsernames)){
172     #print "User: $Username  Allowed=\"".$UserAllowedCommands{$Username}."\" Denied=\"".$UserDeniedCommands{$Username}."\"\n";
173     print "<form action=userpermissions_form.cgi method=get>\n";
174     print $NavigationData;
175     print "<input type=hidden name=\"ChangePermissions\" value=\"".$Username."\">\n";
176     print "<HR WIDTH=\"100%\">\n";
177     print "<H2>User: $Username</H2>\n";
178     print "<table border=1>\n";
179     $Column=0;
180     $Row=0;
181     foreach $Command(sort keys %Commands){
182         if($MinimumCommands =~ /$Command/i){
183             # skip minimum permissions, that all users are allowed to
184             next;
185         }
186         if($CommandToTuple{$Command}){
187             # skip commands that belong to a tuple
188             next;
189         }
190         $FTPCommands=$Command;
191         if($CommandTuples{$FTPCommands}){
192             $FTPCommands = $CommandTuples{$FTPCommands};
193         }
194
195
196         if($Column == 0){
197             if($Row==0){
198                 print "  <tr>\n";
199                 for ($i=0; $i<$MaxColumns; $i++){
200                     print "    <td>Command</td><td>Allow/Deny/Default</td>\n";
201                 }
202                 print "  </tr>\n";
203             }
204             print "  <tr>\n";
205         }
206         $CommandDesc=$Commands{$Command};
207         if(!$CommandDesc){
208             $CommandDesc = $Command;
209         }
210         print "    <td>$CommandDesc</td><td>\n";
211         if(&CommandContains($UserAllowedCommands{$Username},$FTPCommands)){
212             $AllowChecked=" checked";
213         } else {
214             $AllowChecked="";
215         }
216         if(&CommandContains($UserDeniedCommands{$Username},$FTPCommands)){
217             $DenyChecked=" checked";
218         } else {
219             $DenyChecked="";
220         }
221         if($AllowChecked || $DenyChecked){
222             $DefaultChecked = "";
223         } else {
224             $DefaultChecked = " checked";
225         }
226         print "      <input type=\"radio\" name=\"".$Command."\" value=\"allow\"".$AllowChecked.">\n";
227         print "      <input type=\"radio\" name=\"".$Command."\" value=\"deny\"".$DenyChecked.">\n";
228         print "      <input type=\"radio\" name=\"".$Command."\" value=\"default\"".$DefaultChecked.">\n";
229         print "    </td>";
230         $Column++;
231         if($Column == $MaxColumns){
232             print "  </tr>\n";
233             $Column=0;
234             $Row++;
235         }
236     }
237     if($Column > 0){
238         print "  </tr>\n";
239     }
240     print "</table>\n";
241     print "<input type=submit value=\"Change Permissions\">\n";
242     print "</form><br>\n";
243
244     print "<form action=userpermissions_form.cgi method=get>\n";
245     print $NavigationData;
246     print "<input type=hidden name=\"DeleteUser\" value=\"".$Username."\">\n";
247     print "<input type=submit value=\"Delete User Permissions\">\n";
248     print "<input type=checkbox name=\"Confirm Delete User\">I'm sure<br>\n";
249     print "</form>\n";
250 }
251
252
253 #########################################
254 # print textarea
255
256 print "<HR WIDTH=100%>\n";
257 print &text('manual_header', "<tt>$file</tt>"),"<p>\n";
258
259 print "<form action=manual_save.cgi method=post enctype=multipart/form-data>\n";
260 print $NavigationData;
261
262 print "<br><textarea rows=15 cols=80 name=directives>\n";
263 $lref = &read_file_lines($file);
264 if (!defined($start)) {
265         $start = 0;
266         $end = @$lref - 1;
267         }
268 for($i=$start; $i<=$end; $i++) {
269         print &html_escape($lref->[$i]),"\n";
270         }
271 print "</textarea><br><input type=submit value=\"$text{'save'}\"></form>\n";
272
273 #########################################
274 # print footer
275
276 &ui_print_footer("$return?$args", $rmsg);
277
278 exit;
279
280 #########################################################
281
282 sub GetUsers(){
283     my $UserCount=0;
284     setpwent();
285     while(my @uinfo = getpwent()) {
286         if ($uinfo[2] > 100) {
287                 $UserCount++;
288                 $Users[$UserCount]=$uinfo[0];
289                 $Usernames.=" ".$uinfo[0];
290         }
291     }
292     endpwent();
293 }
294
295 sub GetFTPAccessUserPerms(){
296     # Fills global variables:
297     # $UsedUsernames, %UserAllowedCommands, %UserDeniedCommands
298
299     my ($FTPAccessFile) = @_;
300
301     ##################################################
302     # Read .ftpaccess file
303     my $Commands = "";
304
305     open FTPACCESS, "$FTPAccessFile" or &error("Can't open $FTPAccessFile: $!");
306     while (my $line=<FTPACCESS>){
307         chomp $line;
308         #print $line."\n";
309         if($line =~ /<Limit (.*)>/i){
310             $Commands = $1;
311             #print "Limit $Commands\n";
312         }
313         if($line =~ /<\/Limit(.*)>/i){
314             $Commands = "";
315             #print "End Limit $Commands\n";
316         }
317         if($Commands){
318             #print "$line\n";
319             if($line =~ /AllowUser (.+)/i){
320                 my $AllowedUsernames = $1;
321                 #print "AllowUser $AllowedUsernames\n";
322                 foreach $AllowedUsername (split (" ",$AllowedUsernames)){
323                     next unless ($AllowedUsername);
324                     $UserAllowedCommands{$AllowedUsername}.=" ".$Commands;
325                     #print "AllowUser $AllowedUsername\n";
326                 }
327             }
328             if($line =~ /DenyUser (.+)/i){
329                 my $DeniedUsernames = $1;
330                 foreach $DeniedUsername (split (" ",$DeniedUsernames)){
331                     next unless ($DeniedUsername);
332                     $UserDeniedCommands{$DeniedUsername}.=" ".$Commands;
333                 }
334             }
335         }
336     }
337     close FTPACCESS;
338
339     ##################################################
340     # collect all mentioned users in table
341     $UsedUsernames="";
342     foreach $Username(keys %UserAllowedCommands){
343         #print "Adding $Username\n";
344         $UserAllowedCommands{$Username}=
345             &UnifyAndExpandCommands($UserAllowedCommands{$Username}." ".$Commands);
346         if($UsedUsernames !~ /\b$Username\b/){
347             $UsedUsernames.=$Username." ";
348         }
349     }
350     foreach $Username(keys %UserDeniedCommands){
351         $UserDeniedCommands{$Username}=
352             &UnifyAndExpandCommands($UserDeniedCommands{$Username}." ".$Commands);
353         if($UsedUsernames !~ /\b$Username\b/){
354             $UsedUsernames.=$Username." ";
355         }
356     }
357 }
358
359 sub UnifyAndExpandCommands(){
360     (my $Commands) = @_;
361     my $NewCommands = "";
362     foreach $Command(split(" ",$Commands)){
363         next unless($Command);
364         if($CommandTuples{$Command}){
365             $NewCommands.=" ".$CommandTuples{$Command};
366         } else {
367             $NewCommands.=" ".$Command;
368         }
369     }
370     return &UnifyCommands($NewCommands);
371 }
372
373 sub UnifyCommands(){
374     (my $Commands) = @_;
375     my $NewCommands = "";
376     foreach $Command(split(" ",$Commands)){
377         next unless($Command);
378         next if($NewCommands =~ /\b$Command\b/i);
379         if($NewCommands){
380             $NewCommands.=" ";
381         }
382         $NewCommands.=$Command;
383     }
384     return $NewCommands;
385 }
386
387 sub AddUser(){
388     (my $Username, $FTPAccessFile) = @_;
389
390     if($Usernames =~ /\b$Usernames\b/){
391         print "<H2>Username $Username does not exist.</H2>\n";
392         return;
393     }
394
395     if ($UserAllowedCommands{$Username} || $UserDeniedCommands{$Username}){
396         # user already exists
397         print "<H2>Username $Username already exists.</H2>\n";
398         return;
399     }
400     $UserAllowedCommands{$Username}=$MinimumCommands;
401     $UserDeniedCommands{$Username}="";
402     if($UsedUsernames !~ /\b$Username\b/){
403         $UsedUsernames.=$Username." ";
404     }
405
406     &WritePermissions($FTPAccessFile);
407 }
408
409 sub DeleteUser(){
410     (my $Username, $FTPAccessFile) = @_;
411
412     if($UsedUsernames =~ /\b$Usernames\b/){
413         print "<H2>Username $Username does not exist in table.</H2>\n";
414         return;
415     }
416
417     if ((!$UserAllowedCommands{$Username}) && (!$UserDeniedCommands{$Username})){
418         # user already deleted
419         print "<H2>Username $Username is already not in table.</H2>\n";
420         return;
421     }
422     $UserAllowedCommands{$Username}="";
423     $UserDeniedCommands{$Username}="";
424     $UsedUsernames =~ s/\b$Username\b *//;
425
426     &WritePermissions($FTPAccessFile);
427 }
428
429 sub ChangePermissions(){
430     (my $Username, $FTPAccessFile) = @_;
431
432     if($UsedUsernames =~ /\b$Usernames\b/){
433         print "<H2>Username $Username does not exist in table.</H2>\n";
434         return;
435     }
436
437     foreach $Command(keys %Commands){
438         #print "$Command value=".$in{$Command}."<br>\n";
439
440         if($CommandToTuple{$Command}){
441             # skip commands in tuples
442             next;
443         }
444
445         my $FTPCommands=$Command;
446         if($CommandTuples{$FTPCommands}){
447             $FTPCommands = $CommandTuples{$FTPCommands};
448         }
449
450         if ($in{$Command} eq "allow"){
451             $UserAllowedCommands{$Username}.=" ".$FTPCommands;
452             #print "Allow $Username $Command<br>\n";
453         } else {
454             $UserAllowedCommands{$Username} =
455                 &RemoveCommands($UserAllowedCommands{$Username},$FTPCommands);
456         }
457         if ($in{$Command} eq "deny"){
458             $UserDeniedCommands{$Username}.=" ".$FTPCommands;
459             #print "Deny $Username $Command<br>\n";
460         } else {
461             $UserDeniedCommands{$Username} =
462                 &RemoveCommands($UserDeniedCommands{$Username},$FTPCommands);
463         }
464     }
465     $UserAllowedCommands{$Username}=
466         &UnifyCommands($MinimumCommands." ".$UserAllowedCommands{$Username});
467     $UserDeniedCommands{$Username}=
468         &UnifyCommands($UserDeniedCommands{$Username});
469
470     &WritePermissions($FTPAccessFile);
471 }
472
473 sub WritePermissions(){
474     # Read .ftpaccess file, remove all user command permissions
475     # and add new set of user permissions
476     (my $FTPAccessFile) = @_;
477     my $NewConfig = "";
478     my $OldCommands = "";
479     my $Username;
480
481     # Lock .ftpaccess file
482     &lock_file($FTPAccessFile);
483     &lock_file($FTPAccessFile);
484
485
486     # Read old .ftpaccess file
487     open FTPACCESS, "$FTPAccessFile" or die "Can't read $FTPAccessFile: $!";
488     $DenyAllBlockFound = 0;
489     while(my $line = <FTPACCESS>){
490         my $ShortLine = $line;
491         chomp $ShortLine;
492         #print $ShortLine."\n";
493         if($ShortLine =~ /<Limit (.*)>/i){
494             # start of Limit block
495             $OldCommands = $1;
496             #print "Limit $OldCommands\n";
497             $LimitBlock = $line;
498             $ImportantLimitLineFound = 0;
499             $DenyAllFound = 0;
500         } elsif($ShortLine =~ /<\/Limit(.*)>/i){
501             # end of Limit block
502             #print "End Limit $OldCommands\n";
503             $LimitBlock .= $line;
504             if($ImportantLimitLineFound){
505                 $NewConfig .= $LimitBlock;
506             }
507             if(($OldCommands =~ /\bALL\b/i) && ($DenyAllFound)){
508                 # this was a DenyAll for All commands block
509                 $DenyAllBlockFound = 1;
510             }
511             $OldCommands = "";
512         } elsif($OldCommands){
513             #print "$ShortLine\n";
514             if($ShortLine =~ /AllowUser (.*)/i){
515                 # AllowUser line -> will be replaced, not important
516             } elsif($ShortLine =~ /DenyUser (.*)/i){
517                 # DenyUser line -> will be replaced, not important
518             } elsif($ShortLine =~ /^ +$/){
519                 # empty line -> not important, but keep it for readability
520                 $LimitBlock .= $line;
521             } else {
522                 # other limit directive -> important
523                 $LimitBlock .= $line;
524                 $ImportantLimitLineFound = 1;
525                 if($ShortLine =~ /\bDenyAll\b/i){
526                     $DenyAllFound = 1;
527                 }
528             }
529         } else {
530             # other directives -> keep
531             $NewConfig .= $line;
532         }
533     }
534     close FTPACCESS;
535
536     # Append new directives
537
538     # Append DenyAll block if not already there
539     if(!$DenyAllBlockFound){
540         $NewConfig.="<Limit All>\n";
541         $NewConfig.="  DenyAll\n";
542         $NewConfig.="</Limit>\n";
543     }
544
545     # Append Limit blocks for users
546     foreach $Username (sort split(" ",$Usernames)){
547         my $CurAllow = $UserAllowedCommands{$Username};
548         if ($CurAllow){
549             $NewConfig.="<Limit ".$CurAllow.">\n";
550             $NewConfig.="  AllowUser ".$Username."\n";
551             $NewConfig.="</Limit>\n";
552         }
553         my $CurDeny = $UserDeniedCommands{$Username};
554         if ($CurDeny){
555             $NewConfig.="<Limit ".$CurDeny.">\n";
556             $NewConfig.="  DenyUser ".$Username."\n";
557             $NewConfig.="</Limit>\n";
558         }
559     }
560     #print "<br>\n".$NewConfig."<br>\n";
561
562     # Write new .ftpaccess file
563     open FTPACCESS, "> $FTPAccessFile" or die "Can't append to $FTPAccessFile: $!";
564     print FTPACCESS $NewConfig;
565     close FTPACCESS;
566
567     # Unlock .ftpaccess file
568     &unlock_file($FTPAccessFile);
569
570     $logtype = 'ftpaccess'; 
571     $logname = $in{'file'};
572     &webmin_log($logtype, "user permissions", $logname, \%in);
573 }
574
575 sub CommandContains(){
576     (my $Commands, my $SubSet) = @_;
577     foreach my $Command(split(" ",$SubSet)){
578         next unless($Command);
579         if($Commands =~ /\b$Command\b/i){
580             return 1;
581         }
582     }
583     return 0;
584 }
585
586 sub RemoveCommands(){
587     (my $Commands, my $SubSet) = @_;
588     foreach my $Command(split(" ",$SubSet)){
589         next unless($Command);
590         $Commands =~ s/\b$Command\b *//gi;
591     }
592     return $Commands;
593 }
594
595 # end.