File Coverage

blib/lib/WWW/Suffit/AuthDB/Role/AAA.pm
Criterion Covered Total %
statement 89 166 53.6
branch 32 120 26.6
condition 28 117 23.9
subroutine 12 14 85.7
pod 4 4 100.0
total 165 421 39.1


line stmt bran cond sub pod time code
1             package WWW::Suffit::AuthDB::Role::AAA;
2 1     1   293630 use strict;
  1         3  
  1         77  
3 1     1   8 use utf8;
  1         3  
  1         8  
4              
5             =encoding utf8
6              
7             =head1 NAME
8              
9             WWW::Suffit::AuthDB::Role::AAA - Suffit AuthDB methods for AAA
10              
11             =head1 SYNOPSIS
12              
13             use WWW::Suffit::AuthDB;
14              
15             my $authdb = WWW::Suffit::AuthDB->with_roles('+AAA')->new( ... );
16              
17             =head1 DESCRIPTION
18              
19             The API provided by this module deals with access, authentication and authorization phases
20              
21             =head1 METHODS
22              
23             This class extends L and implements the following new ones methods
24              
25             =head2 access
26              
27             $authdb->access(
28             controller => $self, # The Mojo controller object
29             username => "Bob",
30             cachekey => "d1b919c1",
31             base => "https://www.example.com",
32             method => "GET",
33             url => "https://www.example.com/foo/bar",
34             path => "/foo/bar",
35             remote_ip => "127.0.0.1",
36             routename => "root",
37             headers => {
38             Accept => "text/html,text/plain",
39             Connection => "keep-alive",
40             Host => "localhost:8695",
41             },
42             ) or die $authdb->error;
43              
44             ...or short syntax:
45              
46             $authdb->access(
47             c => $self, # The Mojo controller object
48             u => "Bob",
49             k => "d1b919c1",
50             b => "https://www.example.com",
51             m => "GET",
52             url => "https://www.example.com/foo/bar",
53             p => "/foo/bar",
54             i => "127.0.0.1",
55             r => "root",
56             h => {
57             Accept => "text/html,text/plain",
58             Connection => "keep-alive",
59             Host => "localhost:8695",
60             },
61             ) or die $authdb->error;
62              
63             This method performs access control
64              
65             Check by routename:
66              
67             <% if (has_access(path => url_for('settings')->to_string)) { %> ... <% } %>
68             <% if (has_access(route => 'settings') { %> ... <% } %>
69              
70             =head2 authen
71              
72             This method is deprecated! See L
73              
74             =head2 authn
75              
76             $authdb->authn(
77             username => "username",
78             password => "password",
79             address => "127.0.0.1",
80             cachekey => "d1b919c1",
81             ) or die $authdb->error;
82              
83             ...or short syntax:
84              
85             $authdb->authn(
86             u => "username",
87             p => "password",
88             a => "127.0.0.1",
89             k => "d1b919c1",
90             ) or die $authdb->error;
91              
92             This method checks password by specified credential pair (username and password) and remote client IP address.
93              
94             The method returns the User object or undef if errors occurred
95              
96             =head2 authz
97              
98             $authdb->authz(
99             username => "username",
100             scope => 0, # 0 - internal; 1 - external
101             cachekey => "d1b919c1",
102             ) or die $authdb->error;
103              
104             ...or short syntax:
105              
106             $authdb->authz(
107             u => "username",
108             s => 0, # 0 - internal; 1 - external
109             k => "d1b919c1",
110             ) or die $authdb->error;
111              
112             This method checks authorization status by specified username.
113              
114             The scope argument can be false or true.
115             false - determines the fact that internal authorization is being performed
116             (on Suffit system); true - determines the fact that external
117             authorization is being performed (on another sites)
118              
119             The method returns the User object or undef if errors occurred
120              
121             =head2 ERROR CODES
122              
123             List of error codes describes in L
124              
125             =head1 HISTORY
126              
127             See C file
128              
129             =head1 TO DO
130              
131             See C file
132              
133             =head1 SEE ALSO
134              
135             L, L, L
136              
137             =head1 AUTHOR
138              
139             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
140              
141             =head1 COPYRIGHT
142              
143             Copyright (C) 1998-2026 D&D Corporation
144              
145             =head1 LICENSE
146              
147             This program is distributed under the terms of the Artistic License Version 2.0
148              
149             See the C file or L for details
150              
151             =cut
152              
153 1     1   81 use Mojo::Base -role;
  1         2  
  1         10  
154              
155 1     1   870 use Carp;
  1         4  
  1         106  
156              
157 1     1   2093 use Mojolicious::Routes::Pattern;
  1         4676  
  1         9  
158              
159 1     1   57 use Mojo::URL;
  1         3  
  1         43  
160 1     1   54 use Mojo::Util qw/secure_compare deprecated/;
  1         4  
  1         93  
161              
162 1     1   7 use Acrux::RefUtil qw/isnt_void is_integer is_array_ref is_hash_ref is_true_flag/;
  1         2  
  1         95  
163              
164             use constant {
165 1         3645 MAX_DISMISS => 5,
166             AUTH_HOLD_TIME => 60*5, # 5min
167 1     1   6 };
  1         3  
168              
169             # Interface methods
170             sub authn {
171 3     3 1 3964 my $self = shift;
172 3 50       28 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 50       0  
173 3   33     45 my $username = $args->{username} // $args->{u} // ''; # Username
      50        
174 3   33     21 my $password = $args->{password} // $args->{p} // ''; # Password
      50        
175 3   66     26 my $address = $args->{address} // $args->{a} // ''; # IP address
      100        
176 3   33     22 my $cachekey = $args->{cachekey} // $args->{k} // ''; # Cachekey
      50        
177 3         48 $self->clean; # Flush session vars
178 3         13 my $model = $self->model;
179              
180             # Validation username
181 3 50       23 return $self->raise(400 => "E1320: No username specified") unless length($username); # HTTP_BAD_REQUEST
182 3 50       13 return $self->raise(413 => "E1321: The username is too long (1-256 chars required)")
183             unless length($username) <= 256; # HTTP_REQUEST_ENTITY_TOO_LARGE
184              
185             # Validation password
186 3 50       7 return $self->raise(400 => "E1322: No password specified") unless length($password); # HTTP_BAD_REQUEST
187 3 50       11 return $self->raise(413 => "E1323: The password is too long (1-256 chars required)")
188             unless length($password) <= 256; # HTTP_REQUEST_ENTITY_TOO_LARGE
189              
190             # Get user data from AuthDB
191 3         19 my $user = $self->user($username, $cachekey);
192 3 50       86 return if $self->error;
193              
194             # Check consistency
195 3 100       37 return $self->raise(401 => $user->error) unless $user->is_valid; # HTTP_UNAUTHORIZED
196              
197             # Get dismiss and updated by address and username
198 2         15 my %st = $model->stat_get($address, $username);
199 2 100       35 return $self->raise(500 => "E1384: %s", $model->error) if $model->error; # HTTP_INTERNAL_SERVER_ERROR
200 1   50     15 my $dismiss = $st{dismiss} || 0;
201 1   50     6 my $updated = $st{updated} || 0;
202 1 50 33     6 if (($dismiss >= MAX_DISMISS) && (($updated + AUTH_HOLD_TIME) >= time)) {
203 0         0 return $self->raise(403 => "E1324: Account frozen for 5 min"); # HTTP_FORBIDDEN
204             }
205              
206             # Check password checksum
207 1         8 my $digest = $self->checksum($password, $user->algorithm);
208 1 50       4 return $self->raise(501 => "E1325: Incorrect digest algorithm") unless $digest; # HTTP_NOT_IMPLEMENTED
209              
210             # Compare password
211 1 50       6 if (secure_compare($user->password, $digest)) { # Ok
212 1 50       88 unless ($model->stat_set(address => $address, username => $username)) {
213 0   0     0 return $self->raise(500 => "E1385: %s", $model->error || 'Database request error (stat_set)'); # HTTP_INTERNAL_SERVER_ERROR
214             }
215 1         76 return $user;
216             }
217              
218             # Oops!
219 0 0       0 unless ($model->stat_set(address => $address, username => $username, dismiss => ($dismiss + 1))) {
220 0   0     0 return $self->raise(500 => "E1385: %s", $model->error || 'Database request error (stat_set)'); # HTTP_INTERNAL_SERVER_ERROR
221             }
222              
223             # Fail
224 0         0 return $self->raise(401 => "E1326: Incorrect username or password"); # HTTP_UNAUTHORIZED
225             }
226             sub authz {
227 2     2 1 2880 my $self = shift;
228 2 50       20 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 50       0  
229 2   33     16 my $username = $args->{username} // $args->{u} // ''; # Username
      50        
230 2   33     39 my $cachekey = $args->{cachekey} // $args->{k} // ''; # Cachekey
      50        
231 2   50     40 my $scope = $args->{scope} || $args->{s} || 0; # Scope
232 2         17 $self->clean; # Flush session vars
233              
234             # Validation username
235 2 50       6 return $self->raise(400 => "E1320: No username specified") unless length($username); # HTTP_BAD_REQUEST
236 2 50       9 return $self->raise(413 => "E1321: The username is too long (1-256 chars required)")
237             unless length($username) <= 256; # HTTP_REQUEST_ENTITY_TOO_LARGE
238              
239             # Get user data from AuthDB
240 2         9 my $user = $self->user($username, $cachekey);
241 2 50       42 return if $self->error;
242              
243             # Check consistency
244 2 100       22 return $self->raise(401 => $user->error) unless $user->is_valid; # HTTP_UNAUTHORIZED
245              
246             # Disabled/Banned
247 1 50       8 return $self->raise(403 => "E1327: User is disabled") unless $user->is_enabled; # HTTP_FORBIDDEN
248              
249             # Internal or External
250 1 50       5 if ($scope) { # External
251 0 0       0 return $self->raise(403 => "E1317: External requests is blocked") unless $user->allow_ext; # HTTP_FORBIDDEN
252             } else { # Internal (default)
253 1 50       5 return $self->raise(403 => "E1318: Internal requests is blocked") unless $user->allow_int; # HTTP_FORBIDDEN
254             }
255              
256             # Ok
257 1         5 $user->is_authorized(1); # Set flag 'is_authorized'
258 1         11 return $user;
259             }
260             sub access {
261 1     1 1 1695 my $self = shift;
262 1 50       16 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 50       0  
263 1         8 $self->clean; # Flush session vars
264              
265             # Parse arguments
266 1   33     12 my $cachekey = $args->{cachekey} // $args->{k} // ''; # Cachekey
      50        
267 1   33     5 my $controller = $args->{controller} // $args->{c}; # Controller
268 1 50       5 croak "No controller specified" unless ref($controller);
269 1 50       18 my $url = $args->{url} ? Mojo::URL->new($args->{url}) : $controller->req->url; # URL
270 1   33     150 my $username = $args->{username} // $args->{u} // $url->to_abs->username // ''; # Username
      0        
      0        
271 1   33     5 my $routename = $args->{routename} // $args->{r} // $controller->current_route // ''; # Route
      0        
      0        
272 1   33     6 my $method = $args->{method} // $args->{m} // $controller->req->method // '';
      0        
      0        
273 1   33     4 my $url_path = $args->{path} // $args->{p} // $url->path->to_string;
      0        
274 1   33     5 my $url_base = $args->{base} // $args->{b} // $url->base->path_query('/')->to_string // '';
      0        
      0        
275 1         6 $url_base =~ s/\/+$//;
276 1   33     5 my $remote_ip = $args->{remote_ip} // $args->{client_ip} // $args->{i} // $controller->remote_ip;
      0        
      0        
277 1   33     8 my $headers = $args->{headers} // $args->{h};
278             #$controller->log->warn($url_base);
279              
280             # Get routes list for $url_base
281 1         15 my $routes = $self->routes($url_base, $cachekey);
282 1 50       7 return if $self->error;
283              
284             # Route based checks
285 1         9 my %route = ();
286 1 50       4 if (exists($routes->{$routename})) { # By routename
287 0         0 my $r = $routes->{$routename};
288 0         0 %route = (%$r, rule => "by routename directly");
289             } else { # By method and path
290 1         5 foreach my $r (values %$routes) {
291 0         0 my $m = $r->{method};
292 0 0 0     0 next unless $m && (($m eq $method) || ($m eq 'ANY') || ($m eq '*'));
      0        
293 0         0 my $p = $r->{path};
294 0 0       0 next unless $p;
295              
296             # Search directly (eq)
297 0 0       0 if ($p eq $url_path) {
298 0         0 %route = (%$r, rule => "by method and path ($m $p)");
299 0         0 last;
300             }
301              
302             # Search by wildcard (*)
303 0 0       0 if ($p =~ s/\*+$//) {
304 0 0       0 if (index($url_path, $p) >= 0) {
305 0         0 %route = (%$r, rule => "by method and part of path ($m $p)");
306 0         0 last;
307             } else {
308 0         0 next;
309             }
310             }
311              
312             # Match routes (:foo)
313 0         0 for (qw/foo bar baz quz quux corge grault garply waldo fred plugh xyzzy thud/) {
314 0 0       0 $p =~ s/[~]+/(":$_")/e or last
  0         0  
315             }
316 0 0       0 if (defined(Mojolicious::Routes::Pattern->new($p)->match($url_path))) {
317 0         0 %route = (%$r, rule => "by method and pattern of path ($m $p)");
318 0         0 last;
319             }
320             }
321             }
322 1 50       15 return 1 unless $route{realmname};
323 0   0       $controller->log->debug(sprintf("[access] The route \"%s\" was detected %s", $route{routename} // '', $route{rule}));
324              
325             # Get realm instance
326 0           my $realm = $self->realm($route{realmname}, $cachekey);
327 0 0         return if $self->error;
328 0 0         return 1 unless $realm->id; # No realm - no authorization :-)
329 0           $controller->log->debug(sprintf("[access] Use realm \"%s\"", $route{realmname}));
330              
331             # Get user data
332 0           my $user = $self->user($username, $cachekey);
333 0 0         return if $self->error;
334             #$controller->log->debug($controller->dumper($user));
335              
336             # Result of checks
337 0           my @checks = ();
338              
339             # Check by user or group
340 0           my @grants = ();
341             #$controller->log->debug(">>>> Username = $username");
342             #$controller->log->debug(sprintf(">>>> cachekey=$cachekey; Groups=%s", $controller->dumper($user->groups)));
343             #$controller->log->debug($controller->dumper($realm->requirements->{'User/Group'}));
344             #$self->requirements->{'User/Group'}
345 0 0         if (my $s = $realm->_check_by_usergroup($username, $user->groups)) {
346 0 0         if ($s == 1) {
347 0           push @checks, 1;
348 0           push @grants, sprintf("User/Group (username=%s)", $username);
349             }
350             } else {
351 0           push @checks, 0;
352             }
353              
354             # Check by ip or host
355 0 0         if (my $s = $realm->_check_by_host($remote_ip)) {
356 0 0         if ($s == 1) {
357 0           push @checks, 1;
358 0           push @grants, sprintf("Host (ip=%s)", $remote_ip);
359             }
360             } else {
361 0           push @checks, 0;
362             }
363              
364             # Check by ENV
365 0 0         if (my $s = $realm->_check_by_env()) {
366 0 0         if ($s == 1) {
367 0           push @checks, 1;
368 0           push @grants, "Env";
369             }
370             } else {
371 0           push @checks, 0;
372             }
373              
374             # Check by Header
375 0 0         if (my $s = $realm->_check_by_header(sub {
376 0     0     my $_k = $_[0];
377 0 0 0       return $headers->{$_k} if defined($headers) && is_hash_ref($headers);
378 0           return $controller->req->headers->header($_k)
379             })) {
380 0 0         if ($s == 1) {
381 0           push @checks, 1;
382 0           push @grants, "Header";
383             }
384             } else {
385 0           push @checks, 0;
386             }
387              
388             # Check default
389 0           my $default = $realm->_check_by_default();
390              
391             # Calc check result by satisfy politic
392 0           my $status = 0; # False by default
393 0           my $sum = 0;
394 0           $sum += $_ for @checks;
395 0 0 0       my $satisfy_all = lc($realm->satisfy || "any") eq 'all' ? 1 : 0; # All -- true / Any -- 0 (default)
396 0 0         if ($satisfy_all) { # All
397 0 0 0       $status = 1 if ($sum > 0) && scalar(@checks) == $sum; # All tests is passed
398             } else { # Any
399 0 0         $status = 1 if $sum > 0; # One or more tests is passed
400             }
401              
402             # Debug
403 0 0         if ($status) {
404 0 0         $controller->log->debug(sprintf('[access] Access allowed by %s rule(s). Satisfy=%s',
405             join(", ", @grants), $satisfy_all ? 'All' : 'Any'));
406             } else {
407 0 0         $controller->log->debug(sprintf('[access] Access %s by default. Satisfy=%s',
    0          
408             $default ? 'allowed' : 'denied', $satisfy_all ? 'All' : 'Any'));
409             }
410              
411             # Summary
412 0 0         my $summary = $status ? 1 : $default; # True - allowed; False - denied
413              
414             # Access denied
415 0 0         return $self->raise(403 => "E1319: Access denied") unless $summary; # HTTP_FORBIDDEN
416              
417             # Ok
418 0           return 1;
419             }
420              
421             # Deprecated methods
422             sub authen {
423 0     0 1   deprecated 'The "WWW::Suffit::AuthDB::authen" is deprecated in favor of "authn"';
424 0           goto &authn;
425             }
426              
427             1;
428              
429             __END__