Parent Directory
|
Revision Log
This commit was manufactured by cvs2svn to create branch 'rel-2-4-dev'.
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/Authz.pm,v 1.31 2007/03/30 19:07:54 glarose 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 } else { 263 if ($db->existsSetVersion($effectiveUserName,$setName,$verNum)) { 264 $set = $db->getMergedSetVersion($effectiveUserName,$setName,$verNum); 265 } else { 266 return "Requested version ($verNum) of set " . 267 "'$setName' is not assigned to user " . 268 "$effectiveUserName."; 269 } 270 } 271 if ( ! $set ) { 272 return "Requested set '$setName' could not be found " . 273 "in the database for user $effectiveUserName."; 274 } 275 } else { 276 277 if ( $set && $set->set_id eq $setName && 278 $set->user_id eq $effectiveUserName ) { 279 # then we can just use this set, and skip the rest 280 281 } else { 282 if ( $db->existsUserSet($effectiveUserName,$setName) ) { 283 $set = $db->getMergedSet($effectiveUserName,$setName); 284 } else { 285 return "Requested set '$setName' is not " . 286 "assigned to user $effectiveUserName."; 287 } 288 } 289 if ( ! $set ) { 290 return "Requested set '$setName' could not be found " . 291 "in the database for user $effectiveUserName."; 292 } 293 } 294 # cache the set for future use as needed. this should probably 295 # be more sophisticated than this 296 $self->{merged_set} = $set; 297 298 # now we know that the set is assigned to the appropriate user; 299 # check to see if we're trying to access a set that's not open 300 if ( before($set->open_date) && 301 ! $self->hasPermissions($userName, "view_unopened_sets") ) { 302 return "Requested set '$setName' is not yet open."; 303 } 304 305 # also check to make sure that the set is published, or that we're 306 # allowed to view unpublished setes 307 # (do we need to worry about published not being set at this point?) 308 my $published = ( $set && $set->published ne '0' && 309 $set->published ne '1' ) ? 1 : $set->published; 310 if ( ! $published && 311 ! $self->hasPermissions($userName, "view_unpublished_sets") ) { 312 return "Requested set '$setName' is not available yet."; 313 } 314 315 # check to be sure that gateways are being sent to the correct 316 # content generator 317 if (defined($set->assignment_type) && 318 $set->assignment_type =~ /gateway/ && 319 ($node_name eq 'problem_list' || $node_name eq 'problem_detail')) { 320 return "Requested set '$setName' is a test/quiz assignment " . 321 "but the regular homework assignment content " . 322 "generator $node_name was called. Try re-entering " . 323 "the set from the problem sets listing page."; 324 } elsif ( (! defined($set->assignment_type) || 325 $set->assignment_type eq 'homework') && 326 $node_name =~ /gateway/ ) { 327 return "Requested set '$setName' is a homework assignment " . 328 "but the gateway/quiz content " . 329 "generator $node_name was called. Try re-entering " . 330 "the set from the problem sets listing page."; 331 } 332 333 # and check that if we're entering a proctored assignment that we 334 # have a valid proctor login; this is necessary to make sure that 335 # someone doesn't use the unproctored url path to obtain access 336 # to a proctored assignment. 337 if (defined($set->assignment_type) && 338 $set->assignment_type =~ /proctored/ && 339 ! WeBWorK::Authen::Proctor->new($r,$ce,$db)->verify() ) { 340 return "Requested set '$setName' is a proctored test/quiz " . 341 "assignment, but no valid proctor authorization " . 342 "has been obtained."; 343 } 344 345 # and whether there are ip restrictions that we need to check 346 my $badIP = $self->invalidIPAddress($set); 347 return $badIP if $badIP; 348 349 return 0; 350 } 351 352 sub invalidIPAddress { 353 # this exists as a separate routine because we need to check multiple 354 # sets in Hardcopy; having this routine to check the set allows us to do 355 # that for all sets individually there. 356 357 my $self = shift; 358 my $set = shift; 359 360 my $r = $self->{r}; 361 my $db = $r->db; 362 my $urlPath = $r->urlpath; 363 # my $setName = $urlPath->arg("setID"); # not always defined 364 my $setName = $set->set_id; 365 my $userName = $r->param("user"); 366 my $effectiveUserName = $r->param("effectiveUser"); 367 368 return 0 if ($set->restrict_ip eq 'No' || 369 $self->hasPermissions($userName,'view_ip_restricted_sets')); 370 371 my $clientIP = new Net::IP($r->connection->remote_ip); 372 # make sure that we're using the non-versioned set name 373 $setName =~ s/,v\d+$//; 374 375 my $restrictType = $set->restrict_ip; 376 my @restrictLocations = $db->getAllMergedSetLocations($effectiveUserName,$setName); 377 my @locationIDs = ( map {$_->location_id} @restrictLocations ); 378 my @restrictAddresses = ( map {$db->listLocationAddresses($_)} @locationIDs ); 379 380 # if there are no addresses in the locations, return an error that 381 # says this 382 return "Client ip address " . $clientIP->ip() . " is not allowed to " . 383 "work this assignment, because the assignment has ip address " . 384 "restrictions and there are no allowed locations associated " . 385 "with the restriction. Contact your professor to have this " . 386 "problem resolved." if ( ! @restrictAddresses ); 387 388 # build a set of IP objects to match against 389 my @restrictIPs = ( map {new Net::IP($_)} @restrictAddresses ); 390 391 # and check the clientAddress against these: is $clientIP 392 # in @restrictIPs? 393 my $inRestrict = 0; 394 foreach my $rIP ( @restrictIPs ) { 395 if ($rIP->overlaps($clientIP) == $IP_B_IN_A_OVERLAP || 396 $rIP->overlaps($clientIP) == $IP_IDENTICAL) { 397 $inRestrict = $rIP->ip(); 398 last; 399 } 400 } 401 402 # this is slightly complicated by having to check relax_restrict_ip 403 my $badIP = ''; 404 if ( $restrictType eq 'RestrictTo' && ! $inRestrict ) { 405 $badIP = "Client ip address " . $clientIP->ip() . 406 " is not in the list of addresses from " . 407 "which this assignment may be worked."; 408 } elsif ( $restrictType eq 'DenyFrom' && $inRestrict ) { 409 $badIP = "Client ip address " . $clientIP->ip() . 410 " is in the list of addresses from " . 411 "which this assignment may not be worked."; 412 } else { 413 return 0; 414 } 415 416 # if we're here, we failed the IP check, and so need to consider 417 # if ip restrictions were relaxed. the set we were passed in 418 # is either the merged userset or the merged versioned userset, 419 # depending on whether the set is versioned or not 420 421 my $relaxRestrict = $set->relax_restrict_ip; 422 return $badIP if ( $relaxRestrict eq 'No' ); 423 424 if ( $set->assignment_type =~ /gateway/ ) { 425 if ( $relaxRestrict eq 'AfterAnswerDate' ) { 426 # in this case we need to go and get the userset, 427 # not the versioned set (which we already have) 428 # drat! 429 my $userset = $db->getMergedSet($set->user_id,$setName); 430 return( ! $userset || before($userset->answer_date) 431 ? $badIP : 0 ); 432 } else { 433 # this is easier; just look at the current answer date 434 return( before($set->answer_date) ? $badIP : 0 ); 435 } 436 } else { 437 # the set isn't versioned, so assume that $relaxRestrict 438 # is 'AfterAnswerDate', regardless of what it actually 439 # is; 'AfterVersionAnswerDate' doesn't make sense in 440 # this case 441 return( before($set->answer_date) ? $badIP : 0 ); 442 } 443 } 444 445 =back 446 447 =cut 448 449 =head1 AUTHOR 450 451 Written by Dennis Lambe, malsyned at math.rochester.edu. Modified by Sam 452 Hathaway, sh002i at math.rochester.edu. 453 454 =cut 455 456 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |