Parent Directory
|
Revision Log
test development branch
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/Authz.pm,v 1.36 2007/08/13 22:59:54 sh002i Exp $ 5 # 6 # This program is free software; you can redistribute it and/or modify it under 7 # the terms of either: (a) the GNU General Public License as published by the 8 # Free Software Foundation; either version 2, or (at your option) any later 9 # version, or (b) the "Artistic License" which comes with this package. 10 # 11 # This program is distributed in the hope that it will be useful, but WITHOUT 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the 14 # Artistic License for more details. 15 ################################################################################ 16 17 package WeBWorK::Authz; 18 19 =head1 NAME 20 21 WeBWorK::Authz - check user permissions. 22 23 =head1 SYNOPSIS 24 25 # create new authorizer -- $r is a WeBWorK::Request object. 26 my $authz = new WeBWorK::Authz($r); 27 28 # tell authorizer to cache permission level of user spammy. 29 $authz->setCachedUser("spammy"); 30 31 # this call will use the cached data. 32 if ($authz->hasPermissions("spammy", "eat_breakfast")) { 33 eat_breakfast(); 34 } 35 36 # this call will not use the cached data, and will cause a database lookup. 37 if ($authz->hasPermissions("hammy", "go_to_bed")) { 38 go_to_bed(); 39 } 40 41 =head1 DESCRIPTION 42 43 WeBWorK::Authen determines if a user is authorized to perform a specific 44 activity, based on the user's PermissionLevel record in the WeBWorK database and 45 the contents of the %permissionLevels hash in the course environment. 46 47 =head2 Format of the %permissionLevels hash 48 49 %permissionLevels maps text strings describing activities to numeric permission 50 levels. The definitive list of activities is contained in the default version of 51 %permissionLevels, in the file F<conf/global.conf.dist>. 52 53 A user is able to engage in an activity if their permission level is greater 54 than or equal to the level associated with the activity. If the level associated 55 with an activity is undefiend, then no user is permitted to perform the 56 activity, regardless of their permission level. 57 58 =cut 59 60 use strict; 61 use warnings; 62 use Carp qw/croak/; 63 # FIXME SET: set-level auth add 64 use WeBWorK::Utils qw(before after between); 65 use WeBWorK::Authen::Proctor; 66 use Net::IP; 67 68 ################################################################################ 69 70 =head1 CONSTRUCTOR 71 72 =over 73 74 =item WeBWorK::Authz->new($r) 75 76 Creates a new authorizer instance. $r is a WeBWorK::Request object. It must 77 already have its C<ce> and C<db> fields set. 78 79 =cut 80 81 sub new { 82 my ($invocant, $r) = @_; 83 my $class = ref($invocant) || $invocant; 84 my $self = { 85 r => $r, 86 }; 87 88 bless $self, $class; 89 return $self; 90 } 91 92 =back 93 94 =cut 95 96 ################################################################################ 97 98 =head1 METHODS 99 100 =over 101 102 =item setCachedUser($userID) 103 104 Caches the PermissionLevel of the user $userID in an existing authorizer. If a 105 user's PermissionLevel is cached, it will be used whenever hasPermissions() is 106 called on the same user. Only one user can be cached at a time. This is used by 107 WeBWorK to cache the "real" user. 108 109 =cut 110 111 sub setCachedUser { 112 my ($self, $userID) = @_; 113 my $r = $self->{r}; 114 my $db = $r->db; 115 116 delete $self->{userID}; 117 delete $self->{PermissionLevel}; 118 119 if (defined $userID) { 120 $self->{userID} = $userID; 121 my $PermissionLevel = $db->getPermissionLevel($userID); # checked 122 if (defined $PermissionLevel) { 123 # store permission level record in database to avoid later database calls 124 $self->{PermissionLevel} = $PermissionLevel; 125 } 126 } else { 127 warn "setCachedUser() called with userID undefined."; 128 } 129 } 130 131 =item hasPermissions($userID, $activity) 132 133 Checks the %permissionLevels hash in the course environment to determine if the 134 user $userID has permission to engage in the activity $activity. If the user's 135 permission level is greater than or equal to the level associated with $activty, 136 a true value is returned. Otherwise, a false value is returned. 137 138 If $userID has been cached using the setCachedUser() call, the cached data is 139 used. Otherwise, the user's PermissionLevel is looked up in the WeBWorK 140 database. 141 142 If the user does not have a PermissionLevel record, the permission level record 143 is empty, or the activity does not appear in %permissionLevels, hasPermissions() 144 assumes that the user does not have permission. 145 146 =cut 147 148 # This currently only uses two of it's arguments, but it accepts any number, in 149 # case in the future calculating certain permissions requires more information. 150 sub hasPermissions { 151 if (@_ != 3) { 152 shift @_; # get rid of self 153 my $nargs = @_; 154 croak "hasPermissions called with $nargs arguments instead of the expected 2: '@_'" 155 } 156 157 my ($self, $userID, $activity) = @_; 158 my $r = $self->{r}; 159 my $ce = $r->ce; 160 my $db = $r->db; 161 162 # this may need to be changed if we get other permission level data sources 163 return 0 unless defined $db; 164 165 # this may need to be changed if we want to control what unauthenticated users 166 # can do with the permissions system 167 return 0 unless defined $userID and $userID ne ""; 168 169 my $PermissionLevel; 170 171 my $cachedUserID = $self->{userID}; 172 if (defined $cachedUserID and $cachedUserID ne "" and $cachedUserID eq $userID) { 173 # this is the same user -- we can skip the database call 174 $PermissionLevel = $self->{PermissionLevel}; 175 } else { 176 # a different user, or no user was defined before 177 #my $prettyCachedUserID = defined $cachedUserID ? "'$cachedUserID'" : "undefined"; 178 #warn "hasPermissions called with user '$userID', but cached user is $prettyCachedUserID. Accessing database.\n"; 179 $PermissionLevel = $db->getPermissionLevel($userID); # checked 180 } 181 182 my $permission_level; 183 184 if (defined $PermissionLevel) { 185 $permission_level = $PermissionLevel->permission; 186 } else { 187 # uh, oh. this user has no permission level record! 188 warn "User '$userID' has no PermissionLevel record -- assuming no permission."; 189 return 0; 190 } 191 192 unless (defined $permission_level and $permission_level ne "") { 193 warn "User '$userID' has empty permission level -- assuming no permission."; 194 return 0; 195 } 196 197 my $userRoles = $ce->{userRoles}; 198 my $permissionLevels = $ce->{permissionLevels}; 199 200 if (exists $permissionLevels->{$activity}) { 201 my $activity_role = $permissionLevels->{$activity}; 202 if (defined $activity_role) { 203 if (exists $userRoles->{$activity_role}) { 204 my $role_permlevel = $userRoles->{$activity_role}; 205 if (defined $role_permlevel) { 206 return $permission_level >= $role_permlevel; 207 } else { 208 warn "Role '$activity_role' has undefined permisison level -- assuming no permission."; 209 return 0; 210 } 211 } else { 212 warn "Role '$activity_role' for activity '$activity' not found in \%userRoles -- assuming no permission."; 213 return 0; 214 } 215 } else { 216 return 0; # undefiend $activity_role, no one has permission to perform $activity 217 } 218 } else { 219 warn "Activity '$activity' not found in \%permissionLevels -- assuming no permission."; 220 return 0; 221 } 222 } 223 224 #### set-level authorization routines 225 226 sub checkSet { 227 my $self = shift; 228 my $r = $self->{r}; 229 my $ce = $r->ce; 230 my $db = $r->db; 231 my $urlPath = $r->urlpath; 232 233 my $node_name = $urlPath->type; 234 235 # first check to see if we have to worried about set-level access 236 # restrictions 237 return 0 unless (grep {/^$node_name$/} 238 (qw(problem_list problem_detail gateway_quiz 239 proctored_gateway_quiz))); 240 241 # to check set restrictions we need a set and a user 242 my $setName = $urlPath->arg("setID"); 243 my $userName = $r->param("user"); 244 my $effectiveUserName = $r->param("effectiveUser"); 245 246 # if there is no input userName, then the content generator will 247 # be forcing a login, so just bail 248 return 0 if ( ! $userName || ! $effectiveUserName ); 249 250 # do we have a cached set that we can use? 251 my $set = $self->{merged_set}; 252 253 if ( $setName =~ /,v(\d+)$/ ) { 254 my $verNum = $1; 255 $setName =~ s/,v\d+$//; 256 257 if ( $set && $set->set_id eq $setName && 258 $set->user_id eq $effectiveUserName && 259 $set->version_id eq $verNum ) { 260 # then we can just use this set and skip the rest 261 262 } elsif ( $setName eq 'Undefined_Set' and 263 $self->hasPermissions($userName, "access_instructor_tools") ) { 264 # this is the case of previewing a problem 265 # from a 'try it' link 266 return 0; 267 } else { 268 if ($db->existsSetVersion($effectiveUserName,$setName,$verNum)) { 269 $set = $db->getMergedSetVersion($effectiveUserName,$setName,$verNum); 270 } else { 271 return "Requested version ($verNum) of set " . 272 "'$setName' is not assigned to user " . 273 "$effectiveUserName."; 274 } 275 } 276 if ( ! $set ) { 277 return "Requested set '$setName' could not be found " . 278 "in the database for user $effectiveUserName."; 279 } 280 } else { 281 282 if ( $set && $set->set_id eq $setName && 283 $set->user_id eq $effectiveUserName ) { 284 # then we can just use this set, and skip the rest 285 286 } else { 287 if ( $db->existsUserSet($effectiveUserName,$setName) ) { 288 $set = $db->getMergedSet($effectiveUserName,$setName); 289 } elsif ( $setName eq 'Undefined_Set' and 290 $self->hasPermissions($userName, "access_instructor_tools") ) { 291 # this is the weird case of the library 292 # browser, when we don't actually have 293 # a set to look at, but this only happens among 294 # instructor tool users. 295 return 0; 296 } else { 297 return "Requested set '$setName' is not " . 298 "assigned to user $effectiveUserName."; 299 } 300 } 301 if ( ! $set ) { 302 return "Requested set '$setName' could not be found " . 303 "in the database for user $effectiveUserName."; 304 } 305 } 306 # cache the set for future use as needed. this should probably 307 # be more sophisticated than this 308 $self->{merged_set} = $set; 309 310 # now we know that the set is assigned to the appropriate user; 311 # check to see if we're trying to access a set that's not open 312 if ( before($set->open_date) && 313 ! $self->hasPermissions($userName, "view_unopened_sets") ) { 314 return "Requested set '$setName' is not yet open."; 315 } 316 317 # also check to make sure that the set is published, or that we're 318 # allowed to view unpublished setes 319 # (do we need to worry about published not being set at this point?) 320 my $published = ( $set && $set->published ne '0' && 321 $set->published ne '1' ) ? 1 : $set->published; 322 if ( ! $published && 323 ! $self->hasPermissions($userName, "view_unpublished_sets") ) { 324 return "Requested set '$setName' is not available yet."; 325 } 326 327 # check to be sure that gateways are being sent to the correct 328 # content generator 329 if (defined($set->assignment_type) && 330 $set->assignment_type =~ /gateway/ && 331 ($node_name eq 'problem_list' || $node_name eq 'problem_detail')) { 332 return "Requested set '$setName' is a test/quiz assignment " . 333 "but the regular homework assignment content " . 334 "generator $node_name was called. Try re-entering " . 335 "the set from the problem sets listing page."; 336 } elsif ( (! defined($set->assignment_type) || 337 $set->assignment_type eq 'homework') && 338 $node_name =~ /gateway/ ) { 339 return "Requested set '$setName' is a homework assignment " . 340 "but the gateway/quiz content " . 341 "generator $node_name was called. Try re-entering " . 342 "the set from the problem sets listing page."; 343 } 344 345 # and check that if we're entering a proctored assignment that we 346 # have a valid proctor login; this is necessary to make sure that 347 # someone doesn't use the unproctored url path to obtain access 348 # to a proctored assignment. 349 if (defined($set->assignment_type) && 350 $set->assignment_type =~ /proctored/ && 351 ! WeBWorK::Authen::Proctor->new($r,$ce,$db)->verify() ) { 352 return "Requested set '$setName' is a proctored test/quiz " . 353 "assignment, but no valid proctor authorization " . 354 "has been obtained."; 355 } 356 357 # and whether there are ip restrictions that we need to check 358 my $badIP = $self->invalidIPAddress($set); 359 return $badIP if $badIP; 360 361 return 0; 362 } 363 364 sub invalidIPAddress { 365 # this exists as a separate routine because we need to check multiple 366 # sets in Hardcopy; having this routine to check the set allows us to do 367 # that for all sets individually there. 368 369 my $self = shift; 370 my $set = shift; 371 372 my $r = $self->{r}; 373 my $db = $r->db; 374 my $urlPath = $r->urlpath; 375 # my $setName = $urlPath->arg("setID"); # not always defined 376 my $setName = $set->set_id; 377 my $userName = $r->param("user"); 378 my $effectiveUserName = $r->param("effectiveUser"); 379 380 return 0 if ($set->restrict_ip eq '' || $set->restrict_ip eq 'No' || 381 $self->hasPermissions($userName,'view_ip_restricted_sets')); 382 383 my $clientIP = new Net::IP($r->connection->remote_ip); 384 # make sure that we're using the non-versioned set name 385 $setName =~ s/,v\d+$//; 386 387 my $restrictType = $set->restrict_ip; 388 my @restrictLocations = $db->getAllMergedSetLocations($effectiveUserName,$setName); 389 my @locationIDs = ( map {$_->location_id} @restrictLocations ); 390 my @restrictAddresses = ( map {$db->listLocationAddresses($_)} @locationIDs ); 391 392 # if there are no addresses in the locations, return an error that 393 # says this 394 return "Client ip address " . $clientIP->ip() . " is not allowed to " . 395 "work this assignment, because the assignment has ip address " . 396 "restrictions and there are no allowed locations associated " . 397 "with the restriction. Contact your professor to have this " . 398 "problem resolved." if ( ! @restrictAddresses ); 399 400 # build a set of IP objects to match against 401 my @restrictIPs = ( map {new Net::IP($_)} @restrictAddresses ); 402 403 # and check the clientAddress against these: is $clientIP 404 # in @restrictIPs? 405 my $inRestrict = 0; 406 foreach my $rIP ( @restrictIPs ) { 407 if ($rIP->overlaps($clientIP) == $IP_B_IN_A_OVERLAP || 408 $rIP->overlaps($clientIP) == $IP_IDENTICAL) { 409 $inRestrict = $rIP->ip(); 410 last; 411 } 412 } 413 414 # this is slightly complicated by having to check relax_restrict_ip 415 my $badIP = ''; 416 if ( $restrictType eq 'RestrictTo' && ! $inRestrict ) { 417 $badIP = "Client ip address " . $clientIP->ip() . 418 " is not in the list of addresses from " . 419 "which this assignment may be worked."; 420 } elsif ( $restrictType eq 'DenyFrom' && $inRestrict ) { 421 $badIP = "Client ip address " . $clientIP->ip() . 422 " is in the list of addresses from " . 423 "which this assignment may not be worked."; 424 } else { 425 return 0; 426 } 427 428 # if we're here, we failed the IP check, and so need to consider 429 # if ip restrictions were relaxed. the set we were passed in 430 # is either the merged userset or the merged versioned userset, 431 # depending on whether the set is versioned or not 432 433 my $relaxRestrict = $set->relax_restrict_ip; 434 return $badIP if ( $relaxRestrict eq 'No' ); 435 436 if ( $set->assignment_type =~ /gateway/ ) { 437 if ( $relaxRestrict eq 'AfterAnswerDate' ) { 438 # in this case we need to go and get the userset, 439 # not the versioned set (which we already have) 440 # drat! 441 my $userset = $db->getMergedSet($set->user_id,$setName); 442 return( ! $userset || before($userset->answer_date) 443 ? $badIP : 0 ); 444 } else { 445 # this is easier; just look at the current answer date 446 return( before($set->answer_date) ? $badIP : 0 ); 447 } 448 } else { 449 # the set isn't versioned, so assume that $relaxRestrict 450 # is 'AfterAnswerDate', regardless of what it actually 451 # is; 'AfterVersionAnswerDate' doesn't make sense in 452 # this case 453 return( before($set->answer_date) ? $badIP : 0 ); 454 } 455 } 456 457 =back 458 459 =cut 460 461 =head1 AUTHOR 462 463 Written by Dennis Lambe, malsyned at math.rochester.edu. Modified by Sam 464 Hathaway, sh002i at math.rochester.edu. 465 466 =cut 467 468 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |