File Coverage

blib/lib/MPMinusX/AuthSsn.pm
Criterion Covered Total %
statement 132 148 89.1
branch 28 50 56.0
condition 30 66 45.4
subroutine 23 26 88.4
pod 14 15 93.3
total 227 305 74.4


line stmt bran cond sub pod time code
1             package MPMinusX::AuthSsn; # $Id: AuthSsn.pm 5 2019-05-28 10:59:30Z minus $
2 4     4   2259 use strict;
  4         9  
  4         119  
3 4     4   2385 use utf8;
  4         60  
  4         21  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             MPMinusX::AuthSsn - MPMinus AAA via Apache::Session and DBD::SQLite
10              
11             =head1 VERSION
12              
13             Version 1.01
14              
15             =head1 SYNOPSIS
16              
17             use MPMinusX::AuthSsn;
18              
19             # AuthSsn session
20             my $ssn;
21              
22             ... see description ...
23              
24             sub hCleanup {
25             ...
26             undef $ssn;
27             ...
28             }
29              
30             =head1 ABSTRACT
31              
32             MPMinusX::AuthSsn - MPMinus AAA via Apache::Session and DBD::SQLite
33              
34             =head1 DESCRIPTION
35              
36             Methods of using
37              
38             =head2 METHOD #1. MPMINUS HANDLERS LEVEL (RECOMENDED)
39              
40             sub hInit {
41             ...
42             my $usid = $usr{usid} || $q->cookie('usid') || '';
43             $ssn = new MPMinusX::AuthSsn( $m, $usid );
44             ...
45             }
46             sub hResponse {
47             ...
48             my $access = $ssn->access( sub {
49             my $self = shift;
50             return $self->status(0, 'FORBIDDEN') if $self->get('login') eq 'admin';
51             } );
52             if ($access) {
53             # Auhorized!
54             $h{login} = $ssn->get('login');
55             }
56             $template->cast_if("authorized", $access);
57             ....
58             }
59              
60             =head2 METHOD #2. MPMINUS TRANSACTION LEVEL
61              
62             sub default_access {
63             my $usid = $usr{usid} || $q->cookie('usid') || '';
64             $ssn = new MPMinusX::AuthSsn( $m, $usid );
65             return $ssn->access();
66             }
67             sub default_deny {
68             my $m = shift;
69             my $r = $m->r;
70             $r->headers_out->set(Location => "/auth.mpm");
71             return Apache2::Const::REDIRECT;
72             }
73             sub default_form {
74             ...
75             $h{login} = $ssn->get('login');
76             ...
77             }
78              
79             =head1 METHODS
80              
81             =over 8
82              
83             =item B
84              
85             my $authssn = new MPMinusX::AuthSsn( $m, $sid, $expires );
86              
87             Returns object
88              
89             =item B
90              
91             $ssn->authen( $callback, ...arguments... );
92              
93             AAA Authentication.
94              
95             The method returns status operation: 1 - successfully; 0 - not successfully
96              
97             =item B
98              
99             $ssn->authz( $callback, ...arguments... );
100              
101             AAA Authorization.
102              
103             The method returns status operation: 1 - successfully; 0 - not successfully
104              
105             =item B
106              
107             $ssn->access( $callback, ...arguments... );
108              
109             AAA Accounting (AAA Access).
110              
111             The method returns status operation: 1 - successfully; 0 - not successfully
112              
113             =item B
114              
115             $ssn->get( $key );
116              
117             Returns session value by $key
118              
119             =item B
120              
121             $ssn->set( $key, $value );
122              
123             Sets session value by $key
124              
125             =item B
126              
127             $ssn->delete();
128              
129             Delete the session
130              
131             =item B
132              
133             $ssn->sid();
134              
135             Returns current usid value
136              
137             =item B
138              
139             $ssn->expires();
140              
141             Returns current expires value
142              
143             =item B
144              
145             $ssn->status();
146             $ssn->status( $newstatus, $reason );
147              
148             Returns status of a previously executed operation. If you specify $reason, there will push installation $newstatus
149              
150             =item B
151              
152             $ssn->reason();
153              
154             Returns reason of a previously executed operation.
155              
156             Now supported following values: DEFAULT, OK, UNAUTHORIZED, ERROR, SERVER_ERROR, NEW, TIMEOUT, LOGIN_INCORRECT,
157             PASSWORD_INCORRECT, DECLINED, AUTH_REQUIRED, FORBIDDEN.
158              
159             For translating this values to regular form please use method reason_translate like that
160              
161             =item B
162              
163             $ssn->init( $usid, $needcreate );
164              
165             Internal method. Please do not use it
166              
167             Method returns status operation: 1 - successfully; 0 - not successfully
168              
169             =item B
170              
171             $ssn->toexpire( $time );
172              
173             Returns expiration interval relative to ctime() form.
174              
175             If used with no arguments, returns the expiration interval if it was ever set.
176             If no expiration was ever set, returns undef.
177              
178             All the time values should be given in the form of seconds.
179             Following keywords are also supported for your convenience:
180              
181             +-----------+---------------+
182             | alias | meaning |
183             +-----------+---------------+
184             | s | Second |
185             | m | Minute |
186             | h | Hour |
187             | d | Day |
188             | w | Week |
189             | M | Month |
190             | y | Year |
191             +-----------+---------------+
192              
193             Examples:
194              
195             $ssn->toexpire("2h"); # expires in two hours
196             $ssn->toexpire(3600); # expires in one hour
197              
198             Note: all the expiration times are relative to session's last access time, not to its creation time.
199             To expire a session immediately, call delete() method.
200              
201             =back
202              
203             =head1 CONFIGURATION
204              
205             Sample in file conf/auth.conf:
206              
207            
208             expires +3m
209             #sidkey usid
210             #tplkey authorized
211             #tplpfx auth
212             #file /document_root/session.db
213             #dsn dbi:SQLite:dbname=/document_root/session.db
214            
215              
216             =head1 HISTORY
217              
218             See C file
219              
220             =head1 DEPENDENCIES
221              
222             L
223              
224             =head1 TO DO
225              
226             See C file
227              
228             =head1 BUGS
229              
230             * none noted
231              
232             =head1 SEE ALSO
233              
234             L, L, L, L
235              
236             =head1 AUTHOR
237              
238             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
239              
240             =head1 COPYRIGHT
241              
242             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
243              
244             =head1 LICENSE
245              
246             This program is free software; you can redistribute it and/or
247             modify it under the same terms as Perl itself.
248              
249             See C file and L
250              
251             =cut
252              
253              
254 4     4   360 use vars qw/ $VERSION /;
  4         13  
  4         235  
