[system] / branches / rel-2-4-patches / webwork2 / lib / WeBWorK / Authz.pm Repository:
ViewVC logotype

View of /branches/rel-2-4-patches/webwork2/lib/WeBWorK/Authz.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5243 - (download) (as text) (annotate)
Tue Aug 7 22:35:14 2007 UTC (5 years, 9 months ago)
Original Path: branches/rel-2-4-dev/webwork2/lib/WeBWorK/Authz.pm
File size: 15186 byte(s)
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