File Coverage

blib/lib/Web/ACL.pm
Criterion Covered Total %
statement 127 164 77.4
branch 70 102 68.6
condition 16 33 48.4
subroutine 7 7 100.0
pod 2 2 100.0
total 222 308 72.0


line stmt bran cond sub pod time code
1             package Web::ACL;
2              
3 3     3   352555 use 5.006;
  3         8  
4 3     3   13 use strict;
  3         10  
  3         90  
5 3     3   13 use warnings;
  3         4  
  3         154  
6 3     3   13 use base 'Error::Helper';
  3         7  
  3         1555  
7 3     3   5831 use Net::Subnet;
  3         24432  
  3         6607  
8              
9             =head1 NAME
10              
11             Web::ACL - A helper for creating basic apikey/slug/IP based ACLs.
12              
13             =head1 VERSION
14              
15             Version 0.1.0
16              
17             =cut
18              
19             our $VERSION = '0.1.0';
20              
21             =head1 SYNOPSIS
22              
23             use Web::ACL;
24              
25             my $acl = Web::ACL->new(acl=>{
26             fooBar=>{
27             ip_auth => 1,
28             slug_auth => 0,
29             require_ip => 1,
30             require_slug => 0,
31             final => 1,
32             slugs => [],
33             slugs_regex => [],
34             allow_subnets => ['192.168.0.0/16','127.0.0.1/32'],
35             deny_subnets => [],
36             },
37             derp=>{
38             ip_auth => 1,
39             slug_auth => 1,
40             require_ip => 1,
41             require_slug => 0,
42             final => 1,
43             slugs => ['derp'],
44             slugs_regex => [],
45             allow_subnets => ['192.168.0.0/16','127.0.0.1/32'],
46             deny_subnets => ['10.0.10.0/24'],
47             },
48             derpderp=>{
49             ip_auth => 0,
50             slug_auth => 1,
51             require_ip => 0,
52             require_slug => 0,
53             final => 1,
54             slugs => ['derp'],
55             slugs_regex => [],
56             allow_subnets => [],
57             deny_subnets => [],
58             },
59             });
60              
61             my $results=$acl->check(
62             apikey=>'a_test',
63             ip=>'10.1.3.4',
64             slugs=>['test2'],
65             );
66             if ($results) {
67             print "Authed\n";
68             }else{
69             print "Not Authed\n";
70             }
71              
72             my $results=$acl->check(
73             apikey=>'fooBar',
74             ip=>'192.168.1.2',
75             slugs=>['test2'],
76             );
77             if ($results) {
78             print "Authed\n";
79             }else{
80             print "Not Authed\n";
81             }
82              
83             my $results=$acl->check(
84             apikey=>'fooBar',
85             ip=>'192.168.1.2',
86             slugs=>['test2'],
87             );
88             if ($results) {
89             print "Authed\n";
90             }else{
91             print "Not Authed\n";
92             }
93              
94             my $results=$acl->check(
95             apikey=>'derpderp',
96             ip=>'192.168.1.2',
97             slugs=>['derp'],
98             );
99             if ($results) {
100             print "Authed\n";
101             }else{
102             print "Not Authed\n";
103             }
104              
105             my $results=$acl->check(
106             apikey=>'derpderp',
107             ip=>'192.168.1.2',
108             slugs=>['not_derp'],
109             );
110             if ($results) {
111             print "Authed\n";
112             }else{
113             print "Not Authed\n";
114             }
115              
116              
117             =head1 METHODS
118              
119             =head2 new
120              
121             Initiates the object.
122              
123             - acl :: The ACL hash to use.
124             - Default :: {
125             'undef' => {
126             ip_auth => 0,
127             path_auth => 0,
128             slug_auth => 0,
129             ua_auth => 0,
130             require_ip => 0,
131             require_slug => 0,
132             final => 0,
133             slugs => [],
134             slugs_regex => [],
135             allow_subnets => [],
136             deny_subnets => [],
137             ua_regex_allow => [],
138             ua_regex_deny => [],
139             paths_regex_allow => [],
140             paths_regex_deny => [],
141             },
142             'nonexistent' => {
143             ip_auth => 0,
144             path_auth => 0,
145             slug_auth => 0,
146             ua_auth => 0,
147             require_ip => 0,
148             require_slug => 0,
149             final => 0,
150             slugs => [],
151             slugs_regex => [],
152             allow_subnets => [],
153             deny_subnets => [],
154             ua_regex_allow => [],
155             ua_regex_deny => [],
156             paths_regex_allow => [],
157             paths_regex_deny => [],
158             },
159             }
160              
161             =cut
162              
163             sub new {
164 22     22 1 419645 my ( $blank, %opts ) = @_;
165              
166 22         417 my $self = {
167             perror => undef,
168             error => undef,
169             errorLine => undef,
170             errorFilename => undef,
171             errorString => "",
172             errorExtra => {
173             all_errors_fatal => 1,
174             flags => {
175             1 => 'ACLnotHash',
176             2 => 'ACLitemNotArray',
177             3 => 'subnetError',
178             4 => 'ACLnotString',
179             },
180             fatal_flags => {},
181             perror_not_fatal => 0,
182             },
183             acl => {
184             'undef' => {
185             ip_auth => 0,
186             slug_auth => 0,
187             ua_auth => 0,
188             path_auth => 0,
189             require_ip => 0,
190             require_slug => 0,
191             require_ua => 0,
192             require_path => 0,
193             final => 0,
194             slugs => [],
195             slugs_regex => [],
196             allow_subnets => [],
197             deny_subnets => [],
198             ua_regex_allow => [],
199             ua_regex_deny => [],
200             paths_regex_allow => [],
201             paths_regex_deny => [],
202             },
203             'nonexistent' => {
204             ip_auth => 0,
205             slug_auth => 0,
206             ua_auth => 0,
207             path_auth => 0,
208             require_ip => 0,
209             require_slug => 0,
210             require_ua => 0,
211             require_path => 0,
212             final => 0,
213             slugs => [],
214             slugs_regex => [],
215             allow_subnets => [],
216             deny_subnets => [],
217             ua_regex_allow => [],
218             ua_regex_deny => [],
219             paths_regex_allow => [],
220             paths_regex_deny => [],
221             },
222             },
223             };
224 22         60 bless $self;
225              
226 22 100 66     204 if ( defined( $opts{acl} ) && ref( $opts{acl} ) eq 'HASH' ) {
    50 33        
227              
228 21         35 my @acl_keys = keys( %{ $opts{acl} } );
  21         67  
229 21         44 foreach my $acl (@acl_keys) {
230             # check boolean items and define if undef
231 26         83 my @keys_that_are_boolean = (
232             'ip_auth', 'require_ip', 'slug_auth', 'require_slug', 'path_auth', 'require_path',
233             'ua_auth', 'require_ua', 'final',
234             );
235 26         64 foreach my $boolean_key (@keys_that_are_boolean) {
236 190 100       413 if ( !defined( $opts{acl}{$acl}{$boolean_key} ) ) {
237 142         342 $opts{acl}{$acl}{$boolean_key} = 0;
238             } else {
239 47 100       158 if ( ref( $opts{acl}{$acl}{$boolean_key} ) ne '' ) {
240 9         17 $self->{perror} = 1;
241 9         14 $self->{error} = 4;
242             $self->{errorString}
243             = '$opts{acl}{acl}{'
244             . $boolean_key
245             . '} is not ref "", but "'
246 9         33 . ref( $opts{acl}{$acl}{$boolean_key} ) . '"';
247 9         28 $self->warn;
248 0         0 return;
249             } ## end if ( ref( $opts{acl}{$acl}{$boolean_key} )...)
250             } ## end else [ if ( !defined( $opts{acl}{$acl}{$boolean_key...}))]
251             } ## end foreach my $boolean_key (@keys_that_are_boolean)
252              
253 16 50 0     67 if ( !defined( $opts{acl}{$acl}{final} ) && ( $acl eq 'undef' || $acl eq 'nonexistent' ) ) {
    50 33        
254 0         0 $opts{acl}{$acl}{final} = 0;
255             } elsif ( !defined( $opts{acl}{$acl}{final} ) ) {
256 0         0 $opts{acl}{$acl}{final} = 1;
257             }
258              
259             # check array items and error if they are not a array
260             # if undef, create a empty array
261 16         57 my @keys_that_are_arrays = (
262             'slugs', 'ua_regex_allow', 'ua_regex_deny', 'paths_regex_allow',
263             'paths_regex_deny', 'slugs_regex', 'allow_subnets', 'deny_subnets',
264             );
265 16         31 foreach my $array_key (@keys_that_are_arrays) {
266 100 100       270 if ( !defined( $opts{acl}{$acl}{$array_key} ) ) {
    100          
267 62         154 $opts{acl}{$acl}{$array_key} = [];
268             } elsif ( ref( $opts{acl}{$acl}{$array_key} ) ne 'ARRAY' ) {
269 8         17 $self->{perror} = 1;
270 8         12 $self->{error} = 2;
271             $self->{errorString}
272             = '$opts{acl}{acl}{'
273             . $array_key
274             . '} is not ref ARRAY, but "'
275 8         22 . ref( $opts{acl}{$acl}{$array_key} ) . '"';
276 8         43 $self->warn;
277 0         0 return;
278             } ## end elsif ( ref( $opts{acl}{$acl}{$array_key} ) ne...)
279             } ## end foreach my $array_key (@keys_that_are_arrays)
280             } ## end foreach my $acl (@acl_keys)
281              
282 3 50       16 if ( !defined( $opts{acl}{'undef'} ) ) {
283 3         35 $opts{acl}{'undef'} = {
284             ip_auth => 0,
285             slug_auth => 0,
286             ua_auth => 0,
287             path_auth => 0,
288             require_ip => 0,
289             require_slug => 0,
290             require_ua => 0,
291             require_path => 0,
292             final => 0,
293             slugs => [],
294             slugs_regex => [],
295             allow_subnets => [],
296             deny_subnets => [],
297             ua_regex_allow => [],
298             ua_regex_deny => [],
299             paths_regex_allow => [],
300             paths_regex_deny => [],
301             };
302             } ## end if ( !defined( $opts{acl}{'undef'} ) )
303              
304 3 50       11 if ( !defined( $opts{acl}{'nonexistent'} ) ) {
305 3         21 $opts{acl}{'nonexistent'} = {
306             ip_auth => 0,
307             slug_auth => 0,
308             ua_auth => 0,
309             path_auth => 0,
310             require_ip => 0,
311             require_slug => 0,
312             require_ua => 0,
313             require_path => 0,
314             final => 0,
315             slugs => [],
316             slugs_regex => [],
317             allow_subnets => [],
318             deny_subnets => [],
319             ua_regex_allow => [],
320             ua_regex_deny => [],
321             paths_regex_allow => [],
322             paths_regex_deny => [],
323             };
324             } ## end if ( !defined( $opts{acl}{'nonexistent'} ))
325              
326 3         43 $self->{acl} = $opts{acl};
327             } elsif ( defined( $opts{acl} ) && ref( $opts{acl} ) ne 'HASH' ) {
328 1         3 $self->{perror} = 1;
329 1         3 $self->{error} = 1;
330 1         7 $self->{errorString} = '$opts{acl} is not ref HASH, but "' . ref( $opts{acl} ) . '"';
331 1         12 $self->warn;
332 0         0 return;
333             }
334              
335 3         33 return $self;
336             } ## end sub new
337              
338             =head2 check
339              
340             - apikey :: The API key to check for. If not specified it is set to 'undef'
341             and if none match, the it is set to 'nonexistent'.
342             - Default :: 'undef'
343              
344             - slugs :: An array of slugs to check again. All must match. If undef or none
345             are specified, a value of 'undef' is added.
346             - Default :: ['undef']
347              
348             - ip :: An IP to check for.
349             - Default :: undef
350              
351             =cut
352              
353             sub check {
354 16     16 1 133 my ( $self, %opts ) = @_;
355              
356             # set what API key we should check against
357             # undef and nonexistent are two special ones set based on apikey being undef or not being a defined one
358 16 100       74 if ( !defined( $opts{apikey} ) ) {
    100          
359 1         3 $opts{apikey} = 'undef';
360             } elsif ( !defined( $self->{acl}{ $opts{apikey} } ) ) {
361 1         2 $opts{apikey} = 'nonexistent';
362             }
363              
364             # get the slugs
365 16         29 my @slugs;
366 16 100       34 if ( !defined( $opts{slugs} ) ) {
367             # no point in continuing if we require a slug and
368 7 50       18 if ( $self->{acl}{ $opts{apikey} }{require_slug} ) {
369 0         0 return 0;
370             }
371             } else {
372 9 50       22 if ( ref( $opts{slugs} ) eq 'ARRAY' ) {
    0          
373 9         15 push( @slugs, @{ $opts{slugs} } );
  9         20  
374             } elsif ( ref( $opts{slugs} ) eq '' ) {
375 0         0 push( @slugs, $opts{slugs} );
376             }
377              
378 9 50 33     36 if ( $self->{acl}{ $opts{apikey} }{require_slug} && !defined( $slugs[0] ) ) {
    50          
379 0         0 return 0;
380             } elsif ( !defined( $slugs[0] ) ) {
381 0         0 push( @slugs, 'undef' );
382             }
383             } ## end else [ if ( !defined( $opts{slugs} ) ) ]
384              
385             # ensure we have a UA if required
386 16         24 my $ua;
387 16 100       32 if ( !defined( $opts{ua} ) ) {
388 13 100       32 if ( $self->{acl}{ $opts{apikey} }{require_ua} ) {
389 1         4 return 0;
390             }
391             } else {
392 3         6 $ua = $opts{ua};
393             }
394              
395             # ensure we have a path if required
396 15         22 my $path;
397 15 100       43 if ( !defined( $opts{path} ) ) {
398 12 50       31 if ( $self->{acl}{ $opts{apikey} }{require_path} ) {
399 0         0 return 0;
400             }
401             } else {
402 3         5 $path = $opts{path};
403             }
404              
405             # ensure we have a IP if required
406 15         23 my $ip;
407 15 100       26 if ( !defined( $opts{ip} ) ) {
408 6 50       15 if ( $self->{acl}{ $opts{apikey} }{require_ip} ) {
409 0         0 return 0;
410             }
411             } else {
412 9         15 $ip = $opts{ip};
413             }
414              
415             # process IP
416 15 100 100     75 if ( defined($ip) && $self->{acl}{ $opts{apikey} }{ip_auth} ) {
    50 66        
417 5         7 my $denied_subnets;
418 5         11 eval { $denied_subnets = subnet_matcher( @{ $self->{acl}{ $opts{apikey} }{deny_subnets} } ); };
  5         37  
  5         22  
419 5 50       780 if ($@) {
420 0         0 $self->{error} = 3;
421             $self->{errorString}
422 0         0 = 'creating subnet_matcher for deny_subnets for apikey "' . $opts{apikey} . '" failed... ' . $@;
423 0         0 $self->warn;
424 0         0 return 0;
425             }
426 5 50       12 if ( $denied_subnets->($ip) ) {
427 0         0 return 0;
428             }
429              
430 5         38 my $allowed_subnets;
431 5         7 eval { $allowed_subnets = subnet_matcher( @{ $self->{acl}{ $opts{apikey} }{allow_subnets} } ); };
  5         9  
  5         22  
432 5 50       301 if ($@) {
433 0         0 $self->{error} = 3;
434             $self->{errorString}
435 0         0 = 'creating subnet_matcher for allow_subnets for apikey "' . $opts{apikey} . '" failed... ' . $@;
436 0         0 $self->warn;
437 0         0 return 0;
438             }
439 5 100       17 if ( !$allowed_subnets->($ip) ) {
440 2         49 return 0;
441             }
442             } elsif ( !defined($ip) && $self->{acl}{ $opts{apikey} }{ip_auth} ) {
443 0         0 return 0;
444             }
445              
446             # process slugs
447 13 100       87 if ( $self->{acl}{ $opts{apikey} }{slug_auth} ) {
448 4         7 my %matched_slugs;
449              
450             # look for matching slugs
451 4         9 foreach my $slug (@slugs) {
452 4         5 foreach my $item ( @{ $self->{acl}{ $opts{apikey} }{slugs} } ) {
  4         12  
453 4 100       20 if ( $item eq $slug ) {
454 2         6 $matched_slugs{$slug} = 1;
455             }
456             }
457              
458             # only need to check regex if the previous did not match
459 4 100       12 if ( !$matched_slugs{$slug} ) {
460 2         3 foreach my $item ( @{ $self->{acl}{ $opts{apikey} }{slugs_regex} } ) {
  2         7  
461 0 0       0 if ( $slug =~ /$item/ ) {
462 0         0 $matched_slugs{$slug} = 1;
463             }
464             }
465             }
466             } ## end foreach my $slug (@slugs)
467              
468             # check for any slugs not matched
469 4         9 foreach my $slug (@slugs) {
470 4 100       12 if ( !$matched_slugs{$slug} ) {
471 2         10 return 0;
472             }
473             }
474             } ## end if ( $self->{acl}{ $opts{apikey} }{slug_auth...})
475              
476             # process useragent info
477 11 100 66     64 if ( defined($ua) && $self->{acl}{ $opts{apikey} }{ua_auth} ) {
    50 33        
478             # process allows if we have any
479 3 50       10 if ( defined( $self->{acl}{ $opts{apikey} }{ua_regex_allow}[0] ) ) {
480 3         6 my $ua_matched = 0;
481 3         5 foreach my $item ( @{ $self->{acl}{ $opts{apikey} }{ua_regex_allow} } ) {
  3         8  
482 3         6 eval {
483 3 100       35 if ( $ua =~ /$item/ ) {
484 1         3 $ua_matched = 1;
485             }
486             };
487             }
488             # if no allowed regexp matched, deny it
489 3 100       12 if ( !$ua_matched ) {
490 2         8 return 0;
491             }
492             } ## end if ( defined( $self->{acl}{ $opts{apikey} ...}))
493             # process allows if we have any
494 1 50       5 if ( defined( $self->{acl}{ $opts{apikey} }{ua_regex_deny}[0] ) ) {
495 1         3 my $ua_matched = 0;
496 1         2 foreach my $item ( @{ $self->{acl}{ $opts{apikey} }{ua_regex_dney} } ) {
  1         4  
497 0         0 eval {
498 0 0       0 if ( $ua =~ /$item/ ) {
499 0         0 $ua_matched = 1;
500             }
501             };
502             }
503             # if any deny regexp matched, deny it
504 1 50       3 if ($ua_matched) {
505 0         0 return 0;
506             }
507             } ## end if ( defined( $self->{acl}{ $opts{apikey} ...}))
508             } elsif ( !defined($ua) && $self->{acl}{ $opts{apikey} }{ua_auth} ) {
509 0         0 return 0;
510             }
511              
512             # process path info
513 9 100 66     43 if ( defined($path) && $self->{acl}{ $opts{apikey} }{path_auth} ) {
    50 33        
514             # process allows if we have any
515 3 50       10 if ( defined( $self->{acl}{ $opts{apikey} }{path_regex_allow}[0] ) ) {
516 3         4 my $path_matched = 0;
517 3         7 foreach my $item ( @{ $self->{acl}{ $opts{apikey} }{path_regex_allow} } ) {
  3         8  
518 3         24 eval {
519 3 100       27 if ( $path =~ /$item/ ) {
520 1         2 $path_matched = 1;
521             }
522             };
523             }
524             # if no allowed regexp matched, deny it
525 3 100       8 if ( !$path_matched ) {
526 2         8 return 0;
527             }
528             } ## end if ( defined( $self->{acl}{ $opts{apikey} ...}))
529             # process allows if we have any
530 1 50       33 if ( defined( $self->{acl}{ $opts{apikey} }{ua_regex_deny}[0] ) ) {
531 0         0 my $path_matched = 0;
532 0         0 foreach my $item ( @{ $self->{acl}{ $opts{apikey} }{path_regex_dney} } ) {
  0         0  
533 0         0 eval {
534 0 0       0 if ( $path =~ /$item/ ) {
535 0         0 $path_matched = 1;
536             }
537             };
538             }
539             # if any deny regexp matched, deny it
540 0 0       0 if ($path_matched) {
541 0         0 return 0;
542             }
543             } ## end if ( defined( $self->{acl}{ $opts{apikey} ...}))
544             } elsif ( !defined($path) && $self->{acl}{ $opts{apikey} }{path_auth} ) {
545 0         0 return 0;
546             }
547              
548 7         32 return $self->{acl}{ $opts{apikey} }{final};
549             } ## end sub check
550              
551             =head1 ACL HASH
552              
553             The ACL hash is a hash of hashes. The keys for primary hash are API keys. The keys
554             for the subhashes are as below.
555              
556             Slugs should be though of a freeform text field for access check. Function name or whatever.
557              
558             - ip_auth :: Use IP for authing. If false, the IP will not be checked.
559             - Default :: 0
560              
561             - path_auth :; Use the path for authing. If false it won't be checked.
562             - Default :: 0
563              
564             - slug_auth :; Use the slug for authing. If false it won't be checked.
565             - Default :: 0
566              
567             - ua_auth :; Use the UA for authing. If false it won't be checked.
568             - Default :: 0
569              
570             - require_ip :: Require a value for IP to be specified.
571             - Default :: 0
572              
573             - require_path :: Require a value for path to be specified.
574             - Default :: 0
575              
576             - require_slug :: Require a value for slug to be specified.
577             - Default :: 0
578              
579             - require_slug :: Require a value for UA to be specified.
580             - Default :: 0
581              
582             - final :: The return value for if none of the auth checks are denied.
583             - Default for 'undef'/'nonexistent' apikeys:: 0
584             - Default for other apikeys:: 1
585              
586             - slugs :; Slugs that are allowed for access.
587             - Default :: []
588              
589             - slugs_regex :: Regexps to check slug values against.
590             - Default :: []
591              
592             - allow_subnets :: Allowed subnets for remote IPs. This is a array of CIDRs.
593             - Default :: []
594              
595             - deny_subnets :: Denied subnets for remote IPs. This is a array of CIDRs.
596             - Default :: []
597              
598             - paths_regex_allow :: Allowed paths.
599             - Default :: []
600              
601             - paths_regex_deny :: Denied paths.
602             - Default :: []
603              
604             - ua_regex_allow :: Allowed UAs.
605             - Default :: []
606              
607             - ua_regex_deny :: Denied UAs.
608             - Default :: []
609              
610             There are two special ones for the ACL hash. Those are 'undef' and 'nonexistent'
611             and they should not be used as API keys. These are for in the instances that
612             the apikey for the checkis undef or if specified and does not exist 'nonexistent'
613             is used.
614              
615             By default they are as below.
616              
617             {
618             'undef' => {
619             ip_auth => 0,
620             slug_auth => 0,
621             require_ip => 0,
622             require_slug => 0,
623             final => 0,
624             slugs => [],
625             slugs_regex => [],
626             allow_subnets => [],
627             deny_subnets => [],
628             },
629             'nonexistent' => {
630             ip_auth => 0,
631             slug_auth => 0,
632             require_ip => 0,
633             require_slug => 0,
634             final => 0,
635             slugs => [],
636             slugs_regex => [],
637             allow_subnets => [],
638             deny_subnets => [],
639             },
640             }
641              
642             =head1 ERROR CODES / FLAGS
643              
644             =head2 1, ACLnotHash
645              
646             'acl' as passed to new is not of the of the ref type 'HASH'.
647              
648             =head2 2, ACLitemNotArray
649              
650             The expected item is expected to be of the ref type ARRAY.
651              
652             =head2 3, subnetError
653              
654             Could not init sub_matcher.
655              
656             =head2 4, ACLnotString
657              
658             'acl' as passed to new is not of the of the ref type ''.
659              
660             =head1 AUTHOR
661              
662             Zane C. Bowers-Hadley, C<< >>
663              
664             =head1 BUGS
665              
666             Please report any bugs or feature requests to C, or through
667             the web interface at L. I will be notified, and then you'll
668             automatically be notified of progress on your bug as I make changes.
669              
670              
671              
672              
673             =head1 SUPPORT
674              
675             You can find documentation for this module with the perldoc command.
676              
677             perldoc Web::ACL
678              
679              
680             You can also look for information at:
681              
682             =over 4
683              
684             =item * RT: CPAN's request tracker (report bugs here)
685              
686             L
687              
688             =item * Search CPAN
689              
690             L
691              
692             =back
693              
694              
695             =head1 ACKNOWLEDGEMENTS
696              
697              
698             =head1 LICENSE AND COPYRIGHT
699              
700             This software is Copyright (c) 2024 by Zane C. Bowers-Hadley.
701              
702             This is free software, licensed under:
703              
704             The GNU General Public License, Version 2, June 1991
705              
706              
707             =cut
708              
709             1; # End of Web::ACL