setgroups.in 8.27 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
use English;
use Getopt::Std;

#
# Set groups for users. With just a pid all the users in the group
# are modified. Of course, since we might be removing groups, we actuall
# have to go through the entire set of users in the project. Hence, you
# can provide an optional list of users to operate on; the web interface
# uses this option since it know what users have been changed via the web
# form.
#
# Note that this script does not create accounts or groups. That should 
# already have been done with other scripts.
#
sub usage()
{
    print STDOUT
	"Usage: setgroups [-b | -a] -p <pid> [user ...]\n".
        "       setgroups [-b | -a] [user ...]\n";
	
    exit(-1);
}
my  $optlist = "bap:";

#
# Configure variables
#
my $TB      = "@prefix@";
my $TBOPS   = "@TBOPSEMAIL@";
my $TBLOGS  = "@TBLOGSEMAIL@";
my $CONTROL = "@USERNODE@";
33
my $ADMINGRP= "@TBADMINGROUP@";
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278

my $SSH     = "$TB/bin/sshtb";
my $USERMOD = "/usr/sbin/pw usermod";

my $batchmode   = 0;
my $auditmode   = 0;
my $dbuid;
my @userlist;
my $pid;
my $user_name;
my $user_email;
my $logname;
my @db_row;
my $query_result;

#
# Note hardwired control node. 
# 
my $control_node = $CONTROL;

#
# Untaint the path
# 
$ENV{'PATH'} = "/bin:/usr/bin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

#
# Turn off line buffering on output
#
$| = 1;

#
# Load the Testbed support stuff. 
#
use lib "@prefix@/lib";
use libdb;
use libtestbed;

#
# We don't want to run this script unless its the real version.
#
if ($EUID != 0) {
    die("*** $0:\n".
	"    Must be setuid! Maybe its a development version?\n");
}

#
# This script is setuid, so please do not run it as root. Hard to track
# what has happened.
# 
if ($UID == 0) {
    die("*** $0:\n".
	"    Please do not run this as root! Its already setuid!\n");
}

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"b"})) {
    $batchmode = 1;
}
if (defined($options{"a"})) {
    $auditmode = 1;
}
if (defined($options{"p"})) {
    $pid = $options{"p"};

    #
    # Untaint,
    #
    if ($pid =~ /^([-\@\w]+)$/) {
	$pid = $1;
    }
    else {
	die("Bad data in pid: $pid.");
    }
}

#
# See if a userlist was provided. This is an optimization. The web interface
# knows which users actually changed, so its quicker to modify that set
# instead of the entire project set.
#
if (@ARGV) {
    # Untaint the users.
    foreach my $user ( @ARGV ) {
	if ($user =~ /^([\w]+)$/) {
	    $user = $1;
	}
	else {
	    die("Bad user name: $user.");
	}
    
	push(@userlist, $user);
    }
}

if (!defined($pid) && !defined(@userlist)) {
    usage();
}

#
# Get user DB uid.
#
if (! UNIX2DBUID($UID, \$dbuid)) {
    die("*** $0:\n".
        "    You do not exist in the Emulab Database!\n");
}

#
# Get email info.
#
if (! UserDBInfo($dbuid, \$user_name, \$user_email)) {
    die("*** $0:\n".
        "    Cannot determine email info for you!\n");
}

#
# This script always does the right thing, so it does not matter who
# calls it. But we guard it anyway in the case where ops/boss are the
# same.
# 
if (!TBAdmin($UID)) {
    #
    # Check if group_root/project_root anyplace, which indicates caller
    # has some level of responsibility.
    #
    $query_result =
	DBQueryFatal("select trust from group_membership ".
		     "where uid='$dbuid' and ".
		     "trust='project_root' or trust='group_root'");

    if ($query_result->numrows == 0) {
	die("*** $0:\n".
	    "    $dbuid does not have permission to update groups!\n");
    }
}

#
# In batch mode, go to background and send email later.
# 
if ($batchmode || $auditmode) {
    my $childpid;

    #
    # Create a temporary name for a log file.
    #
    $logname = TBMakeLogname("setgroups");
    
    if ($childpid = TBBackGround($logname)) {
	if ($auditmode) {
	    waitpid($childpid, 0);
	    exit($? >> 8);
	}
	    
	#
	# Parent exits normally
	#
	print STDOUT
	    "Updating groups.\n".
	    "You will be notified via email when the update is complete.\n";
	exit(0);
    }
}

# 
# All this stuff must be done as root (ssh).
#
$UID = $EUID;

