| 1 | ################################################################################ |
1 | ################################################################################ |
| 2 | # WeBWorK mod_perl (c) 1995-2002 WeBWorK Team, Univeristy of Rochester |
2 | # WeBWorK Online Homework Delivery System |
| 3 | # $Id$ |
3 | # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/ |
|
|
4 | # $CVSHeader$ |
|
|
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. |
| 4 | ################################################################################ |
15 | ################################################################################ |
| 5 | |
16 | |
| 6 | package WeBWorK::Authz; |
17 | package WeBWorK::Authz; |
| 7 | |
18 | |
| 8 | =head1 NAME |
19 | =head1 NAME |
| 9 | |
20 | |
| 10 | WeBWorK::Authz - check user permissions. |
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. |
| 11 | |
57 | |
| 12 | =cut |
58 | =cut |
| 13 | |
59 | |
| 14 | use strict; |
60 | use strict; |
| 15 | use warnings; |
61 | use warnings; |
| 16 | use WeBWorK::DB::Auth; |
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; |
| 17 | |
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 | |
| 18 | sub new($$$) { |
81 | sub new { |
| 19 | my $invocant = shift; |
82 | my ($invocant, $r) = @_; |
| 20 | my $class = ref($invocant) || $invocant; |
83 | my $class = ref($invocant) || $invocant; |
| 21 | my $self = {}; |
84 | my $self = { |
| 22 | ($self->{r}, $self->{courseEnvironment}) = @_; |
85 | r => $r, |
|
|
86 | }; |
|
|
87 | |
| 23 | bless $self, $class; |
88 | bless $self, $class; |
| 24 | return $self; |
89 | return $self; |
| 25 | } |
90 | } |
| 26 | |
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. |
| 27 | sub hasPermissions { |
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 | |
| 28 | my ($self, $user, $activity) = @_; |
157 | my ($self, $userID, $activity) = @_; |
| 29 | my $r = $self->{r}; |
158 | my $r = $self->{r}; |
| 30 | my $courseEnvironment = $self->{courseEnvironment}; |
159 | my $ce = $r->ce; |
| 31 | my $permission_hash = $courseEnvironment->{permission_hash}; |
160 | my $db = $r->db; |
| 32 | my $auth = WeBWorK::DB::Auth->new($courseEnvironment); |
|
|
| 33 | |
161 | |
| 34 | my $permissionLevel = $auth->getPermissions($user); |
162 | # this may need to be changed if we get other permission level data sources |
| 35 | if ($permissionLevel >= $permission_hash->{$activity}) { |
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."; |
| 36 | return 1; |
189 | return 0; |
| 37 | } else {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 | } |
| 38 | } |
222 | } |
| 39 | |
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 | } elsif ( $setName eq 'Undefined_Set' ) { |
|
|
285 | # this is the weird case of the library |
|
|
286 | # browser, when we don't actually have |
|
|
287 | # a set to look at. |
|
|
288 | return 0; |
|
|
289 | } else { |
|
|
290 | return "Requested set '$setName' is not " . |
|
|
291 | "assigned to user $effectiveUserName."; |
|
|
292 | } |
|
|
293 | } |
|
|
294 | if ( ! $set ) { |
|
|
295 | return "Requested set '$setName' could not be found " . |
|
|
296 | "in the database for user $effectiveUserName."; |
|
|
297 | } |
|
|
298 | } |
|
|
299 | # cache the set for future use as needed. this should probably |
|
|
300 | # be more sophisticated than this |
|
|
301 | $self->{merged_set} = $set; |
|
|
302 | |
|
|
303 | # now we know that the set is assigned to the appropriate user; |
|
|
304 | # check to see if we're trying to access a set that's not open |
|
|
305 | if ( before($set->open_date) && |
|
|
306 | ! $self->hasPermissions($userName, "view_unopened_sets") ) { |
|
|
307 | return "Requested set '$setName' is not yet open."; |
|
|
308 | } |
|
|
309 | |
|
|
310 | # also check to make sure that the set is published, or that we're |
|
|
311 | # allowed to view unpublished setes |
|
|
312 | # (do we need to worry about published not being set at this point?) |
|
|
313 | my $published = ( $set && $set->published ne '0' && |
|
|
314 | $set->published ne '1' ) ? 1 : $set->published; |
|
|
315 | if ( ! $published && |
|
|
316 | ! $self->hasPermissions($userName, "view_unpublished_sets") ) { |
|
|
317 | return "Requested set '$setName' is not available yet."; |
|
|
318 | } |
|
|
319 | |
|
|
320 | # check to be sure that gateways are being sent to the correct |
|
|
321 | # content generator |
|
|
322 | if (defined($set->assignment_type) && |
|
|
323 | $set->assignment_type =~ /gateway/ && |
|
|
324 | ($node_name eq 'problem_list' || $node_name eq 'problem_detail')) { |
|
|
325 | return "Requested set '$setName' is a test/quiz assignment " . |
|
|
326 | "but the regular homework assignment content " . |
|
|
327 | "generator $node_name was called. Try re-entering " . |
|
|
328 | "the set from the problem sets listing page."; |
|
|
329 | } elsif ( (! defined($set->assignment_type) || |
|
|
330 | $set->assignment_type eq 'homework') && |
|
|
331 | $node_name =~ /gateway/ ) { |
|
|
332 | return "Requested set '$setName' is a homework assignment " . |
|
|
333 | "but the gateway/quiz content " . |
|
|
334 | "generator $node_name was called. Try re-entering " . |
|
|
335 | "the set from the problem sets listing page."; |
|
|
336 | } |
|
|
337 | |
|
|
338 | # and check that if we're entering a proctored assignment that we |
|
|
339 | # have a valid proctor login; this is necessary to make sure that |
|
|
340 | # someone doesn't use the unproctored url path to obtain access |
|
|
341 | # to a proctored assignment. |
|
|
342 | if (defined($set->assignment_type) && |
|
|
343 | $set->assignment_type =~ /proctored/ && |
|
|
344 | ! WeBWorK::Authen::Proctor->new($r,$ce,$db)->verify() ) { |
|
|
345 | return "Requested set '$setName' is a proctored test/quiz " . |
|
|
346 | "assignment, but no valid proctor authorization " . |
|
|
347 | "has been obtained."; |
|
|
348 | } |
|
|
349 | |
|
|
350 | # and whether there are ip restrictions that we need to check |
|
|
351 | my $badIP = $self->invalidIPAddress($set); |
|
|
352 | return $badIP if $badIP; |
|
|
353 | |
|
|
354 | return 0; |
|
|
355 | } |
|
|
356 | |
|
|
357 | sub invalidIPAddress { |
|
|
358 | # this exists as a separate routine because we need to check multiple |
|
|
359 | # sets in Hardcopy; having this routine to check the set allows us to do |
|
|
360 | # that for all sets individually there. |
|
|
361 | |
|
|
362 | my $self = shift; |
|
|
363 | my $set = shift; |
|
|
364 | |
|
|
365 | my $r = $self->{r}; |
|
|
366 | my $db = $r->db; |
|
|
367 | my $urlPath = $r->urlpath; |
|
|
368 | # my $setName = $urlPath->arg("setID"); # not always defined |
|
|
369 | my $setName = $set->set_id; |
|
|
370 | my $userName = $r->param("user"); |
|
|
371 | my $effectiveUserName = $r->param("effectiveUser"); |
|
|
372 | |
|
|
373 | return 0 if ($set->restrict_ip eq '' || $set->restrict_ip eq 'No' || |
|
|
374 | $self->hasPermissions($userName,'view_ip_restricted_sets')); |
|
|
375 | |
|
|
376 | my $clientIP = new Net::IP($r->connection->remote_ip); |
|
|
377 | # make sure that we're using the non-versioned set name |
|
|
378 | $setName =~ s/,v\d+$//; |
|
|
379 | |
|
|
380 | my $restrictType = $set->restrict_ip; |
|
|
381 | my @restrictLocations = $db->getAllMergedSetLocations($effectiveUserName,$setName); |
|
|
382 | my @locationIDs = ( map {$_->location_id} @restrictLocations ); |
|
|
383 | my @restrictAddresses = ( map {$db->listLocationAddresses($_)} @locationIDs ); |
|
|
384 | |
|
|
385 | # if there are no addresses in the locations, return an error that |
|
|
386 | # says this |
|
|
387 | return "Client ip address " . $clientIP->ip() . " is not allowed to " . |
|
|
388 | "work this assignment, because the assignment has ip address " . |
|
|
389 | "restrictions and there are no allowed locations associated " . |
|
|
390 | "with the restriction. Contact your professor to have this " . |
|
|
391 | "problem resolved." if ( ! @restrictAddresses ); |
|
|
392 | |
|
|
393 | # build a set of IP objects to match against |
|
|
394 | my @restrictIPs = ( map {new Net::IP($_)} @restrictAddresses ); |
|
|
395 | |
|
|
396 | # and check the clientAddress against these: is $clientIP |
|
|
397 | # in @restrictIPs? |
|
|
398 | my $inRestrict = 0; |
|
|
399 | foreach my $rIP ( @restrictIPs ) { |
|
|
400 | if ($rIP->overlaps($clientIP) == $IP_B_IN_A_OVERLAP || |
|
|
401 | $rIP->overlaps($clientIP) == $IP_IDENTICAL) { |
|
|
402 | $inRestrict = $rIP->ip(); |
|
|
403 | last; |
|
|
404 | } |
|
|
405 | } |
|
|
406 | |
|
|
407 | # this is slightly complicated by having to check relax_restrict_ip |
|
|
408 | my $badIP = ''; |
|
|
409 | if ( $restrictType eq 'RestrictTo' && ! $inRestrict ) { |
|
|
410 | $badIP = "Client ip address " . $clientIP->ip() . |
|
|
411 | " is not in the list of addresses from " . |
|
|
412 | "which this assignment may be worked."; |
|
|
413 | } elsif ( $restrictType eq 'DenyFrom' && $inRestrict ) { |
|
|
414 | $badIP = "Client ip address " . $clientIP->ip() . |
|
|
415 | " is in the list of addresses from " . |
|
|
416 | "which this assignment may not be worked."; |
|
|
417 | } else { |
|
|
418 | return 0; |
|
|
419 | } |
|
|
420 | |
|
|
421 | # if we're here, we failed the IP check, and so need to consider |
|
|
422 | # if ip restrictions were relaxed. the set we were passed in |
|
|
423 | # is either the merged userset or the merged versioned userset, |
|
|
424 | # depending on whether the set is versioned or not |
|
|
425 | |
|
|
426 | my $relaxRestrict = $set->relax_restrict_ip; |
|
|
427 | return $badIP if ( $relaxRestrict eq 'No' ); |
|
|
428 | |
|
|
429 | if ( $set->assignment_type =~ /gateway/ ) { |
|
|
430 | if ( $relaxRestrict eq 'AfterAnswerDate' ) { |
|
|
431 | # in this case we need to go and get the userset, |
|
|
432 | # not the versioned set (which we already have) |
|
|
433 | # drat! |
|
|
434 | my $userset = $db->getMergedSet($set->user_id,$setName); |
|
|
435 | return( ! $userset || before($userset->answer_date) |
|
|
436 | ? $badIP : 0 ); |
|
|
437 | } else { |
|
|
438 | # this is easier; just look at the current answer date |
|
|
439 | return( before($set->answer_date) ? $badIP : 0 ); |
|
|
440 | } |
|
|
441 | } else { |
|
|
442 | # the set isn't versioned, so assume that $relaxRestrict |
|
|
443 | # is 'AfterAnswerDate', regardless of what it actually |
|
|
444 | # is; 'AfterVersionAnswerDate' doesn't make sense in |
|
|
445 | # this case |
|
|
446 | return( before($set->answer_date) ? $badIP : 0 ); |
|
|
447 | } |
|
|
448 | } |
|
|
449 | |
|
|
450 | =back |
|
|
451 | |
|
|
452 | =cut |
|
|
453 | |
|
|
454 | =head1 AUTHOR |
|
|
455 | |
|
|
456 | Written by Dennis Lambe, malsyned at math.rochester.edu. Modified by Sam |
|
|
457 | Hathaway, sh002i at math.rochester.edu. |
|
|
458 | |
|
|
459 | =cut |
|
|
460 | |
| 40 | 1; |
461 | 1; |