File Coverage

blib/lib/Apache/AuthDBI.pm
Criterion Covered Total %
statement 6 7 85.7
branch 1 2 50.0
condition n/a
subroutine 2 2 100.0
pod n/a
total 9 11 81.8


line stmt bran cond sub pod time code
1             package Apache::AuthDBI;
2              
3             $Apache::AuthDBI::VERSION = '0.98';
4              
5             # 1: report about cache miss
6             # 2: full debug output
7             $Apache::AuthDBI::DEBUG = 0;
8              
9 1 50   1   54 use constant MP2 => $ENV{MOD_PERL_API_VERSION} == 2 ? 1 : 0;
  1         2  
  1         151  
10            
11             BEGIN {
12 1     1   8 my @constants = qw( OK AUTH_REQUIRED FORBIDDEN DECLINED SERVER_ERROR );
13 1         3 if (MP2) {
14             require Apache2::Const;
15             import Apache2::Const @constants;
16             }
17             else {
18 1         3796 require Apache::Constants;
19 0           import Apache::Constants @constants;
20             }
21             }
22              
23             use DBI ();
24             use Digest::SHA1;
25             use Digest::MD5;
26             use strict;
27              
28             sub push_handlers {
29             if ( MP2 ) {
30             require Apache2::ServerUtil;
31             my $s = Apache2::ServerUtil->server;
32             $s->push_handlers(@_);
33             }
34             else {
35             Apache->push_handlers(@_);
36             }
37             }
38              
39              
40             # configuration attributes, defaults will be overwritten with values from .htaccess.
41              
42             my %Config = (
43             'Auth_DBI_data_source' => '',
44             'Auth_DBI_username' => '',
45             'Auth_DBI_password' => '',
46             'Auth_DBI_pwd_table' => '',
47             'Auth_DBI_uid_field' => '',
48             'Auth_DBI_pwd_field' => '',
49             'Auth_DBI_pwd_whereclause' => '',
50             'Auth_DBI_grp_table' => '',
51             'Auth_DBI_grp_field' => '',
52             'Auth_DBI_grp_whereclause' => '',
53             'Auth_DBI_log_field' => '',
54             'Auth_DBI_log_string' => '',
55             'Auth_DBI_authoritative' => 'on',
56             'Auth_DBI_nopasswd' => 'off',
57             'Auth_DBI_encrypted' => 'on',
58             'Auth_DBI_encryption_salt' => 'password',
59             'Auth_DBI_encryption_method'=> 'sha1hex/md5/crypt', #Using Two (or more) Methods Will Allow for Fallback to older Methods
60             'Auth_DBI_uidcasesensitive' => 'on',
61             'Auth_DBI_pwdcasesensitive' => 'on',
62             'Auth_DBI_placeholder' => 'off',
63             );
64              
65             # stores the configuration of current URL.
66             # initialized during authentication, eventually re-used for authorization.
67             my $Attr = { };
68              
69              
70             # global cache: all records are put into one string.
71             # record separator is a newline. Field separator is $;.
72             # every record is a list of id, time of last access, password, groups (authorization only).
73             # the id is a comma separated list of user_id, data_source, pwd_table, uid_field.
74             # the first record is a timestamp, which indicates the last run of the CleanupHandler followed by the child counter.
75              
76             my $Cache = time . "$;0\n";
77              
78             # unique id which serves as key in $Cache.
79             # the id is generated during authentication and re-used for authorization.
80             my $ID;
81              
82              
83             # minimum lifetimes of cache entries in seconds.
84             # setting the CacheTime to 0 will not use the cache at all.
85              
86             my $CacheTime = 0;
87              
88             # supposed to be called in a startup script.
89             # sets CacheTime to a user defined value.
90              
91             sub setCacheTime {
92             my $class = shift;
93             my $cache_time = shift;
94             # sanity check
95             $CacheTime = $cache_time if ($cache_time =~ /\d+/);
96             }
97              
98              
99             # minimum time interval in seconds between two runs of the PerlCleanupHandler.
100             # setting CleanupTime to 0 will run the PerlCleanupHandler after every request.
101             # setting CleanupTime to a negative value will disable the PerlCleanupHandler.
102              
103             my $CleanupTime = -1;
104              
105             # supposed to be called in a startup script.
106             # sets CleanupTime to a user defined value.
107              
108             sub setCleanupTime {
109             my $class = shift;
110             my $cleanup_time = shift;
111             # sanity check
112             $CleanupTime = $cleanup_time if ($cleanup_time =~ /\-*\d+/);
113             }
114              
115              
116             # optionally the string with the global cache can be stored in a shared memory segment.
117             # the segment will be created from the first child and it will be destroyed if the last child exits.
118             # the reason for not handling everything in the main server is simply, that there is no way to setup
119             # an ExitHandler which runs in the main server and which would remove the shared memory and the semaphore.
120             # hence we have to keep track about the number of children, so that the last one can do all the cleanup.
121             # creating the shared memory in the first child also has the advantage, that we don't have to cope
122             # with changing the ownership.
123             # if a shm-function fails, the global cache will automatically fall back to one string per process.
124              
125             my $SHMKEY = 0; # unique key for shared memory segment and semaphore set
126             my $SEMID = 0; # id of semaphore set
127             my $SHMID = 0; # id of shared memory segment
128             my $SHMSIZE = 50000; # default size of shared memory segment
129             my $SHMPROJID = 1; # default project id for shared memory segment
130              
131             # Supposed to be called in a startup script.
132             # Sets SHMPROJID to a user defined value
133             sub setProjID {
134             my $class = shift;
135             my $shmprojid = shift;
136              
137             #Set ProjID prior to calling initIPC!
138             return if $SHMKEY;
139              
140             # sanity check - Must be numeric and less than or equal to 255
141             $SHMPROJID = int($shmprojid) if ($shmprojid =~ /\d{1,3}/ && $shmprojid <= 255 && $shmprojid > 0);
142             }
143              
144              
145             # shortcuts for semaphores
146             my $obtain_lock = pack("sss", 0, 0, 0) . pack("sss", 0, 1, 0);
147             my $release_lock = pack("sss", 0, -1, 0);
148              
149             # supposed to be called in a startup script.
150             # sets SHMSIZE to a user defined value and initializes the unique key, used for the shared memory segment and for the semaphore set.
151             # creates a PerlChildInitHandler which creates the shared memory segment and the semaphore set.
152             # creates a PerlChildExitHandler which removes the shared memory segment and the semaphore set upon server shutdown.
153             # keep in mind, that this routine runs only once, when the main server starts up.
154              
155             sub initIPC {
156             my $class = shift;
157             my $shmsize = shift;
158             require IPC::SysV;
159              
160             # make sure, this method is called only once
161             return if $SHMKEY;
162              
163             # ensure minimum size of shared memory segment
164             $SHMSIZE = $shmsize if $shmsize >= 500;
165              
166             # generate unique key based on path of AuthDBI.pm + SHMPROJID
167             foreach my $file (keys %INC) {
168             if ($file eq 'Apache/AuthDBI.pm') {
169             $SHMKEY = IPC::SysV::ftok($INC{$file}, $SHMPROJID);
170             last;
171             }
172             }
173              
174             # provide a handler which initializes the shared memory segment (first child)
175             # or which increments the child counter.
176             push_handlers( PerlChildInitHandler => \&childinit);
177              
178             # provide a handler which decrements the child count or which destroys the shared memory
179             # segment upon server shutdown, which is defined by the exit of the last child.
180             push_handlers( PerlChildExitHandler => \&childexit);
181             }
182              
183              
184             # authentication handler
185              
186             sub authen {
187              
188             my ($r) = @_;
189             my ($key, $val, $dbh);
190              
191             my $prefix = "$$ Apache::AuthDBI::authen";
192              
193             if ($Apache::AuthDBI::DEBUG > 1) {
194             my ($type) = '';
195             if (MP2) {
196             $type .= 'initial ' if $r->is_initial_req();
197             $type .= 'main' if $r->main();
198             } else {
199             $type .= 'initial ' if $r->is_initial_req;
200             $type .= 'main' if $r->is_main;
201             }
202             print STDERR "==========\n$prefix request type = >$type< \n";
203             }
204              
205             return MP2 ? Apache2::Const::OK() : Apache::Constants::OK() unless $r->is_initial_req; # only the first internal request
206              
207             print STDERR "REQUEST:\n", $r->as_string if $Apache::AuthDBI::DEBUG > 1;
208              
209             # here the dialog pops up and asks you for username and password
210             my($res, $passwd_sent) = $r->get_basic_auth_pw;
211             print STDERR "$prefix get_basic_auth_pw: res = >$res<, password sent = >$passwd_sent<\n" if $Apache::AuthDBI::DEBUG > 1;
212             return $res if $res; # e.g. HTTP_UNAUTHORIZED
213              
214             # get username
215             my ($user_sent) = $r->user;
216             print STDERR "$prefix user sent = >$user_sent<\n" if $Apache::AuthDBI::DEBUG > 1;
217              
218             # do we use shared memory for the global cache ?
219             print STDERR "$prefix cache in shared memory, shmid $SHMID, shmsize $SHMSIZE, semid $SEMID \n" if ($SHMID and $Apache::AuthDBI::DEBUG > 1);
220              
221             # get configuration
222             while(($key, $val) = each %Config) {
223             $val = $r->dir_config($key) || $val;
224             $key =~ s/^Auth_DBI_//;
225             $Attr->{$key} = $val;
226             printf STDERR "$prefix Config{ %-16s } = %s\n", $key, $val if $Apache::AuthDBI::DEBUG > 1;
227             }
228              
229             # parse connect attributes, which may be tilde separated lists
230             my @data_sources = split(/~/, $Attr->{data_source});
231             my @usernames = split(/~/, $Attr->{username});
232             my @passwords = split(/~/, $Attr->{password});
233             $data_sources[0] = '' unless $data_sources[0]; # use ENV{DBI_DSN} if not defined
234              
235             # obtain the id for the cache
236             my $data_src = $Attr->{data_source};
237             $data_src =~ s/\(.+\)//go; # remove any embedded attributes, because of trouble with regexps
238             $ID = join ',', $user_sent, $data_src, $Attr->{pwd_table}, $Attr->{uid_field};
239              
240             # if not configured decline
241             unless ($Attr->{pwd_table} && $Attr->{uid_field} && $Attr->{pwd_field}) {
242             print STDERR "$prefix not configured, return DECLINED\n" if $Apache::AuthDBI::DEBUG > 1;
243             return MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
244             }
245              
246             # do we want Windows-like case-insensitivity?
247             $user_sent = lc($user_sent) if $Attr->{uidcasesensitive} eq "off";
248             $passwd_sent = lc($passwd_sent) if $Attr->{pwdcasesensitive} eq "off";
249              
250             # check whether the user is cached but consider that the password possibly has changed
251             my $passwd = '';
252             if ($CacheTime) { # do we use the cache ?
253             if ($SHMID) { # do we keep the cache in shared memory ?
254             semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
255             shmread($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmread failed \n";
256             substr($Cache, index($Cache, "\0")) = '';
257             semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
258             }
259             # find id in cache
260             my ($last_access, $passwd_cached, $groups_cached);
261             if ($Cache =~ /$ID$;(\d+)$;(.+)$;(.*)\n/) {
262             $last_access = $1;
263             $passwd_cached = $2;
264             $groups_cached = $3;
265             print STDERR "$prefix cache: found >$ID< >$last_access< >$passwd_cached< \n" if $Apache::AuthDBI::DEBUG > 1;
266             my (@passwds_to_check, $passwd_to_check);
267            
268             @passwds_to_check = &get_passwds_to_check($Attr, user_sent=>$user_sent, passwd_sent=>$passwd_sent, password=>$passwd_cached);
269            
270             print STDERR "$prefix ". scalar(@passwds_to_check) . " passwords to check\n" if $Apache::AuthDBI::DEBUG > 1;;
271             foreach $passwd_to_check(@passwds_to_check) {
272             # match cached password with password sent
273             $passwd = $passwd_cached if $passwd_to_check eq $passwd_cached;
274             if ($passwd) {
275             last;
276             }
277             }
278             }
279             }
280              
281             if ($passwd) { # found in cache
282             print STDERR "$prefix passwd found in cache \n" if $Apache::AuthDBI::DEBUG > 1;
283             } else { # password not cached or changed
284             print STDERR "$prefix passwd not found in cache \n" if $Apache::AuthDBI::DEBUG;
285             # connect to database, use all data_sources until the connect succeeds
286             my $j;
287             for ($j = 0; $j <= $#data_sources; $j++) {
288             last if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j]));
289             }
290             unless ($dbh) {
291             $r->log_reason("$prefix db connect error with data_source >$Attr->{data_source}<: $DBI::errstr", $r->uri);
292             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
293             }
294              
295             # generate statement
296             my $user_sent_quoted = $dbh->quote($user_sent);
297             my $select = "SELECT $Attr->{pwd_field}";
298             my $from = "FROM $Attr->{pwd_table}";
299             my $where = ($Attr->{uidcasesensitive} eq "off") ? "WHERE lower($Attr->{uid_field}) =" : "WHERE $Attr->{uid_field} =";
300             my $compare = ($Attr->{placeholder} eq "on") ? "?" : "$user_sent_quoted";
301             my $statement = "$select $from $where $compare";
302             $statement .= " AND $Attr->{pwd_whereclause}" if $Attr->{pwd_whereclause};
303             print STDERR "$prefix statement: $statement\n" if $Apache::AuthDBI::DEBUG > 1;
304              
305             # prepare statement
306             my $sth;
307             unless ($sth = $dbh->prepare($statement)) {
308             $r->log_reason("$prefix can not prepare statement: $DBI::errstr", $r->uri);
309             $dbh->disconnect;
310             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
311             }
312              
313             # execute statement
314             my $rv;
315             unless ($rv = ($Attr->{placeholder} eq "on") ? $sth->execute($user_sent) : $sth->execute) {
316             $r->log_reason("$prefix can not execute statement: $DBI::errstr", $r->uri);
317             $dbh->disconnect;
318             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
319             }
320              
321             # fetch result
322             while ($_ = $sth->fetchrow_array) {
323             # strip trailing blanks for fixed-length data-type
324             $_ =~ s/ +$// if $_;
325             # consider the case with many users sharing the same userid
326             $passwd .= "$_$;";
327             }
328              
329             chop $passwd if $passwd;
330             undef $passwd if 0 == $sth->rows; # so we can distinguish later on between no password and empty password
331              
332             if ($sth->err) {
333             $dbh->disconnect;
334             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
335             }
336             $sth->finish;
337              
338             # re-use dbh for logging option below
339             $dbh->disconnect unless ($Attr->{log_field} && $Attr->{log_string});
340             }
341              
342             $r->subprocess_env(REMOTE_PASSWORDS => $passwd);
343             print STDERR "$prefix passwd = >$passwd<\n" if $Apache::AuthDBI::DEBUG > 1;
344              
345             # check if password is needed
346             if (!defined($passwd)) { # not found in database
347             # if authoritative insist that user is in database
348             if ($Attr->{authoritative} eq 'on') {
349             $r->log_reason("$prefix password for user $user_sent not found", $r->uri);
350             $r->note_basic_auth_failure;
351             return MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED();
352             } else {
353             # else pass control to the next authentication module
354             return MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
355             }
356             }
357              
358             # allow any password if nopasswd = on and the retrieved password is empty
359             if ($Attr->{nopasswd} eq 'on' && !$passwd) {
360             return MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
361             }
362              
363             # if nopasswd is off, reject user
364             unless ($passwd_sent && $passwd) {
365             $r->log_reason("$prefix user $user_sent: empty password(s) rejected", $r->uri);
366             $r->note_basic_auth_failure;
367             return MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED();
368             }
369              
370             # compare passwords
371             my $found = 0;
372             my $password;
373             foreach $password (split(/$;/, $passwd)) {
374             # compare all the passwords using as many encryption methods in fallback as needed
375             my (@passwds_to_check, $passwd_to_check);
376              
377             @passwds_to_check = &get_passwds_to_check($Attr, user_sent=>$user_sent, passwd_sent=>$passwd_sent, password=>$password);
378              
379             print STDERR "$prefix ". scalar(@passwds_to_check) . " passwords to check\n" if $Apache::AuthDBI::DEBUG > 1;
380             foreach $passwd_to_check(@passwds_to_check) {
381             print STDERR "$prefix user $user_sent: Password after Preparation >$passwd_to_check< - trying for a match with >$password< \n" if $Apache::AuthDBI::DEBUG > 1;
382             if ($passwd_to_check eq $password) {
383             $found = 1;
384             $r->subprocess_env(REMOTE_PASSWORD => $password);
385             print STDERR "$prefix user $user_sent: Password from Web Server >$passwd_sent< - Password after Preparation >$passwd_to_check< - password match for >$password< \n" if $Apache::AuthDBI::DEBUG > 1;
386             # update timestamp and cache userid/password if CacheTime is configured
387             if ($CacheTime) { # do we use the cache ?
388             if ($SHMID) { # do we keep the cache in shared memory ?
389             semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
390             shmread($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmread failed \n";
391             substr($Cache, index($Cache, "\0")) = '';
392             }
393             # update timestamp and password or append new record
394             my $now = time;
395             if (!($Cache =~ s/$ID$;\d+$;.*$;(.*)\n/$ID$;$now$;$password$;$1\n/)) {
396             $Cache .= "$ID$;$now$;$password$;\n";
397             } else {
398             }
399             if ($SHMID) { # write cache to shared memory
400             shmwrite($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmwrite failed \n";
401             semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
402             }
403             }
404             last;
405             }
406             }
407              
408             #if the passwd matched (encrypted or otherwise), don't check the myriad other passwords that may or may not exist
409             if ($found > 0) {
410             last;
411             }
412             }
413             unless ($found) {
414             $r->log_reason("$prefix user $user_sent: password mismatch", $r->uri);
415             $r->note_basic_auth_failure;
416             return MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED();
417             }
418              
419             # logging option
420             if ($Attr->{log_field} && $Attr->{log_string}) {
421             if (!$dbh) { # connect to database if not already done
422             my ($j, $connect);
423             for ($j = 0; $j <= $#data_sources; $j++) {
424             if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j])) {
425             $connect = 1;
426             last;
427             }
428             }
429             unless ($connect) {
430             $r->log_reason("$prefix db connect error with $Attr->{data_source}", $r->uri);
431             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
432             }
433             }
434             my $user_sent_quoted = $dbh->quote($user_sent);
435             my $statement = "UPDATE $Attr->{pwd_table} SET $Attr->{log_field} = $Attr->{log_string} WHERE $Attr->{uid_field}=$user_sent_quoted";
436             print STDERR "$prefix statement: $statement\n" if $Apache::AuthDBI::DEBUG > 1;
437             unless ($dbh->do($statement)) {
438             $r->log_reason("$prefix can not do statement: $DBI::errstr", $r->uri);
439             $dbh->disconnect;
440             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
441             }
442             $dbh->disconnect;
443             }
444              
445             # Unless the cache or the CleanupHandler is disabled, the CleanupHandler is initiated
446             # if the last run was more than $CleanupTime seconds before.
447             # Note, that it runs after the request, hence it cleans also the authorization entries
448             if ($CacheTime and $CleanupTime >= 0) {
449             my $diff = time - substr($Cache, 0, index($Cache, "$;"));
450             print STDERR "$prefix secs since last CleanupHandler: $diff, CleanupTime: $CleanupTime \n" if $Apache::AuthDBI::DEBUG > 1;
451             if ($diff > $CleanupTime) {
452             print STDERR "$prefix push PerlCleanupHandler \n" if $Apache::AuthDBI::DEBUG > 1;
453             push_handlers( PerlCleanupHandler => \&cleanup);
454             }
455             }
456              
457             print STDERR "$prefix return OK\n" if $Apache::AuthDBI::DEBUG > 1;
458             return MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
459             }
460              
461             #Encrypts a password in all supported/requested methods and passes back array for comparison
462             sub get_passwds_to_check {
463             my ($Attr, %params) = @_;
464              
465             my ($prefix) = "$$ Apache::AuthDBI::get_passwds_to_check ";
466              
467             my ($salt, @passwds_to_check);
468              
469             if ($Attr->{encrypted} eq 'on') {
470             #SHA1
471             if ($Attr->{encryption_method} =~ /(^|\/)sha1hex($|\/)/i) {
472             push (@passwds_to_check, &SHA1_digest(text=>$params{'passwd_sent'}, format=>'hex'));
473             }
474             #MD5
475             if ($Attr->{encryption_method} =~ /(^|\/)md5hex($|\/)/i) {
476             push (@passwds_to_check, &MD5_digest(text=>$params{'passwd_sent'}, format=>'hex'));
477             }
478             #CRYPT
479             if ($Attr->{encryption_method} =~ /(^|\/)crypt($|\/)/i) {
480             $salt = $Attr->{encryption_salt} eq 'userid' ? $params{'user_sent'} : $params{'password'};
481             #Bug Fix in v0.94 (marked as 0.93 in file. salt was NOT being sent to crypt) - KAM - 06-16-2005
482             push (@passwds_to_check, crypt($params{'passwd_sent'}, $salt));
483             }
484              
485             #WE DIDN'T GET ANY PASSWORDS TO CHECK. MUST BE A PROBLEM
486             if (scalar(@passwds_to_check) < 1) {
487             print STDERR "$prefix Error: No Valid Encryption Method Specified.\n" if $Apache::AuthDBI::DEBUG > 1;
488             }
489             } else {
490             #IF NO ENCRYPTION, JUST PUSH THE CLEARTEXT PASS
491             push (@passwds_to_check, $params{'passwd_sent'});
492             }
493              
494             return (@passwds_to_check);
495             }
496              
497             # authorization handler, it is called immediately after the authentication
498             sub authz {
499              
500             my ($r) = @_;
501             my ($key, $val, $dbh);
502              
503             my ($prefix) = "$$ Apache::AuthDBI::authz ";
504              
505             if ($Apache::AuthDBI::DEBUG > 1) {
506             my ($type) = '';
507             if (MP2) {
508             $type .= 'initial ' if $r->is_initial_req();
509             $type .= 'main' if $r->main();
510             } else {
511             $type .= 'initial ' if $r->is_initial_req;
512             $type .= 'main' if $r->is_main;
513             }
514             print STDERR "==========\n$prefix request type = >$type< \n";
515             }
516              
517             unless ($r->is_initial_req) {
518             return MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
519             }; # only the first internal request
520              
521             my ($user_result) = MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
522             my ($group_result) = MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
523              
524             # get username
525             my ($user_sent) = $r->user;
526             print STDERR "$prefix user sent = >$user_sent<\n" if $Apache::AuthDBI::DEBUG > 1 ;
527              
528             # here we could read the configuration, but we re-use the configuration from the authentication
529              
530             # parse connect attributes, which may be tilde separated lists
531             my @data_sources = split(/~/, $Attr->{data_source});
532             my @usernames = split(/~/, $Attr->{username});
533             my @passwords = split(/~/, $Attr->{password});
534             $data_sources[0] = '' unless $data_sources[0]; # use ENV{DBI_DSN} if not defined
535              
536             # if not configured decline
537             unless ($Attr->{pwd_table} && $Attr->{uid_field} && $Attr->{grp_field}) {
538             print STDERR "$prefix not configured, return DECLINED\n" if $Apache::AuthDBI::DEBUG > 1;
539             return MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
540             }
541              
542             # do we want Windows-like case-insensitivity?
543             $user_sent = lc($user_sent) if $Attr->{uidcasesensitive} eq "off";
544              
545             # select code to return if authorization is denied:
546             my ($authz_denied);
547             if (MP2) {
548             $authz_denied = $Attr->{expeditive} eq 'on' ? Apache2::Const::FORBIDDEN() : Apache2::Const::AUTH_REQUIRED();
549             } else {
550             $authz_denied = $Attr->{expeditive} eq 'on' ? Apache::Constants::FORBIDDEN() : Apache::Constants::AUTH_REQUIRED();
551             }
552              
553             # check if requirements exists
554             my ($ary_ref) = $r->requires;
555             unless ($ary_ref) {
556             if ($Attr->{authoritative} eq 'on') {
557             $r->log_reason("user $user_sent denied, no access rules specified (DBI-Authoritative)", $r->uri);
558             if ($authz_denied == MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED()) {
559             $r->note_basic_auth_failure;
560             }
561             return $authz_denied;
562             }
563             print STDERR "$prefix no requirements and not authoritative, return DECLINED\n" if $Apache::AuthDBI::DEBUG > 1;
564             return MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
565             }
566              
567             # iterate over all requirement directives and store them according to their type (valid-user, user, group)
568             my($hash_ref, $valid_user, $user_requirements, $group_requirements);
569             foreach $hash_ref (@$ary_ref) {
570             while (($key,$val) = each %$hash_ref) {
571             last if $key eq 'requirement';
572             }
573             $val =~ s/^\s*require\s+//;
574             # handle different requirement-types
575             if ($val =~ /valid-user/) {
576             $valid_user = 1;
577             } elsif ($val =~ s/^user\s+//go) {
578             $user_requirements .= " $val";
579             } elsif ($val =~ s/^group\s+//go) {
580             $group_requirements .= " $val";
581             }
582             }
583             $user_requirements =~ s/^ //go;
584             $group_requirements =~ s/^ //go;
585             print STDERR "$prefix requirements: valid-user=>$valid_user< user=>$user_requirements< group=>$group_requirements< \n" if $Apache::AuthDBI::DEBUG > 1;
586              
587             # check for valid-user
588             if ($valid_user) {
589             $user_result = MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
590             print STDERR "$prefix user_result = OK: valid-user\n" if $Apache::AuthDBI::DEBUG > 1;
591             }
592              
593             # check for users
594             if (($user_result != MP2 ? Apache2::Const::OK() : Apache::Constants::OK()) && $user_requirements) {
595             $user_result = MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED();
596             my $user_required;
597             foreach $user_required (split /\s+/, $user_requirements) {
598             if ($user_required eq $user_sent) {
599             print STDERR "$prefix user_result = OK for $user_required \n" if $Apache::AuthDBI::DEBUG > 1;
600             $user_result = MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
601             last;
602             }
603             }
604             }
605              
606             # check for groups
607             if (($user_result != MP2 ? Apache2::Const::OK() : Apache::Constants::OK()) && $group_requirements) {
608             $group_result = MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED();
609             my ($group, $group_required);
610              
611             # check whether the user is cached but consider that the group possibly has changed
612             my $groups = '';
613             if ($CacheTime) { # do we use the cache ?
614             # we need to get the cached groups for the current id, which has been read already
615             # during authentication, so we do not read the Cache from shared memory again
616             my ($last_access, $passwd_cached, $groups_cached);
617             if ($Cache =~ /$ID$;(\d+)$;(.*)$;(.+)\n/) {
618             $last_access = $1;
619             $passwd_cached = $2;
620             $groups_cached = $3;
621             print STDERR "$prefix cache: found >$ID< >$last_access< >$groups_cached< \n" if $Apache::AuthDBI::DEBUG > 1;
622             REQUIRE_1: foreach $group_required (split /\s+/, $group_requirements) {
623             foreach $group (split(/,/, $groups_cached)) {
624             if ($group_required eq $group) {
625             $groups = $groups_cached;
626             last REQUIRE_1;
627             }
628             }
629             }
630             }
631             }
632              
633             if ($groups) { # found in cache
634             print STDERR "$prefix groups found in cache \n" if $Apache::AuthDBI::DEBUG > 1;
635             } else { # groups not cached or changed
636             print STDERR "$prefix groups not found in cache \n" if $Apache::AuthDBI::DEBUG;
637              
638             # connect to database, use all data_sources until the connect succeeds
639             my ($j, $connect);
640             for ($j = 0; $j <= $#data_sources; $j++) {
641             if ($dbh = DBI->connect($data_sources[$j], $usernames[$j], $passwords[$j])) {
642             $connect = 1;
643             last;
644             }
645             }
646             unless ($connect) {
647             $r->log_reason("$prefix db connect error with $Attr->{data_source}", $r->uri);
648             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
649             }
650              
651             # generate statement
652             my $user_sent_quoted = $dbh->quote($user_sent);
653             my $select = "SELECT $Attr->{grp_field}";
654             my $from = ($Attr->{grp_table}) ? "FROM $Attr->{grp_table}" : "FROM $Attr->{pwd_table}";
655             my $where = ($Attr->{uidcasesensitive} eq "off") ? "WHERE lower($Attr->{uid_field}) =" : "WHERE $Attr->{uid_field} =";
656             my $compare = ($Attr->{placeholder} eq "on") ? "?" : "$user_sent_quoted";
657             my $statement = "$select $from $where $compare";
658             $statement .= " AND $Attr->{grp_whereclause}" if ($Attr->{grp_whereclause});
659             print STDERR "$prefix statement: $statement\n" if $Apache::AuthDBI::DEBUG > 1;
660              
661             # prepare statement
662             my $sth;
663             unless ($sth = $dbh->prepare($statement)) {
664             $r->log_reason("can not prepare statement: $DBI::errstr", $r->uri);
665             $dbh->disconnect;
666             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
667             }
668              
669             # execute statement
670             my $rv;
671             unless ($rv = ($Attr->{placeholder} eq "on") ? $sth->execute($user_sent) : $sth->execute) {
672             $r->log_reason("can not execute statement: $DBI::errstr", $r->uri);
673             $dbh->disconnect;
674             return MP2 ? Apache2::Const::SERVER_ERROR() : Apache::Constants::SERVER_ERROR();
675             }
676              
677             # fetch result and build a group-list
678             my $group;
679             while ( $group = $sth->fetchrow_array ) {
680             # strip trailing blanks for fixed-length data-type
681             $group =~ s/ +$//;
682             $groups .= "$group,";
683             }
684             chop $groups if $groups;
685              
686             $sth->finish;
687             $dbh->disconnect;
688             }
689              
690             $r->subprocess_env(REMOTE_GROUPS => $groups);
691             print STDERR "$prefix groups = >$groups<\n" if $Apache::AuthDBI::DEBUG > 1;
692              
693             # skip through the required groups until the first matches
694             REQUIRE_2: foreach $group_required (split /\s+/, $group_requirements) {
695             foreach $group (split(/,/, $groups)) {
696             # check group
697             if ($group_required eq $group) {
698             $group_result = MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
699             $r->subprocess_env(REMOTE_GROUP => $group);
700             print STDERR "$prefix user $user_sent: group_result = OK for >$group< \n" if $Apache::AuthDBI::DEBUG > 1;
701             # update timestamp and cache userid/groups if CacheTime is configured
702             if ($CacheTime) { # do we use the cache ?
703             if ($SHMID) { # do we keep the cache in shared memory ?
704             semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
705             shmread($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmread failed \n";
706             substr($Cache, index($Cache, "\0")) = '';
707             }
708             # update timestamp and groups
709             my $now = time;
710             # entry must exists from authentication
711             $Cache =~ s/$ID$;\d+$;(.*)$;.*\n/$ID$;$now$;$1$;$groups\n/;
712             if ($SHMID) { # write cache to shared memory
713             shmwrite($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmwrite failed \n";
714             semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
715             }
716             }
717             last REQUIRE_2;
718             }
719             }
720             }
721             }
722              
723             # check the results of the requirement checks
724             if ($Attr->{authoritative} eq 'on' && ($user_result != MP2 ? Apache2::Const::OK() : Apache::Constants::OK()) && ($group_result != MP2 ? Apache2::Const::OK() : Apache::Constants::OK())) {
725             my $reason;
726             if ($user_result == MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED()) {
727             $reason .= " USER";
728             }
729             if ($group_result == MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED()) {
730             $reason .= " GROUP";
731             }
732             $r->log_reason("DBI-Authoritative: Access denied on $reason rule(s)", $r->uri);
733            
734             if ($authz_denied == MP2 ? Apache2::Const::AUTH_REQUIRED() : Apache::Constants::AUTH_REQUIRED()) {
735             $r->note_basic_auth_failure;
736             }
737             return $authz_denied;
738             }
739              
740             # return OK if authorization was successful
741             if (($user_result == MP2 ? Apache2::Const::OK() : Apache::Constants::OK()) || ($group_result == MP2 ? Apache2::Const::OK() : Apache::Constants::OK())) {
742             print STDERR "$prefix return OK\n" if $Apache::AuthDBI::DEBUG > 1;
743             return MP2 ? Apache2::Const::OK() : Apache::Constants::OK();
744             }
745              
746             # otherwise fall through
747             print STDERR "$prefix fall through, return DECLINED\n" if $Apache::AuthDBI::DEBUG > 1;
748             return MP2 ? Apache2::Const::DECLINED() : Apache::Constants::DECLINED();
749             }
750              
751              
752             sub dec2hex($) {
753             my ($dec) = @_;
754              
755             return sprintf("%lx", $dec );
756             }
757              
758             # The PerlChildInitHandler initializes the shared memory segment (first child)
759             # or increments the child counter.
760             # Note: this handler runs in every child server, but not in the main server.
761              
762             sub childinit {
763             my $prefix = "$$ Apache::AuthDBI PerlChildInitHandler";
764             # create (or re-use existing) semaphore set
765              
766             my ($SHMKEY_hex);
767              
768             $SHMKEY_hex = &dec2hex($SHMKEY);
769              
770             print STDERR "$prefix SHMProjID = >$SHMPROJID< Shared Memory Key >$SHMKEY Decimal - $SHMKEY_hex Hex<\n" if $Apache::AuthDBI::DEBUG > 1;
771              
772             $SEMID = semget($SHMKEY, 1,
773             IPC::SysV::IPC_CREAT() | IPC::SysV::S_IRUSR() | IPC::SysV::S_IWUSR());
774             if (!defined($SEMID)) {
775             print STDERR "$prefix semget failed - SHMKEY $SHMKEY - Error $!\n";
776             if (uc(chomp($!)) eq 'PERMISSION DENIED') {
777             print STDERR " $prefix Read/Write Permission Denied to Shared Memory Array.\n";
778             print STDERR " $prefix Use ipcs -s to list semaphores and look for $SHMKEY_hex. If found, shutdown Apache and use ipcrm sem $SHMKEY_hex to remove the colliding (and hopefully unused) semaphore. See documentation for setProjID for more information. \n";
779             }
780             return;
781            
782             }
783              
784             # create (or re-use existing) shared memory segment
785             $SHMID = shmget($SHMKEY, $SHMSIZE,
786             IPC::SysV::IPC_CREAT() | IPC::SysV::S_IRUSR() | IPC::SysV::S_IWUSR());
787             if (!defined($SHMID)) {
788             print STDERR "$prefix shmget failed - Error $!\n";
789             return;
790             }
791             # make ids accessible to other handlers
792             $ENV{AUTH_SEMID} = $SEMID;
793             $ENV{AUTH_SHMID} = $SHMID;
794             # read shared memory, increment child count and write shared memory segment
795             semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
796             shmread($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmread failed \n";
797             substr($Cache, index($Cache, "\0")) = '';
798             my $child_count_new = 1;
799             if ($Cache =~ /^(\d+)$;(\d+)\n/) { # segment already exists (eg start of additional server)
800             my $time_stamp = $1;
801             my $child_count = $2;
802             $child_count_new = $child_count + 1;
803             $Cache =~ s/^$time_stamp$;$child_count\n/$time_stamp$;$child_count_new\n/;
804             } else { # first child => initialize segment
805             $Cache = time . "$;$child_count_new\n";
806             }
807             print STDERR "$prefix child count = $child_count_new \n" if $Apache::AuthDBI::DEBUG > 1;
808             shmwrite($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmwrite failed \n";
809             semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
810             1;
811             }
812              
813              
814             # The PerlChildExitHandler decrements the child count or destroys the shared memory
815             # segment upon server shutdown, which is defined by the exit of the last child.
816             # Note: this handler runs in every child server, but not in the main server.
817              
818             sub childexit {
819             my $prefix = "$$ Apache::AuthDBI PerlChildExitHandler";
820             # read Cache from shared memory, decrement child count and exit or write Cache to shared memory
821             semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
822             shmread($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmread failed \n";
823             substr($Cache, index($Cache, "\0")) = '';
824             $Cache =~ /^(\d+)$;(\d+)\n/;
825             my $time_stamp = $1;
826             my $child_count = $2;
827             my $child_count_new = $child_count - 1;
828             if ($child_count_new) {
829             print STDERR "$prefix child count = $child_count \n" if $Apache::AuthDBI::DEBUG > 1;
830             # write Cache into shared memory
831             $Cache =~ s/^$time_stamp$;$child_count\n/$time_stamp$;$child_count_new\n/;
832             shmwrite($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmwrite failed \n";
833             semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
834             } else { # last child
835             # remove shared memory segment and semaphore set
836             print STDERR "$prefix child count = $child_count, remove shared memory $SHMID and semaphore $SEMID \n" if $Apache::AuthDBI::DEBUG > 1;
837             shmctl($SHMID, IPC::SysV::IPC_RMID(), 0) or print STDERR "$prefix shmctl failed \n";
838             semctl($SEMID, 0, IPC::SysV::IPC_RMID(), 0) or print STDERR "$prefix semctl failed \n";
839             }
840             1;
841             }
842              
843              
844             # The PerlCleanupHandler skips through the cache and deletes any outdated entry.
845             # Note: this handler runs after the response has been sent to the client.
846              
847             sub cleanup {
848             my $prefix = "$$ Apache::AuthDBI PerlCleanupHandler";
849             print STDERR "$prefix \n" if $Apache::AuthDBI::DEBUG > 1;
850             my $now = time;
851             if ($SHMID) { # do we keep the cache in shared memory ?
852             semop($SEMID, $obtain_lock) or print STDERR "$prefix semop failed \n";
853             shmread($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmread failed \n";
854             substr($Cache, index($Cache, "\0")) = '';
855             }
856             my $newCache = "$now$;"; # initialize timestamp for CleanupHandler
857             my ($time_stamp, $child_count);
858             foreach my $record (split(/\n/, $Cache)) {
859             if (!$time_stamp) { # first record: timestamp of CleanupHandler and child count
860             ($time_stamp, $child_count) = split(/$;/, $record);
861             $newCache .= "$child_count\n";
862             next;
863             }
864             my ($id, $last_access, $passwd, $groups) = split(/$;/, $record);
865             my $diff = $now - $last_access;
866             if ($diff >= $CacheTime) {
867             print STDERR "$prefix delete >$id<, last access $diff s before \n" if $Apache::AuthDBI::DEBUG > 1;
868             } else {
869             print STDERR "$prefix keep >$id<, last access $diff s before \n" if $Apache::AuthDBI::DEBUG > 1;
870             $newCache .= "$id$;$now$;$passwd$;$groups\n";
871             }
872             }
873             $Cache = $newCache;
874             if ($SHMID) { # write Cache to shared memory
875             shmwrite($SHMID, $Cache, 0, $SHMSIZE) or print STDERR "$prefix shmwrite failed \n";
876             semop($SEMID, $release_lock) or print STDERR "$prefix semop failed \n";
877             }
878             1;
879             }
880              
881             # Added 06-14-2005 - KAM - Returns SHA1 digest - Modified from PerlCMS' more generic routine to remove IO::File requirement
882             sub SHA1_digest {
883             my $prefix = "$$ Apache::AuthDBI SHA1_digest";
884             print STDERR "$prefix \n" if $Apache::AuthDBI::DEBUG > 1;
885             my (%params) = @_;
886             my ($sha1);
887              
888             $params{'format'} ||= "base64";
889              
890             $sha1 = Digest::SHA1->new;
891              
892             if ($params{'text'} ne '') {
893             $sha1->add($params{'text'});
894             } else {
895             return -1;
896             }
897            
898             if ($params{'format'} =~ /base64/i) {
899             return $sha1->b64digest;
900             } elsif ($params{'format'} =~ /hex/i) {
901             return $sha1->hexdigest;
902             } elsif ($params{'format'} =~ /binary/i) {
903             return $sha1->binary;
904             }
905              
906             return -1;
907             }
908              
909             # Added 06-20-2005 - KAM - Returns MD5 digest - Modified from PerlCMS' more generic routine to remove IO::File requirement
910             sub MD5_digest {
911             my $prefix = "$$ Apache::AuthDBI MD5_digest";
912             print STDERR "$prefix \n" if $Apache::AuthDBI::DEBUG > 1;
913             my (%params) = @_;
914             my ($md5);
915              
916             $params{'format'} ||= "hex";
917              
918             $md5 = Digest::MD5->new;
919              
920             if ($params{'text'} ne '') {
921             $md5->add($params{'text'});
922             } else {
923             return -1;
924             }
925              
926             if ($params{'format'} =~ /base64/i) {
927             return $md5->b64digest;
928             } elsif ($params{'format'} =~ /hex/i) {
929             return $md5->hexdigest;
930             } elsif ($params{'format'} =~ /binary/i) {
931             return $md5->digest;
932             }
933              
934             return -1;
935             }
936              
937             1;
938              
939             __END__