#
# If no user list provided, we have to do this for the entire project
# member list since we have no idea who got changed.
#
if (! defined(@userlist)) {
    $query_result =
	DBQueryFatal("select uid from group_membership ".
		     "where pid='$pid' and pid=gid");

    $query_result->numrows ||
	fatal("No project members for $pid!\n");

    while (@db_row = $query_result->fetchrow_array() ) {
	push(@userlist, $db_row[0]);
    }
}

#
# Loop through user set, building up the group set and issuing commands.
#
foreach my $uid (@userlist) {
    my @groupnames;
    my @grouplist;
    my $groupargument;
    my $project;

    #
    # Form a list of project (group) membership names. We do this in two
    # steps to ensure that we get the default group membership since we
    # want that to be the user's primary group. Not sure this really matters
    # all that much, but might as well. 
    # 
    $query_result =
	DBQueryFatal("select g.unix_name from group_membership as m ".
		     "left join groups as g on m.pid=g.pid and m.gid=g.gid ".
		     "where m.uid='$uid' and m.pid=m.gid and m.trust!='none'");

    $query_result->numrows ||
	fatal("$uid is not in any groups!\n");
    
    while (@db_row = $query_result->fetchrow_array() ) {
	push(@groupnames, $db_row[0]);
    }

    $query_result =
	DBQueryFatal("select g.unix_name from group_membership as m ".
		     "left join groups as g on m.pid=g.pid and m.gid=g.gid ".
		     "where m.uid='$uid' and m.pid!=m.gid ".
		     " and m.trust!='none'");

    while (@db_row = $query_result->fetchrow_array() ) {
	push(@groupnames, $db_row[0]);
    }

    print "Processing user $uid: @groupnames\n";

    #
    # Construct an appropriate group list for the pw commands. Main project
    # is the first on the list, and that becomes the primary group. The rest
    # (if any) of the groups become a comma separated list for the -G option.
    #
    $groupargument = " ";
    $project       = shift @groupnames;
    $grouplist     = join(",",@groupnames);

    #
    # Add special groups. These are listed in the DB so that special local
    # users can have more unix groups than just the projects/groups they are
    # in. These groups must already exist. 
    #
279 280 281 282 283 284 285 286 287 288 289 290 291 292
    my @extragrouplist = TBUnixGroupList($uid);

    #
    # Add special admin group. Check to make sure that its not a dup
    # since the above mechanism could cause a duplicate entry. No big
    # deal to catch it.
    #
    if (TBAdmin($uid)) {
	if (! grep(/^${ADMINGRP}$/, @extragrouplist)) {
	    push(@extragrouplist, $ADMINGRP);
	}
    }
    
    if (@extragrouplist) {
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
	print "Adding extra groups to list: @extragrouplist\n";
    
	if ($grouplist) {
	    $grouplist = "$grouplist," . join(",", @extragrouplist);
	}
	else {
	    $grouplist = join(",", @extragrouplist);
	}
    }

    if ($grouplist) {
	$groupargument = "-G $grouplist";
    }
    else {
	$groupargument = "-G \"\"";
    }

    print "Updating user $uid record on local node.\n";

    #
    # MAKE SURE not to update anything else!
    #
    if (system("$USERMOD $uid -g $project $groupargument")) {
	fatal("Could not modify user $uid on local node.");
    }

    print "Updating user $uid record on $control_node.\n";

    if (system("$SSH $control_node ".
	       "'$USERMOD $uid -g $project $groupargument'")) {
	fatal("Could not modify user $uid record on $control_node.");
    }
}

print "Group Update Completed!\n";
if ($batchmode || $auditmode) {
    donotify("Group Update Completed!\n", 0);
    unlink($logname);
}
exit(0);

sub fatal($)
{
    my($mesg) = $_[0];

    print STDOUT "$mesg\n";

    if ($batchmode || $auditmode) {
	donotify($mesg, 1);
	unlink($logname);
    }
    exit(-1);
}

sub donotify($$)
{
    my($mesg, $iserr) = @_;
    my($subject, $from, $to, $hdrs);
    my $MAIL;

    $from    = $TBOPS;
    $hdrs    = "Reply-To: $TBOPS";
    
    #
    # An error goes just to Testbed Operations. Normal status messages go
    # to the user and to the Testbed Logs address.
    # 
    if ($iserr) {
	$subtext = "Failure";
	$to      = "$TBOPS";
    }
    else {
	$subtext = "Success";
	$to      = "$user_name <$user_email>";
	$hdrs    = "Bcc: $TBLOGS\n" . "$hdrs";
    }

    SENDMAIL($to,
371
	     "Group Update $subtext",
372 373 374
	     $mesg, $from, $hdrs,
	     ($logname));
}