File Coverage

blib/lib/FAQ/OMatic/Auth.pm
Criterion Covered Total %
statement 27 206 13.1
branch 0 90 0.0
condition 0 79 0.0
subroutine 9 23 39.1
pod 0 14 0.0
total 36 412 8.7


line stmt bran cond sub pod time code
1             ##############################################################################
2             # The Faq-O-Matic is Copyright 1997 by Jon Howell, all rights reserved. #
3             # #
4             # This program is free software; you can redistribute it and/or #
5             # modify it under the terms of the GNU General Public License #
6             # as published by the Free Software Foundation; either version 2 #
7             # of the License, or (at your option) any later version. #
8             # #
9             # This program is distributed in the hope that it will be useful, #
10             # but WITHOUT ANY WARRANTY; without even the implied warranty of #
11             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
12             # GNU General Public License for more details. #
13             # #
14             # You should have received a copy of the GNU General Public License #
15             # along with this program; if not, write to the Free Software #
16             # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.#
17             # #
18             # Jon Howell can be contacted at: #
19             # 6211 Sudikoff Lab, Dartmouth College #
20             # Hanover, NH 03755-3510 #
21             # jonh@cs.dartmouth.edu #
22             # #
23             # An electronic copy of the GPL is available at: #
24             # http://www.gnu.org/copyleft/gpl.html #
25             # #
26             ##############################################################################
27              
28 1     1   6 use strict;
  1         2  
  1         45  
29              
30             ###
31             ### The FaqAuth module provides identification, authentication,
32             ### and authorization services for the Faq-O-Matic.
33             ###
34             ### Custom authentication schemes should
35             ### be implemented by using the hooks in this module. (For
36             ### now there are no hooks, but in theory there should be one
37             ### replaceable function.) You'd rather not modify this file,
38             ### so you can be drop-in compatible with future faqomatic
39             ### releases.
40             ###
41              
42             package FAQ::OMatic::Auth;
43              
44 1     1   4 use FAQ::OMatic;
  1         2  
  1         17  
45 1     1   4 use FAQ::OMatic::Item;
  1         2  
  1         15  
46 1     1   499 use FAQ::OMatic::AuthLocal;
  1         2  
  1         24  
47 1     1   491 use FAQ::OMatic::Groups;
  1         2  
  1         31  
48 1     1   9 use FAQ::OMatic::I18N;
  1         2  
  1         161  
49 1     1   559 use FAQ::OMatic::Entropy;
  1         3  
  1         36  
50 1     1   6 use Digest::MD5 qw(md5_hex);
  1         2  
  1         170  
51              
52             # a global constant (accessible outside using my namespace)
53 1     1   7 use vars qw($cookieExtra); # constant, visible to maintenance.pm
  1         2  
  1         2765  
