Batch group creation / deletion
authorJamie Cameron <jcameron@webmin.com>
Sun, 14 Dec 2008 03:54:39 +0000 (03:54 +0000)
committerJamie Cameron <jcameron@webmin.com>
Sun, 14 Dec 2008 03:54:39 +0000 (03:54 +0000)
useradmin/CHANGELOG
useradmin/gbatch_exec.cgi [new file with mode: 0755]
useradmin/gbatch_form.cgi [new file with mode: 0755]
useradmin/lang/en
useradmin/log_parser.pl

index 08a0278..0b9dc9f 100644 (file)
@@ -53,3 +53,4 @@ Improved support for default password changing options on AIX.
 Added a non-editable list of users who have this group as their primary to the Edit Group page.
 Added a Module Config option to use a text box for entering secondary group members, rather than the left/right user chooser.
 Created a page for exporting groups to a batch file, for importing on other systems.
+Added support for creating, deleting and modifying groups from a batch file. This is similar to the long-standing batch user management functionality.
diff --git a/useradmin/gbatch_exec.cgi b/useradmin/gbatch_exec.cgi
new file mode 100755 (executable)
index 0000000..76ea4bc
--- /dev/null
@@ -0,0 +1,418 @@
+#!/usr/local/bin/perl
+# Execute create/modify/delete group commands in a batch file
+
+require './user-lib.pl';
+$access{'batch'} || &error($text{'gbatch_ecannot'});
+if ($ENV{'REQUEST_METHOD'} eq 'GET') {
+       &ReadParse();
+       }
+else {
+       &ReadParseMime();
+       }
+if ($in{'source'} == 0) {
+       $data = $in{'file'};
+       $data =~ /\S/ || &error($text{'batch_efile'});
+       }
+elsif ($in{'source'} == 1) {
+       open(LOCAL, $in{'local'}) || &error($text{'batch_elocal'});
+       while(<LOCAL>) {
+               $data .= $_;
+               }
+       close(LOCAL);
+       }
+elsif ($in{'source'} == 2) {
+       $data = $in{'text'};
+       $data =~ /\S/ || &error($text{'batch_etext'});
+       }
+
+&ui_print_unbuffered_header(undef, $text{'gbatch_title'}, "");
+
+# Force defaults for save options
+$in{'chgid'} = 1 if (!$access{'chgid'});
+
+# Work out a good base GID for new groups
+&build_group_used(\%gused, \%gtaken);
+$newgid = int($config{'base_gid'} > $access{'lowgid'} ?
+             $config{'base_gid'} : $access{'lowgid'});
+
+# Process the file
+&batch_start() if ($in{'batch'});
+&lock_user_files();
+$lnum = $created = $modified = $deleted = 0;
+print "<pre>\n";
+$pft = &passfiles_type();
+foreach $line (split(/[\r\n]+/, $data)) {
+       $lnum++;
+       $line =~ s/^\s*#.*$//;
+       next if ($line !~ /\S/);
+       local @line = split(/:/, $line, -1);
+       local %group;
+       if ($line[0] eq 'create') {
+               # Creating a new group
+
+               # Validate line
+               if (!$line[1]) {
+                       print &text('batch_eline', $lnum),"\n";
+                       next;
+                       }
+               if (@line != 5) {
+                       print &text('batch_elen', $lnum, 5),"\n";
+                       next;
+                       }
+               if ($line[1] !~ /^[^:\t]+$/) {
+                       print &text('gbatch_egroupname', $lnum),"\n";
+                       next;
+                       }
+               $group{'group'} = $line[1];
+
+               if ($gtaken{$group{'group'}}) {
+                       print &text('gbatch_egroup', $lnum,
+                                   $group{'group'}),"\n";
+                       next;
+                       }
+               if ($line[3] !~ /^\d+$/) {
+                       # make up a GID
+                       while($gused{$newgid}) {
+                               $newgid++;
+                               }
+                       $group{'gid'} = $newgid;
+                       }
+               else {
+                       # use the given UID
+                       if ($gused{$line[3]} && !$access{'gmultiple'}) {
+                               print &text('gbatch_ecaccess', $lnum,
+                                           $text{'gsave_egidused2'}),"\n";
+                               next;
+                               }
+                       $group{'gid'} = $line[3];
+                       }
+               $gused{$group{'gid'}}++;
+               $group{'members'} = $line[4];
+
+               # Check access control restrictions
+               if (!$access{'gcreate'}) {
+                       print &text('gbatch_ecaccess', $lnum,
+                                   $text{'gsave_ecreate'});
+                       next;
+                       }
+               local $ch = &check_group(\%group);
+               if ($ch) {
+                       print &text('gbatch_ecaccess', $lnum, $ch),"\n";
+                       next;
+                       }
+
+               if ($line[2] eq '') {
+                       # No password needed
+                       $group{'pass'} = '';
+                       $group{'passmode'} = 0;
+                       }
+               else {
+                       # Normal password
+                       $group{'pass'} = &encrypt_password($line[2]);
+                       $group{'passmode'} = 3;
+                       $group{'plainpass'} = $line[2];
+                       }
+
+               # Run the before command
+               &set_user_envs(\%group, 'CREATE_GROUP');
+               $merr = &making_changes();
+               &error(&text('gsave_emaking', "<tt>$merr</tt>"))
+                       if (defined($merr));
+
+               # Create the group!
+               &create_group(\%group);
+
+               # All done
+               &made_changes();
+
+               # Call other modules, ignoring any failures
+               $error_must_die = 1;
+               eval {
+                       &other_modules("useradmin_create_group", \%group)
+                               if ($access{'cothers'} == 1 && $in{'others'} ||
+                                   $access{'cothers'} == 0);
+                       };
+               $other_err = $@;
+               $error_must_die = 0;
+
+               print "<b>",&text('gbatch_created', $group{'group'}),"</b>\n";
+               print "<b><i>",&text('batch_eother', $other_err),"</i></b>\n"
+                       if ($other_err);
+               $created++;
+               }
+       elsif ($line[0] eq 'delete') {
+               # Deleting an existing group
+               if (@line != 2) {
+                       print &text('batch_elen', $lnum, 2),"\n";
+                       next;
+                       }
+               local @glist = &list_groups();
+               local ($group) = grep { $_->{'group'} eq $line[1] } @glist;
+               if (!$group) {
+                       print &text('gbatch_enogroup', $lnum, $line[1]),"\n";
+                       next;
+                       }
+
+               # Check if deletion is allowed
+               if (!&can_edit_group(\%access, $group)) {
+                       print &text('gbatch_edaccess', $lnum,
+                                   $text{'gdel_egroup'}),"\n";
+                       next;
+                       }
+               if (!$config{'delete_root'} && $group->{'gid'} <= 10) {
+                       print &text('gbatch_edaccess', $lnum,
+                                   $text{'gdel_egroup'}),"\n";
+                       next;
+                       }
+
+               # Check if has primary members
+               local $prim;
+               foreach $u (&list_users()) {
+                       if ($u->{'gid'} == $group->{'gid'}) {
+                               $prim = $u;
+                               last;
+                               }
+                       }
+               if ($prim) {
+                       print &text('gbatch_eprimary', $lnum,
+                                   $prim->{'user'}),"\n";
+                       next;
+                       }
+
+               # Run the before command
+               &set_user_envs($group, 'DELETE_GROUP');
+               $merr = &making_changes();
+               &error(&text('usave_emaking', "<tt>$merr</tt>"))
+                       if (defined($merr));
+
+               # Delete from other modules, ignoring errors
+               $error_must_die = 1;
+               eval {
+                       &other_modules("useradmin_delete_group", $group)
+                               if ($access{'dothers'} == 1 && $in{'others'} ||
+                                   $access{'dothers'} == 0);
+                       };
+               $other_err = $@;
+               $error_must_die = 0;
+
+               # Delete the user entry
+               &delete_group($group);
+
+               &made_changes();
+
+               print "<b>",&text('gbatch_deleted',$group->{'group'}),"</b>\n";
+               print "<b><i>",&text('batch_eother', $other_err),"</i></b>\n"
+                       if ($other_err);
+               $deleted++;
+               }
+       elsif ($line[0] eq 'modify') {
+               # Modifying an existing user
+               local $wlen = $pft == 5 ? 11 :
+                             $pft == 4 ? 13 :
+                             $pft == 2 ? 14 :
+                             $pft == 1 || $pft == 6 ? 12 : 9;
+               if (@line != $wlen) {
+                       print &text('batch_elen', $lnum, $wlen),"\n";
+                       next;
+                       }
+               local @ulist = &list_users();
+               local ($user) = grep { $_->{'user'} eq $line[1] } @ulist;
+               if (!$user) {
+                       print &text('batch_enouser', $lnum, $line[1]),"\n";
+                       next;
+                       }
+               %olduser = %user = %$user;
+               $user{'olduser'} = $user->{'user'};
+               if (!&can_edit_user(\%access, \%user)) {
+                       print &text('batch_emaccess', $lnum,
+                                   $text{'usave_eedit'}),"\n";
+                       next;
+                       }
+
+               # Update supplied fields
+               if ($line[2] ne '') {
+                       if (!$access{'urename'}) {
+                               print &text('batch_erename', $lnum, $line[1]),"\n";
+                               }
+                       $user{'user'} = $line[2];
+                       }
+               if ($in{'crypt'} && $line[3] ne '') {
+                       # Changing to pre-encrypted password
+                       $user{'pass'} = $line[3];
+                       $user{'passmode'} = 2;
+                       }
+               elsif ($line[3] eq 'x') {
+                       # No login allowed
+                       $user{'pass'} = $config{'lock_string'};
+                       $user{'passmode'} = 1;
+                       }
+               elsif ($line[3] ne '') {
+                       # Normal password
+                       $user{'pass'} = &encrypt_password($line[3]);
+                       $user{'passmode'} = 3;
+                       $user{'plainpass'} = $line[3];
+                       }
+               else {
+                       # No change
+                       $user{'passmode'} = 4;
+                       }
+               $user{'uid'} = $line[4] if ($line[4] ne '');
+               $user{'gid'} = $line[5] if ($line[5] ne '');
+               $user{'real'} = $line[6] if ($line[6] ne '');
+               $user{'home'} = $line[7] if ($line[7] ne '');
+               $user{'shell'} = $line[8] if ($line[8] ne '');
+               if ($access{'peopt'}) {
+                       if ($pft == 5) {
+                               # Openserver password and short shadow
+                               $user{'min'}=$line[9] if ($line[9] ne '');
+                               $user{'max'}=$line[10] if ($line[10] ne '');
+                               $user{'change'}=int(time() / (60*60*24))
+                                       if ($line[3] ne '');
+                               }
+                       elsif ($pft == 4) {
+                               # AIX password and security information
+                               $user{'min'}=$line[9] if ($line[9] ne '');
+                               $user{'max'}=$line[10] if ($line[10] ne '');
+                               $user{'expire'}=$line[11] if ($line[11] ne '');
+                               if ($line[12] ne '') {
+                                       delete($user{'admin'});
+                                       delete($user{'admchg'});
+                                       delete($user{'nocheck'});
+                                       map { $user{$_}++ }
+                                           split(/\s+/, $line[12]);
+                                       }
+                               $user{'change'}=time() if ($line[3] ne '');
+                               }
+                       elsif ($pft == 2) {
+                               # SYSV-style passwd and shadow information
+                               $user{'min'}=$line[9] if ($line[9] ne '');
+                               $user{'max'}=$line[10] if ($line[10] ne '');
+                               $user{'warn'}=$line[11] if ($line[11] ne '');
+                               $user{'inactive'}=$line[12]
+                                       if ($line[12] ne '');
+                               $user{'expire'}=$line[13] if ($line[13] ne '');
+                               $user{'change'}=int(time() / (60*60*24))
+                                       if ($line[3] ne '');
+                               }
+                       elsif ($pft == 1 || $pft == 6) {
+                               # BSD master.passwd information
+                               $user{'class'}=$line[9] if ($line[9] ne '');
+                               $user{'change'}=$line[10] if ($line[10] ne '');
+                               $user{'expire'}=$line[11] if ($line[11] ne '');
+                               }
+                       }
+
+               # Check access control restrictions
+               local $ch = &check_user(\%user, \%olduser);
+               if ($ch) {
+                       print &text('batch_emaccess', $lnum, $ch),"\n";
+                       next;
+                       }
+
+               # Run the before command
+               &set_user_envs(\%user, 'MODIFY_USER', $user{'plainpass'},
+                              [ &secondary_groups($user{'user'}) ]);
+               $merr = &making_changes();
+               &error(&text('usave_emaking', "<tt>$merr</tt>"))
+                       if (defined($merr));
+
+               # Move home directory if needed
+               if ($olduser{'home'} ne $user{'home'} && $in{'movehome'} &&
+                   $user{'home'} ne '/' && $olduser{'home'} ne '/') {
+                       if (-d $olduser{'home'} && !-e $user{'home'}) {
+                               local $out = &backquote_logged(
+                                       "mv \"$olduser{'home'}\" ".
+                                       "\"$user{'home'}\" 2>&1");
+                               if ($?) { &error(&text('batch_emove',
+                                                $lnum, $out)); }
+                               }
+                       }
+
+               # Change UIDs and GIDs
+               if ($olduser{'gid'} != $user{'gid'} && $in{'chgid'}) {
+                       if ($in{'chgid'} == 1) {
+                               &recursive_change($user{'home'},$olduser{'uid'},
+                                         $olduser{'gid'}, -1, $user{'gid'});
+                               }
+                       else {
+                               &recursive_change("/", $olduser{'uid'},
+                                         $olduser{'gid'}, -1, $user{'gid'});
+                               }
+                       }
+               if ($olduser{'uid'} != $user{'uid'} && $in{'chuid'}) {
+                       if ($in{'chuid'} == 1) {
+                               &recursive_change($user{'home'},$olduser{'uid'},
+                                                 -1, $user{'uid'}, -1);
+                               }
+                       else {
+                               &recursive_change("/", $olduser{'uid'},
+                                                 -1, $user{'uid'}, -1);
+                               }
+                       }
+
+               # Actually modify the user
+               &modify_user(\%olduser, \%user);
+
+               # If the user has been renamed, update any secondary groups
+               if ($olduser{'user'} ne $user{'user'}) {
+                       foreach $group (@glist) {
+                               local @mems = split(/,/, $group->{'members'});
+                               local $idx = &indexof($olduser{'user'}, @mems);
+                               if ($idx >= 0) {
+                                       $mems[$idx] = $user{'user'};
+                                       $group->{'members'} = join(",", @mems);
+                                       &modify_group($group, $group);
+                                       }
+                               }
+                       }
+
+               &made_changes();
+
+               # Modify in other modules, ignoring errors
+               $error_must_die = 1;
+               eval {
+                       &other_modules("useradmin_modify_user",
+                                      \%user, \%olduser)
+                               if ($access{'mothers'} == 1 && $in{'others'} ||
+                                   $access{'mothers'} == 0);
+                       };
+               $error_must_die = 0;
+               $other_err = $@;
+
+               print "<b>",&text('batch_modified',$olduser{'user'}),"</b>\n";
+               print "<b><i>",&text('batch_eother', $other_err),"</i></b>\n"
+                       if ($other_err);
+               $modified++;
+               }
+       else {
+               print &text('batch_eaction', $lnum, $line[0]),"\n";
+               next;
+               }
+       }
+print "</pre>\n";
+&batch_end() if ($in{'batch'});
+&unlock_user_files();
+&webmin_log("gbatch", undef, $in{'source'} == 1 ? $in{'local'} : undef,
+           { 'created' => $created, 'modified' => $modified,
+             'deleted' => $deleted, 'lnum' => $lnum } );
+
+&ui_print_footer("gbatch_form.cgi", $text{'batch_return'},
+                "index.cgi?mode=groups", $text{'index_return'});
+
+# check_group(\%group, [\%oldgroup])
+# Check access control restrictions for a group
+sub check_group
+{
+# check if gid is within range
+if ($access{'lowgid'} && $_[0]->{'gid'} < $access{'lowgid'}) {
+       return &text('usave_elowgid', $access{'lowuid'});
+       }
+if ($access{'hiuid'} && $_[0]->{'uid'} > $access{'hiuid'}) {
+       return &text('usave_ehiuid', $access{'hiuid'});
+       }
+if ($_[1] && !$access{'ggid'} && $_[1]->{'gid'} != $_[0]->{'gid'}) {
+       return $text{'gsave_eggid'};
+       }
+return undef;
+}
+
diff --git a/useradmin/gbatch_form.cgi b/useradmin/gbatch_form.cgi
new file mode 100755 (executable)
index 0000000..572d9bd
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/local/bin/perl
+# Display a form for doing batch group creation, updates or deletion from
+# a text file
+
+require './user-lib.pl';
+$access{'batch'} || &error($text{'gbatch_ecannot'});
+&ui_print_header(undef, $text{'gbatch_title'}, "");
+
+# Instructions
+print &ui_hidden_start($text{'batch_instr'}, "instr", 0, "batch_form.cgi");
+print "$text{'gbatch_desc'}<p>\n";
+print "$text{'gbatch_descafter'}<br>\n";
+print "$text{'gbatch_descafter2'}\n";
+print &ui_hidden_end("instr");
+
+print &ui_form_start("gbatch_exec.cgi", "form-data");
+print &ui_table_start($text{'gbatch_header'}, undef, 2);
+
+# Source file
+print &ui_table_row($text{'batch_source'},
+       &ui_radio_table("source", 0,
+         [ [ 0, $text{'batch_source0'}, &ui_upload("file") ],
+           [ 1, $text{'batch_source1'}, &ui_textbox("local", undef, 40)." ".
+                                        &file_chooser_button("local") ],
+           [ 2, $text{'batch_source2'}, &ui_textarea("text", undef, 5, 60) ]
+         ]));
+
+if ($access{'cothers'} == 1 || $access{'mothers'} == 1 ||
+    $access{'dothers'} == 1) {
+       # Do other modules?
+       print &ui_table_row($text{'gbatch_others'},
+               &ui_yesno_radio("others", int($config{'default_other'})));
+       }
+
+# Only run post-command at end?
+print &ui_table_row($text{'gbatch_batch'},
+       &ui_yesno_radio("batch", 0));
+
+if ($access{'chgid'}) {
+       # Update GIDs on files
+       print &ui_table_row($text{'gbatch_chgid'},
+               &ui_radio("chgid", 1, [ [ 0, $text{'no'} ],
+                                       [ 1, $text{'home'} ],
+                                       [ 2, $text{'uedit_allfiles'} ] ]));
+       }
+
+print &ui_table_end();
+print &ui_form_end([ [ undef, $text{'batch_upload'} ] ]);
+
+&ui_print_footer("", $text{'index_return'});
+
index 8d8c25d..deb1869 100644 (file)
@@ -357,6 +357,10 @@ log_batch=Executed batch file $1
 log_batch_l=Executed batch file $1 ($2 created, $3 modified, $4 deleted)
 log_ubatch=Executed uploaded batch file
 log_ubatch_l=Executed uploaded batch file ($1 created, $2 modified, $3 deleted)