255             $VERSION = '1.01';
256              
257 4     4   27 use Carp;
  4         9  
  4         226  
258 4     4   1879 use Apache::Session::Flex;
  4         10953  
  4         122  
259 4     4   32 use File::Spec;
  4         7  
  4         100  
260              
261 4     4   2597 use CTK::Util qw/ :API :FORMAT :DATE /;
  4         557984  
  4         1404  
262 4     4   1732 use CTK::ConfGenUtil;
  4         8114  
  4         364  
263 4     4   1732 use CTK::DBI;
  4         110605  
  4         356  
264              
265             use constant {
266 4         6653 CONFKEY => 'auth',
267             SIDKEY => 'usid',
268             TPLKEY => 'authorized',
269             TPLPFX => '',
270             SESSION_DIR => 'sessions',
271             SESSION_FILE => 'sessions.db',
272             SESSION_DSN_MSK => 'dbi:SQLite:dbname=%s',
273             SESSION_EXPIRES => '+1h', # 3600sec as default
274              
275             # Statuses translating map
276             STAT => {
277             DEFAULT => "Status not defined",
278             OK => "OK",
279             UNAUTHORIZED => "Unauthorized",
280             ERROR => "Session not exists or session creation error has occurred",
281             SERVER_ERROR => "Server error",
282             NEW => "Created",
283             TIMEOUT => "The session has expired",
284             LOGIN_INCORRECT => "Login incorrect",
285             PASSWORD_INCORRECT => "Password incorrect",
286             DECLINED => "Account not found",
287             AUTH_REQUIRED => "Auth required",
288             FORBIDDEN => "Forbidden",
289             },
290 4     4   34 };
  4         9  