54              
55             my $trustedID = undef;
56             # Perm values only:
57             # '7','9' -- returned by perm routines to indicate
58             # authQuality must be 5, and user must be the
59             # moderator of the item.
60             # '6' -- a perm that indicates a group membership
61             # requirement. (actually "6 group_name".)
62             # $authQuality's and Perm* values:
63             # '5' -- user has provided proof that ID is correct
64             # '3' -- user has merely claimed this ID
65             # '1' -- no ID is offered
66              
67             $cookieExtra = 600; # 10 extra minutes to submit forms after filling
68             # them out so you don't have to worry about
69             # losing your text.
70              
71             sub getID {
72 0     0 0   my $params = FAQ::OMatic::getParams(); # get cached params
73              
74 0           my $trustedID = FAQ::OMatic::getLocal('trustedID');
75 0           my $authQuality = FAQ::OMatic::getLocal('authQuality');
76 0 0         if (not defined $trustedID) {
77 0 0         if (defined $params->{'auth'}) {
    0          
78             # use a user-overridable auth function
79 0           ($trustedID,$authQuality) = authenticate($params);
80             } elsif (defined $params->{'id'}) {
81             # id without authorization
82 0           $trustedID = $params->{'id'};
83 0           $authQuality = 3;
84             } else {
85             # no authorization offered
86 0           $trustedID = 'anonymous';
87 0           $authQuality = 1;
88             }
89             }
90              
91 0           FAQ::OMatic::setLocal('trustedID', $trustedID);
92 0           FAQ::OMatic::setLocal('authQuality', $authQuality);
93 0           return ($trustedID,$authQuality);
94             }
95              
96             # result is 'false' if user CAN edit the part, else an error message
97             sub checkPerm {
98 0     0 0   my $item = shift;
99 0           my $operation = shift;
100              
101 0           my ($id,$aq) = getID();
102              
103 0           my $whocan = getInheritedProperty($item, $operation);
104              
105             # if just some low quality of authentication is required, prove
106             # user has provided it:
107 0           $whocan =~ m/^(\d+)/;
108 0   0       my $whocanNum = $1 || 7;
109             # THANKS to Mikel Smith
110             # for pointing out that this code was generating warning messages
111 0 0 0       if ($whocanNum <= 5 and $whocanNum <= $aq) {
112             # users' ID dominates required ID
113 0           return 0;
114             }
115              
116             # prove user belongs to required group:
117 0 0 0       if ($whocanNum == 6
118             and FAQ::OMatic::Groups::checkMembership($whocan, $id)) {
119             # user belongs to the specified group
120 0           return 0;
121             }
122              
123             # prove user has at least moderator priveleges
124 0 0 0       if ((($whocanNum==7) and ($aq==5))
      0        
      0        
125             and (($id eq getInheritedProperty($item, 'Moderator'))
126             or ($id eq $FAQ::OMatic::Config::adminAuth)
127             or ('anybody' eq getInheritedProperty($item, 'Moderator'))
128             )
129             ) {
130             # user has proven authentication, and is the moderator of the item
131 0           return 0;
132             }
133              
134 0           return $whocan;
135             }
136              
137             sub getInheritedProperty {
138 0     0 0   my $item = shift;
139 0           my $property = shift;
140 0   0       my $depth = shift || 0;
141              
142 0 0         if (isPropertyGlobal($property)) {
143             # save a recursive walk up the tree -- this property
144             # is defined at the top.
145             # THANKS to John Goerzen for
146             # finding a dumb bug here.
147 0           $item = new FAQ::OMatic::Item('1');
148             }
149 0 0         if (not ref $item) {
150             # get property from top item if no item specified
151 0           $item = new FAQ::OMatic::Item('1');
152             }
153              
154 0 0 0       if (defined($item) and defined $item->{$property}) {
155             return wantarray()
156 0 0         ? ($item->{$property}, $item)
157             : $item->{$property};
158             }
159              
160 0 0 0       if (not defined($item)
      0        
      0        
161             or ($item eq '')
162             or ($item->getParent() eq $item)
163             or ($depth > 80)) {
164              
165             # no-one defines it, all the way up the chain
166             return wantarray()
167 0 0         ? (getDefaultProperty($property), undef)
168             : getDefaultProperty($property);
169             } else {
170 0           return getInheritedProperty($item->getParent(), $property, $depth+1);
171             }
172             }
173              
174             # fields: [ default value, isGlobal ]
175             my %defaultProperties = (
176             'Moderator' => [ 'nobody', 0 ],
177             'MailModerator' => [ 0, 0 ],
178             'Notifier' => [ 'nobody', 0 ],
179             'MailNotifier' => [ 0, 0 ],
180             'PermEditPart' => [ 5, 0 ], # users with proven authentication
181             'PermAddPart' => [ 5, 0 ],
182             'PermAddItem' => [ 5, 0 ],
183             # 'PermEditItem' => [ 5, 0 ], # (deprecated)
184             'PermEditTitle' => [ 5, 0 ], # moderator
185             'PermEditDirectory' => [ 7, 0 ],
186             'PermModOptions' => [ 7, 0 ],
187             'PermUseHTML' => [ 7, 0 ],
188             'PermNewBag' => [ 7, 1 ],
189             'PermReplaceBag' => [ 7, 1 ],
190             'PermInstall' => [ 7, 1 ],
191             'PermEditGroups' => [ 7, 1 ],
192             'RelaxChildPerms' => [ 'norelax', 0],
193             );
194              
195             sub getDefaultProperty {
196 0     0 0   my $property = shift;
197              
198 0           my $result = $defaultProperties{$property};
199 0 0         if (not defined $result) {
200 0           $result = 7;
201 0           FAQ::OMatic::gripe('panic',
202             "Property $property expected but not defined"); # tell author
203             }
204 0           return $result->[0];
205             }
206              
207             sub isPropertyGlobal {
208 0     0 0   my $property = shift;
209              
210 0           my $result = $defaultProperties{$property};
211 0   0       return $result->[1] || 0;
212             }
213              
214             # ensurePerm()
215             # Checks permissions, returns '' if okay, else returns a redirect to
216             # authenticate.
217             # In list context, returns the same value followed by a quality
218             # value, so if you require two ensurePerms, you return the redirect
219             # with the higher qualityf value (so the user gets all the authentication
220             # done at once). See submitMove.
221             sub ensurePerm {
222 0     0 0   my @p = @_;
223              
224             my (
225 0           $item,
226             $operation,
227             $restart, # which program to run to restart operation
228             # after user presents ID
229             $cgi,
230             $extraTime, # allow slightly stale cookies, so that a
231             # cookie isn't likely to time out between
232             # clicking "edit" and "submit", which annoys.
233             $xreason, # an extra reason, needed to distinguish
234             # two cases (modOptions) in editItem.
235             $failexit # redirect and exit on failure
236             ) = FAQ::OMatic::rearrange(
237             ['item','operation','restart','cgi','extraTime','xreason',
238             'failexit'],
239             @p);
240 0   0       $item ||= '';
241              
242 0           my $result = '';
243              
244 0   0       my $cookieActual = $FAQ::OMatic::Config::cookieLife || 3600;
245 0 0         $cookieActual += $cookieExtra if ($extraTime);
246 0           FAQ::OMatic::setLocal('cookieActual', $cookieActual);
247              
248 0           my $authFailed = checkPerm($item,$operation);
249              
250 0 0         if ($authFailed) {
251 0   0       my $url = FAQ::OMatic::makeAref('authenticate',
252             {'_restart' => $restart, '_reason'=>$authFailed,
253             '_xreason'=>($xreason||'')}, 'url', 'saveTransients');
254 0           $result = FAQ::OMatic::redirect($cgi, $url, 'asString');
255 0 0 0       if ($failexit||'') {
256 0           FAQ::OMatic::redirect($cgi, $result);
257             }
258             }
259              
260 0 0         return wantarray ? ($result, $authFailed)
261             : $result;
262             }
263              
264             sub newCookie {
265 0     0 0   my $id = shift;
266              
267             # Use an existing cookie if available. (why is this good? Just
268             # to keep the cookies file slimmer?)
269 0           my ($cookie,$cid,$ctime);
270 0           ($cookie,$cid,$ctime) = findCookie($id,'id');
271 0 0         return $cookie if (defined $cookie);
272              
273 0           $cookie = "ck".FAQ::OMatic::Entropy::gatherRandomString();
274              
275 0           my $cookiesFile = "$FAQ::OMatic::Config::metaDir/cookies";
276 0           open COOKIEFILE, ">>$cookiesFile";
277 0           print COOKIEFILE "$cookie $id ".time()."\n";
278 0           close COOKIEFILE;
279 0 0         if (not chmod(0600, "$cookiesFile")) {
280 0           FAQ::OMatic::gripe('problem', "chmod failed on $cookiesFile");
281             }
282              
283 0           return $cookie;
284             }
285              
286             sub findCookie {
287 0     0 0   my $match = shift;
288 0           my $by = shift;
289              
290 0           my ($cookie,$cid,$ctime);
291 0 0         if (not open COOKIEFILE, "<$FAQ::OMatic::Config::metaDir/cookies") {
292 0           return undef;
293             }
294 0           while (defined($_=)) {
295 0           chomp;
296 0           ($cookie,$cid,$ctime) = split(' ');
297              
298             # ignore dead cookies
299 0   0       my $cookieActual = FAQ::OMatic::getLocal('cookieActual')
300             || $FAQ::OMatic::Config::cookieLife
301             || 3600;
302 0 0         next if ((time() - $ctime) > $cookieActual);
303              
304 0 0 0       if (($by eq 'id') and ($cid eq $match)) {
305 0           close COOKIEFILE;
306 0           return ($cookie,$cid,$ctime);
307             }
308 0 0 0       if (($by eq 'cookie') and ($cookie eq $match)) {
309 0           close COOKIEFILE;
310 0           return ($cookie,$cid,$ctime);
311             }
312             }
313 0           close COOKIEFILE;
314 0           return undef;
315             }
316              
317             # these functions manipulate a file that maps IDs to
318             # (ID,password,...) tuples. (... = future expansion)
319             # Right now it's a flat file, but maybe someday it should be a
320             # dbm file if anyone ever has zillions of authorized posters.
321              
322             # given an ($id,$password,...) array, writes it into idfile
323             sub writeIDfile {
324 0     0 0   my ($id,$password,@rest) = @_;
325              
326 0           my $lockf = FAQ::OMatic::lockFile("idfile");
327 0 0         FAQ::OMatic::gripe('error', "idfile is locked.") if (not $lockf);
328              
329 0 0         if (not open(IDFILE, "<$FAQ::OMatic::Config::metaDir/idfile")) {
330 0           FAQ::OMatic::unlockFile($lockf);
331 0           FAQ::OMatic::gripe('abort', "FAQ::OMatic::Auth::writeIDfile: Couldn't "
332             ."read $FAQ::OMatic::Config::metaDir/idfile because $!");
333 0           return;
334             }
335              
336             # read id mappings in
337 0           my %idmap;
338 0           my ($idf,$passf,@restf);
339 0           while (defined($_=)) {
340 0           chomp;
341 0           ($idf,$passf,@restf) = split(' ');
342 0           $idmap{$idf} = $_;
343             }
344 0           close IDFILE;
345              
346             # change the mapping for id
347 0           $idmap{$id} = join(' ', $id, $password, @rest);
348            
349             # write id mappings.
350 0 0         if (not open(IDFILE, ">$FAQ::OMatic::Config::metaDir/idfile-new")) {
351 0           FAQ::OMatic::unlockFile($lockf);
352 0           FAQ::OMatic::gripe('abort', "FAQ::OMatic::Auth::writeIDfile: Couldn't "
353             ."write $FAQ::OMatic::Config::metaDir/idfile-new because $!");
354 0           return;
355             }
356              
357 0           foreach $idf (sort keys %idmap) {
358 0           print IDFILE $idmap{$idf}."\n";
359             }
360 0           close IDFILE;
361              
362 0 0         unlink("$FAQ::OMatic::Config::metaDir/idfile") or
363             FAQ::OMatic::gripe('abort', "FAQ::OMatic::Auth::writeIDfile: Couldn't "
364             ."unlink $FAQ::OMatic::Config::metaDir/idfile because $!");
365 0 0         rename("$FAQ::OMatic::Config::metaDir/idfile-new", "$FAQ::OMatic::Config::metaDir/idfile") or
366             FAQ::OMatic::gripe('abort', "FAQ::OMatic::Auth::writeIDfile: Couldn't "
367             ."rename $FAQ::OMatic::Config::metaDir/idfile-new to idfile because $!");
368 0 0         chmod 0600, "$FAQ::OMatic::Config::metaDir/idfile" or
369             FAQ::OMatic::gripe('problem', "FAQ::OMatic::Auth::writeIDfile: Couldn't "
370             ."chmod $FAQ::OMatic::Config::metaDir/idfile because $!");
371            
372 0           FAQ::OMatic::unlockFile($lockf);
373             }
374              
375             # given an id, returns an array starting ($id,$password,...)
376             sub readIDfile {
377 0   0 0 0   my $id = shift || ''; # key to lookup on
378 0   0       my $dontHideVersion = shift || '';
379             # keep regular lookups from seeing version number
380             # record. (smacks of a hack, but this is Perl!)
381              
382 0 0 0       return undef if (($id eq 'version') and (not $dontHideVersion));
383              
384 0           my $lockf = FAQ::OMatic::lockFile("idfile");
385 0 0         FAQ::OMatic::gripe('error', "idfile is locked.") if (not $lockf);
386              
387 0 0         if (not open(IDFILE, "<$FAQ::OMatic::Config::metaDir/idfile")) {
388 0           FAQ::OMatic::unlockFile($lockf);
389 0           FAQ::OMatic::gripe('abort', "FAQ::OMatic::Auth::readIDfile: Couldn't "
390             ."read $FAQ::OMatic::Config::metaDir/idfile because $!");
391 0           return undef;
392             }
393              
394 0           my ($idf,$passf,@restf);
395 0           while (defined($_=)) {
396 0           chomp;
397 0           ($idf,$passf,@restf) = split(' ');
398 0 0         last if ($idf eq $id);
399             }
400 0           close IDFILE;
401              
402 0           FAQ::OMatic::unlockFile($lockf);
403              
404 0 0 0       if (defined($idf) and ($idf eq $id)) {
405 0           return ($idf,$passf,@restf);
406             }
407              
408 0           return undef;
409             }
410              
411             sub checkCryptPass {
412 0     0 0   my ($cleartext, $crypted) = @_;
413 0 0         if ($crypted =~ m/md5\((\S+),(\S+)\)/) {
414             # if this record was encoded with the new md5 encoding, then
415             # it'll contain a big salt and then the result:
416 0           my $salt = $1;
417 0           my $cryptedResult = $2;
418 0           my $attemptedCrypt = md5_hex($salt, $cleartext);
419 0           return ($attemptedCrypt eq $cryptedResult);
420             } else {
421             # compatibility mode: use crypt()
422             # We no longer generate passwords with crypt, but we
423             # allow checking against crypt()ed passwords to avoid
424             # annoying users with a password-reset demand.
425             #my $salt = substr($crypted, 0, 2);
426             # specific fix from Evan Torrie : most crypt()s
427             # don't care of there's excess salt, and those with MD5 crypts use
428             # more than the first two bytes as salt.
429 0           my $salt = $crypted;
430 0           return (crypt($cleartext, $salt) eq $crypted);
431             }
432             }
433              
434             sub cryptPass {
435 0     0 0   my $pass = shift;
436 0           my $salt = FAQ::OMatic::Entropy::gatherRandomString();
437 0           return "md5(".$salt.",".md5_hex($salt.$pass).")";
438             }
439              
440             sub authenticate {
441 0     0 0   my $params = shift;
442              
443 0           my $auth = $params->{'auth'};
444              
445             # if there's a cookie...
446 0 0         if ($auth =~ m/^ck/) {
447 0           my ($cookie,$cid,$ctime) = findCookie($auth,'cookie');
448             # and it's good, then return the implied id
449 0 0         return ($cid,5) if (defined $cid);
450             # if it's bad, fall through and inherit anonymous auth
451             }
452              
453             # if we authenticate...
454 0 0 0       if (($params->{'auth'}||'') eq 'pass' or
      0        
      0        
      0        
      0        
455             ((($params->{'_none_id'}||'') eq '')
456             and (($params->{'_pass_id'}||'') ne ''))) {
457 0           my $id = $params->{'_pass_id'};
458 0           my $pass = $params->{'_pass_pass'};
459 0 0         if (FAQ::OMatic::AuthLocal::checkPassword($id, $pass)) {
460             # set up a cookie to use for a shortcut later,
461             # and return the authentication pair
462 0           $params->{'auth'} = newCookie($id);
463 0           return ($id,5);
464             } else {
465             # let authenticate know to report the bad password
466 0           $params->{'badPass'} = 1;
467             # remove the password from the parameters, since
468             # we don't want it ending up in a later GET request
469             # (and then in server logs). (It got here by a POST
470             # from the password form.)
471 0           $params->{'_pass_id'} = '';
472 0           $params->{'_pass_pass'} = '';
473             # fall through to inherit some crummier Authentication Quality
474             }
475             }
476              
477 0 0 0       if (($params->{'auth'} eq 'none')
478             and (defined $params->{'_none_id'})) {
479             # move id where we can pass it around
480 0           $params->{'id'} = $params->{'_none_id'};
481             }
482              
483             # default authentication: whatever id we can come up with,
484             # but quality is at most 3
485 0   0       my $id = $params->{'id'} || 'anonymous';
486 0 0         my $aq = $params->{'id'} ? 3 : 1;
487 0           return ($id, $aq);
488             }
489              
490             sub authError {
491 0     0 0   my $reason = shift;
492 0           my $file = shift;
493              
494 0           my %staticErrors = (
495             9 => gettext("the administrator of this Faq-O-Matic"),
496             5 => gettext("someone who has proven their identification"),
497             3 => gettext("someone who has offered identification"),
498             1 => gettext("anybody") );
499              
500 0 0         return $staticErrors{$reason} if ($staticErrors{$reason});
501              
502 0 0         if ($reason eq '7') {
503 0           my $modname = '';
504 0 0 0       if (defined($file)
505             && ($file ne '')) {
506             # THANKS "Alan J. Flavell"
507             # for fixing a "Use of uninitialized value" here.
508 0           my $item = new FAQ::OMatic::Item($file);
509 0           $modname = " (".getInheritedProperty($item, 'Moderator').")";
510             }
511 0           return gettext("the moderator of the item").$modname;
512             }
513              
514 0 0         if ($reason =~ m/^6/) {
515 0           return gettexta("%0 group members",FAQ::OMatic::Groups::groupCodeToName($reason));
516             }
517              
518 0           return "I don't know who";
519             }
520              
521             1;
522