--- trunk/webwork2/lib/WeBWorK/Authz.pm 2002/06/19 22:45:47 390 +++ branches/rel-2-4-dev/webwork2/lib/WeBWorK/Authz.pm 2007/08/09 21:48:16 5266 @@ -1,27 +1,461 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/ +# $CVSHeader$ +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. +################################################################################ + package WeBWorK::Authz; -use WeBWorK::DB::Auth; +=head1 NAME + +WeBWorK::Authz - check user permissions. + +=head1 SYNOPSIS + + # create new authorizer -- $r is a WeBWorK::Request object. + my $authz = new WeBWorK::Authz($r); + + # tell authorizer to cache permission level of user spammy. + $authz->setCachedUser("spammy"); + + # this call will use the cached data. + if ($authz->hasPermissions("spammy", "eat_breakfast")) { + eat_breakfast(); + } + + # this call will not use the cached data, and will cause a database lookup. + if ($authz->hasPermissions("hammy", "go_to_bed")) { + go_to_bed(); + } + +=head1 DESCRIPTION + +WeBWorK::Authen determines if a user is authorized to perform a specific +activity, based on the user's PermissionLevel record in the WeBWorK database and +the contents of the %permissionLevels hash in the course environment. + +=head2 Format of the %permissionLevels hash + +%permissionLevels maps text strings describing activities to numeric permission +levels. The definitive list of activities is contained in the default version of +%permissionLevels, in the file F. + +A user is able to engage in an activity if their permission level is greater +than or equal to the level associated with the activity. If the level associated +with an activity is undefiend, then no user is permitted to perform the +activity, regardless of their permission level. + +=cut + +use strict; +use warnings; +use Carp qw/croak/; +# FIXME SET: set-level auth add +use WeBWorK::Utils qw(before after between); +use WeBWorK::Authen::Proctor; +use Net::IP; -sub new($$$) { - my $invocant = shift; +################################################################################ + +=head1 CONSTRUCTOR + +=over + +=item WeBWorK::Authz->new($r) + +Creates a new authorizer instance. $r is a WeBWorK::Request object. It must +already have its C and C fields set. + +=cut + +sub new { + my ($invocant, $r) = @_; my $class = ref($invocant) || $invocant; - my $self = {}; - ($self->{r}, $self->{courseEnvironment}) = @_; + my $self = { + r => $r, + }; + bless $self, $class; return $self; } +=back + +=cut + +################################################################################ + +=head1 METHODS + +=over + +=item setCachedUser($userID) + +Caches the PermissionLevel of the user $userID in an existing authorizer. If a +user's PermissionLevel is cached, it will be used whenever hasPermissions() is +called on the same user. Only one user can be cached at a time. This is used by +WeBWorK to cache the "real" user. + +=cut + +sub setCachedUser { + my ($self, $userID) = @_; + my $r = $self->{r}; + my $db = $r->db; + + delete $self->{userID}; + delete $self->{PermissionLevel}; + + if (defined $userID) { + $self->{userID} = $userID; + my $PermissionLevel = $db->getPermissionLevel($userID); # checked + if (defined $PermissionLevel) { + # store permission level record in database to avoid later database calls + $self->{PermissionLevel} = $PermissionLevel; + } + } else { + warn "setCachedUser() called with userID undefined."; + } +} + +=item hasPermissions($userID, $activity) + +Checks the %permissionLevels hash in the course environment to determine if the +user $userID has permission to engage in the activity $activity. If the user's +permission level is greater than or equal to the level associated with $activty, +a true value is returned. Otherwise, a false value is returned. + +If $userID has been cached using the setCachedUser() call, the cached data is +used. Otherwise, the user's PermissionLevel is looked up in the WeBWorK +database. + +If the user does not have a PermissionLevel record, the permission level record +is empty, or the activity does not appear in %permissionLevels, hasPermissions() +assumes that the user does not have permission. + +=cut + +# This currently only uses two of it's arguments, but it accepts any number, in +# case in the future calculating certain permissions requires more information. sub hasPermissions { - my ($self, $user, $activity) = @_; + if (@_ != 3) { + shift @_; # get rid of self + my $nargs = @_; + croak "hasPermissions called with $nargs arguments instead of the expected 2: '@_'" + } + + my ($self, $userID, $activity) = @_; my $r = $self->{r}; - my $courseEnvironment = $self->{courseEnvironment}; - my $permission_hash = $courseEnvironment->{permission_hash}; - my $auth = WeBWorK::DB::Auth->new($courseEnvironment); - - my $permissionLevel = $auth->getPermissions($user); - if ($permissionLevel >= $permission_hash->{$activity}) { - return 1; - } else {return 0;} + my $ce = $r->ce; + my $db = $r->db; + + # this may need to be changed if we get other permission level data sources + return 0 unless defined $db; + + # this may need to be changed if we want to control what unauthenticated users + # can do with the permissions system + return 0 unless defined $userID and $userID ne ""; + + my $PermissionLevel; + + my $cachedUserID = $self->{userID}; + if (defined $cachedUserID and $cachedUserID ne "" and $cachedUserID eq $userID) { + # this is the same user -- we can skip the database call + $PermissionLevel = $self->{PermissionLevel}; + } else { + # a different user, or no user was defined before + #my $prettyCachedUserID = defined $cachedUserID ? "'$cachedUserID'" : "undefined"; + #warn "hasPermissions called with user '$userID', but cached user is $prettyCachedUserID. Accessing database.\n"; + $PermissionLevel = $db->getPermissionLevel($userID); # checked + } + + my $permission_level; + + if (defined $PermissionLevel) { + $permission_level = $PermissionLevel->permission; + } else { + # uh, oh. this user has no permission level record! + warn "User '$userID' has no PermissionLevel record -- assuming no permission."; + return 0; + } + + unless (defined $permission_level and $permission_level ne "") { + warn "User '$userID' has empty permission level -- assuming no permission."; + return 0; + } + + my $userRoles = $ce->{userRoles}; + my $permissionLevels = $ce->{permissionLevels}; + + if (exists $permissionLevels->{$activity}) { + my $activity_role = $permissionLevels->{$activity}; + if (defined $activity_role) { + if (exists $userRoles->{$activity_role}) { + my $role_permlevel = $userRoles->{$activity_role}; + if (defined $role_permlevel) { + return $permission_level >= $role_permlevel; + } else { + warn "Role '$activity_role' has undefined permisison level -- assuming no permission."; + return 0; + } + } else { + warn "Role '$activity_role' for activity '$activity' not found in \%userRoles -- assuming no permission."; + return 0; + } + } else { + return 0; # undefiend $activity_role, no one has permission to perform $activity + } + } else { + warn "Activity '$activity' not found in \%permissionLevels -- assuming no permission."; + return 0; + } +} + +#### set-level authorization routines + +sub checkSet { + my $self = shift; + my $r = $self->{r}; + my $ce = $r->ce; + my $db = $r->db; + my $urlPath = $r->urlpath; + + my $node_name = $urlPath->type; + + # first check to see if we have to worried about set-level access + # restrictions + return 0 unless (grep {/^$node_name$/} + (qw(problem_list problem_detail gateway_quiz + proctored_gateway_quiz))); + + # to check set restrictions we need a set and a user + my $setName = $urlPath->arg("setID"); + my $userName = $r->param("user"); + my $effectiveUserName = $r->param("effectiveUser"); + + # if there is no input userName, then the content generator will + # be forcing a login, so just bail + return 0 if ( ! $userName || ! $effectiveUserName ); + + # do we have a cached set that we can use? + my $set = $self->{merged_set}; + + if ( $setName =~ /,v(\d+)$/ ) { + my $verNum = $1; + $setName =~ s/,v\d+$//; + + if ( $set && $set->set_id eq $setName && + $set->user_id eq $effectiveUserName && + $set->version_id eq $verNum ) { + # then we can just use this set and skip the rest + + } else { + if ($db->existsSetVersion($effectiveUserName,$setName,$verNum)) { + $set = $db->getMergedSetVersion($effectiveUserName,$setName,$verNum); + } else { + return "Requested version ($verNum) of set " . + "'$setName' is not assigned to user " . + "$effectiveUserName."; + } + } + if ( ! $set ) { + return "Requested set '$setName' could not be found " . + "in the database for user $effectiveUserName."; + } + } else { + + if ( $set && $set->set_id eq $setName && + $set->user_id eq $effectiveUserName ) { + # then we can just use this set, and skip the rest + + } else { + if ( $db->existsUserSet($effectiveUserName,$setName) ) { + $set = $db->getMergedSet($effectiveUserName,$setName); + } elsif ( $setName eq 'Undefined_Set' ) { + # this is the weird case of the library + # browser, when we don't actually have + # a set to look at. + return 0; + } else { + return "Requested set '$setName' is not " . + "assigned to user $effectiveUserName."; + } + } + if ( ! $set ) { + return "Requested set '$setName' could not be found " . + "in the database for user $effectiveUserName."; + } + } + # cache the set for future use as needed. this should probably + # be more sophisticated than this + $self->{merged_set} = $set; + + # now we know that the set is assigned to the appropriate user; + # check to see if we're trying to access a set that's not open + if ( before($set->open_date) && + ! $self->hasPermissions($userName, "view_unopened_sets") ) { + return "Requested set '$setName' is not yet open."; + } + + # also check to make sure that the set is published, or that we're + # allowed to view unpublished setes + # (do we need to worry about published not being set at this point?) + my $published = ( $set && $set->published ne '0' && + $set->published ne '1' ) ? 1 : $set->published; + if ( ! $published && + ! $self->hasPermissions($userName, "view_unpublished_sets") ) { + return "Requested set '$setName' is not available yet."; + } + + # check to be sure that gateways are being sent to the correct + # content generator + if (defined($set->assignment_type) && + $set->assignment_type =~ /gateway/ && + ($node_name eq 'problem_list' || $node_name eq 'problem_detail')) { + return "Requested set '$setName' is a test/quiz assignment " . + "but the regular homework assignment content " . + "generator $node_name was called. Try re-entering " . + "the set from the problem sets listing page."; + } elsif ( (! defined($set->assignment_type) || + $set->assignment_type eq 'homework') && + $node_name =~ /gateway/ ) { + return "Requested set '$setName' is a homework assignment " . + "but the gateway/quiz content " . + "generator $node_name was called. Try re-entering " . + "the set from the problem sets listing page."; + } + + # and check that if we're entering a proctored assignment that we + # have a valid proctor login; this is necessary to make sure that + # someone doesn't use the unproctored url path to obtain access + # to a proctored assignment. + if (defined($set->assignment_type) && + $set->assignment_type =~ /proctored/ && + ! WeBWorK::Authen::Proctor->new($r,$ce,$db)->verify() ) { + return "Requested set '$setName' is a proctored test/quiz " . + "assignment, but no valid proctor authorization " . + "has been obtained."; + } + + # and whether there are ip restrictions that we need to check + my $badIP = $self->invalidIPAddress($set); + return $badIP if $badIP; + + return 0; } +sub invalidIPAddress { +# this exists as a separate routine because we need to check multiple +# sets in Hardcopy; having this routine to check the set allows us to do +# that for all sets individually there. + + my $self = shift; + my $set = shift; + + my $r = $self->{r}; + my $db = $r->db; + my $urlPath = $r->urlpath; +# my $setName = $urlPath->arg("setID"); # not always defined + my $setName = $set->set_id; + my $userName = $r->param("user"); + my $effectiveUserName = $r->param("effectiveUser"); + + return 0 if ($set->restrict_ip eq '' || $set->restrict_ip eq 'No' || + $self->hasPermissions($userName,'view_ip_restricted_sets')); + + my $clientIP = new Net::IP($r->connection->remote_ip); + # make sure that we're using the non-versioned set name + $setName =~ s/,v\d+$//; + + my $restrictType = $set->restrict_ip; + my @restrictLocations = $db->getAllMergedSetLocations($effectiveUserName,$setName); + my @locationIDs = ( map {$_->location_id} @restrictLocations ); + my @restrictAddresses = ( map {$db->listLocationAddresses($_)} @locationIDs ); + + # if there are no addresses in the locations, return an error that + # says this + return "Client ip address " . $clientIP->ip() . " is not allowed to " . + "work this assignment, because the assignment has ip address " . + "restrictions and there are no allowed locations associated " . + "with the restriction. Contact your professor to have this " . + "problem resolved." if ( ! @restrictAddresses ); + + # build a set of IP objects to match against + my @restrictIPs = ( map {new Net::IP($_)} @restrictAddresses ); + + # and check the clientAddress against these: is $clientIP + # in @restrictIPs? + my $inRestrict = 0; + foreach my $rIP ( @restrictIPs ) { + if ($rIP->overlaps($clientIP) == $IP_B_IN_A_OVERLAP || + $rIP->overlaps($clientIP) == $IP_IDENTICAL) { + $inRestrict = $rIP->ip(); + last; + } + } + + # this is slightly complicated by having to check relax_restrict_ip + my $badIP = ''; + if ( $restrictType eq 'RestrictTo' && ! $inRestrict ) { + $badIP = "Client ip address " . $clientIP->ip() . + " is not in the list of addresses from " . + "which this assignment may be worked."; + } elsif ( $restrictType eq 'DenyFrom' && $inRestrict ) { + $badIP = "Client ip address " . $clientIP->ip() . + " is in the list of addresses from " . + "which this assignment may not be worked."; + } else { + return 0; + } + + # if we're here, we failed the IP check, and so need to consider + # if ip restrictions were relaxed. the set we were passed in + # is either the merged userset or the merged versioned userset, + # depending on whether the set is versioned or not + + my $relaxRestrict = $set->relax_restrict_ip; + return $badIP if ( $relaxRestrict eq 'No' ); + + if ( $set->assignment_type =~ /gateway/ ) { + if ( $relaxRestrict eq 'AfterAnswerDate' ) { + # in this case we need to go and get the userset, + # not the versioned set (which we already have) + # drat! + my $userset = $db->getMergedSet($set->user_id,$setName); + return( ! $userset || before($userset->answer_date) + ? $badIP : 0 ); + } else { + # this is easier; just look at the current answer date + return( before($set->answer_date) ? $badIP : 0 ); + } + } else { + # the set isn't versioned, so assume that $relaxRestrict + # is 'AfterAnswerDate', regardless of what it actually + # is; 'AfterVersionAnswerDate' doesn't make sense in + # this case + return( before($set->answer_date) ? $badIP : 0 ); + } +} + +=back + +=cut + +=head1 AUTHOR + +Written by Dennis Lambe, malsyned at math.rochester.edu. Modified by Sam +Hathaway, sh002i at math.rochester.edu. + +=cut + 1;