291              
292             sub new {
293 3     3 1 64 my $class = shift;
294 3         7 my $m = shift;
295 3   50     21 my $usid = shift || undef; # USID User Session IDentifier
296 3   50     14 my $expires = shift || undef;
297 3 50       19 croak("The method call not in the MPMinus context") unless ref($m) =~ /MPMinus/;
298              
299 3         14 my $authconf = hash($m->conf(CONFKEY));
300 3   50     47 my $s_sidkey = value($authconf, 'sidkey') || SIDKEY;
301 3   50     197 my $s_tplkey = value($authconf, 'tplkey') || TPLKEY;
302 3   50     121 my $s_tplpfx = value($authconf, 'tplpfx') || TPLPFX;
303 3   33     125 my $s_dir = value($authconf, 'dir') || File::Spec->catdir($m->conf('document_root'), SESSION_DIR);
304 3   33     14 my $s_file = value($authconf, 'file') || File::Spec->catfile($s_dir, SESSION_FILE);
305 3   33     123 my $dsn = value($authconf, 'dsn') || sprintf(SESSION_DSN_MSK, $s_file);
306 3   50     132 my $s_expires = value($authconf, 'expires') || SESSION_EXPIRES;
307 3   33     123 $expires ||= $s_expires;
308              
309             # Create
310 3         18 my $self = bless {
311             session => {},
312             transtable => STAT,
313             status => 0, # No tied
314             reason => "UNAUTHORIZED",
315             dir => $s_dir,
316             file => $s_file,
317             expires => $class->toexpire($expires),
318             $s_sidkey => undef,
319             sidkey => $s_sidkey,
320             tplkey => $s_tplkey,
321             tplpfx => $s_tplpfx,
322             dsn => $dsn,
323             prepared => 0,
324             }, $class;
325              
326             # Initialize as "read mode"
327 3 50       11 $self->init($usid, 0) if $usid;
328              
329 3         11 return $self;
330             }
331             sub init {
332 1     1 1 3 my $self = shift;
333 1   50     6 my $usid = shift || undef; # USID User Session IDentifier
334 1   50     4 my $create = shift || 0; # 1 - create session; 0 - read session
335              
336 1         2 my %session;
337 1 50 33     8 my $ssndata = $self->{session} && ref($self->{session}) eq 'HASH' ? $self->{session} : {};
338 1         2 my $reason;
339              
340             # Paths
341 1         2 my $dsn = $self->{dsn};
342 1         5 my $sfile = $self->{file};
343              
344             # Prepare database
345 1 50       5 if ($sfile) { # My custom file
346 1 50 33     45 $self->{prepared} = 1 if (-e $sfile) && (-s _); # file and file is not empty
347             }
348              
349 1 50       4 unless ($self->{prepared}) {
350 1         14 my $sqlc = new CTK::DBI(-dsn => $dsn);
351 1         12390 $sqlc->execute('CREATE TABLE IF NOT EXISTS sessions ( id CHAR(32) NOT NULL PRIMARY KEY, a_session TEXT NOT NULL )');
352 1         13791 $sqlc->disconnect;
353 1         102 $self->{prepared} = 1;
354             }
355              
356             # Apache::Session options
357 1         75 my $opts = {
358             Store => 'MySQL',
359             Lock => 'Null',
360             Generate => 'MD5',
361             Serialize => 'Base64',
362             DataSource => $dsn,
363             };
364              
365             # Tie!
366 1         7 my $retstat = $self->status;
367 1         3 eval { tie %session, 'Apache::Session::Flex', $usid, $opts; };
  1         21  
368 1 50       20634 if ($@) {
369 0         0 $reason = 'ERROR'; # Init error
370             } else {
371 1 50       5 if ($usid) {
372 0         0 $reason = 'OK'; # Not init request
373             } else {
374 1         3 $reason = 'NEW'; # Init request
375             }
376             # USID
377 1         12 $self->{$self->{sidkey}} = $session{_session_id};
378              
379             # Add data to created session
380 1 50       18 if ($create) {
381 1         7 $session{time_create} = time(); # Create time
382 1         16 $session{time_access} = time(); # Access time
383 1         24 $session{expires} = $self->{expires}; # Time for current session
384 1         19 $session{$_} = $ssndata->{$_} foreach (keys %$ssndata);
385             }
386 1         24 $self->{session} = \%session;
387 1         3 $retstat = 1; # Ok
388             }
389              
390 1         4 return $self->status($retstat, $reason);
391             }
392              
393             sub authen { # AAA-authen
394             #
395             # Possible responses:
396             # LOGIN_INCORRECT / PASSWORD_INCORRECT / AUTH_REQUIRED / DECLINED / FORBIDDEN / OK
397             #
398              
399 4     4 1 12 my $self = shift;
400 4         6 my $callback = shift;
401              
402             # !!! callback here !!!
403 4 50 33     21 if ($callback && ref($callback) eq 'CODE') {
404 4 100       10 return 0 unless $callback->($self, @_);
405             }
406              
407 1         11 return $self->status(1, 'OK');
408             }
409             sub authz { # AAA-authz
410             #
411             # Possible responses:
412             # FORBIDDEN / OK
413             #
414              
415 2     2 1 6 my $self = shift;
416 2         3 my $callback = shift;
417              
418             # !!! callback here !!!
419 2 50 33     13 if ($callback && ref($callback) eq 'CODE') {
420 2 100       7 return 0 unless $callback->($self, @_);
421             }
422              
423             # Авторизация прошла успешно. Можно создавать сессию
424 1 50       9 return 0 unless $self->init(undef, 1);
425 1         14 return 1;
426             }
427             sub access { # AAA-access
428 3     3 1 285 my $self = shift;
429 3         7 my $callback = shift;
430 3 100       28 return $self->status(0, $self->reason) unless $self->status;
431              
432             # Проверка expires и обновление данных последнего доступа.
433 2         7 my $expires = $self->expires;
434 2   50     8 my $lastaccess = $self->get('time_access') || 0;
435 2         19 my $newaccess = time();
436 2         5 my $accessto = (($newaccess - $lastaccess) > $expires); # true - timeout
437 2 50       5 if ($accessto) {
438 0         0 $self->delete(); # Expired
439 0         0 return $self->status(0, 'TIMEOUT');
440             }
441              
442             # !!! callback here !!!
443 2 50 33     12 if ($callback && ref($callback) eq 'CODE') {
444 2 100       8 return 0 unless $callback->($self, @_);
445             }
446              
447             # Ok
448 1         17 $self->set('time_access', $newaccess);
449              
450 1         14 return $self->status(1, 'OK'); # Access granted
451             }
452              
453             sub sid {
454 1     1 1 3 my $self = shift;
455 1   50     7 return $self->{$self->{sidkey}} || undef;
456             }
457 0     0 1 0 sub usid { goto &sid }
458             sub expires {
459 2     2 1 5 my $self = shift;
460 2 50       36 my $expires = $self->{session}->{expires} ? $self->{session}->{expires} : $self->{expires};
461 2   50     33 return $expires || 0;
462             }
463             sub status {
464 13     13 1 57 my $self = shift;
465 13   100     62 my $ns = shift || 0;
466 13   100     38 my $nr = shift || '';
467 13 100       31 if ($nr) {
468 9         22 $self->{status} = $ns;
469 9         16 $self->{reason} = $nr;
470             }
471 13         74 return $self->{status};
472             }
473             sub reason {
474 9     9 1 19 my $self = shift;
475 9         56 return $self->{reason};
476             }
477             sub get {
478 6     6 1 21 my $self = shift;
479 6   50     16 my $key = shift || return;
480 6   100     23 return $self->{session}->{$key} || undef;
481             }
482             sub set {
483 4     4 1 19 my $self = shift;
484 4   50     37 my $key = shift || return 0;
485 4         8 my $value = shift;
486 4         14 return $self->{session}->{$key} = $value;
487             }
488             sub delete {
489 0     0 1 0 my $self = shift;
490 0 0       0 tied(%{$self->{session}})->delete() if $self->{status};
  0         0  
491 0         0 return $self->status(0, 'UNAUTHORIZED');
492             }
493             sub reason_translate {
494 0     0 0 0 my $self = shift;
495 0   0     0 my $reason = shift || $self->reason() || 'DEFAULT';
496             return $self->{transtable}->{DEFAULT} unless
497 0 0       0 grep {$_ eq 'DEFAULT'} keys %{$self->{transtable}};
  0         0  
  0         0  
498              
499 0         0 return $self->{transtable}->{$reason};
500             }
501             sub toexpire {
502 3     3 1 8 my $self = shift;
503 3   50     9 my $str = shift || 0;
504              
505 3 50       10 return 0 unless defined $str;
506 3 50       21 return $1 if $str =~ m/^[-+]?(\d+)$/;
507              
508 3         28 my %_map = (
509             s => 1,
510             m => 60,
511             h => 3600,
512             d => 86400,
513             w => 604800,
514             M => 2592000,
515             y => 31536000
516             );
517              
518 3         21 my ($koef, $d) = $str =~ m/^([+-]?\d+)([smhdwMy])$/;
519 3 50 33     19 unless ( defined($koef) && defined($d) ) {
520 0         0 croak "toexpire(): couldn't parse '$str' into \$koef and \$d parts. Possible invalid syntax";
521             }
522 3         52 return $koef * $_map{ $d };
523             }
524             sub DESTROY {
525 3     3   732 my $self = shift;
526 3         266 undef $self;
527             }
528              
529             1;