[system] / branches / gage_dev / webwork2 / lib / WeBWorK / Authz.pm Repository:
ViewVC logotype

View of /branches/gage_dev/webwork2/lib/WeBWorK/Authz.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6299 - (download) (as text) (annotate)
Tue Jun 22 14:46:58 2010 UTC (2 years, 10 months ago) by gage
File size: 15718 byte(s)
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