+log_gbatch=Executed group batch file $1
+log_gbatch_l=Executed group batch file $1 ($2 created, $3 modified, $4 deleted)
+log_ugbatch=Executed uploaded group batch file
+log_ugbatch_l=Executed uploaded group batch file ($1 created, $2 modified, $3 deleted)
 
 batch_title=Execute Batch File
 batch_ecannot=You cannot use the batch file form
@@ -495,3 +499,23 @@ emass_pass=Unlocking password ..
 emass_doing=Enabling user $1 ..
 emass_already=.. already enabled!
 
+gbatch_title=Execute Group Batch File
+gbatch_ecannot=You cannot use the group batch file form
+batch_desc=This form allows you to create, modify or delete many groups at once from an uploaded or local text file. Each line in the file specifies one action to take, depending on its first field. The line formats are :
+gbatch_desc=<b>create</b>:groupname:passwd:gid:member,member,...<p><b>modify</b>:oldgroupname:groupname:passwd:gid:member,member,...<p><b>delete</b>:groupname
+gbatch_descafter=In <b>create</b> lines, if the <tt>gid</tt> field is left empty, Webmin will assign a GID automatically.
+gbatch_descafter2=In <b>modify</b> lines, an empty field will be taken to mean that the corresponding group attribute is not to be modified.
+gbatch_header=Batch group creation, update and deletion options
+gbatch_others=Create, modify or delete groups in other modules?
+gbatch_batch=Only update groups file when batch is complete?
+gbatch_chgid=Change GID on files of modified groups?
+gbatch_ecaccess=You are not allowed to create the group at line $1 : $2
+gbatch_emaccess=You are not allowed to modify the group at line $1 : $2
+gbatch_edaccess=You are not allowed to delete the group at line $1 : $2
+gbatch_created=Created group $1
+gbatch_deleted=Deleted group $1
+gbatch_modified=Modified group $1
+gbatch_enogroup=Group does not exist at line $1 : $2
+gbatch_eprimary=Group at line $1 cannot be deleted, as it is the primary group of user $2.
+gbatch_egroup=Duplicate group name at line $1 : $2
+gbatch_egroupname=Invalid group name at line $1
index f2302b0..0984192 100644 (file)
@@ -40,14 +40,14 @@ elsif ($type eq 'group') {
                return &text('log_gdelete', "<tt>$object</tt>");
                }
        }
-elsif ($action eq 'batch') {
+elsif ($action eq 'batch' || $action eq 'gbatch') {
        if ($object =~ /^\//) {
-               return &text($long ? 'log_batch_l' : 'log_batch',
+               return &text($long ? 'log_'.$action.'_l' : 'log_'.$action,
                             "<tt>$object</tt>", $p->{'created'},
                             $p->{'modified'}, $p->{'deleted'});
                }
        else {
-               return &text($long ? 'log_ubatch_l' : 'log_ubatch',
+               return &text($long ? 'log_u'.$action.'_l' : 'log_u'.$action,
                             $p->{'created'}, $p->{'modified'},$p->{'deleted'});
                }
        }