Squashed commit of the following: commit7673f3ee1eAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat May 6 14:19:19 2017 +0100 allow check_acl to accept Device or NetAddr::IP instance commitc31059bc01Author: Oliver Gorwits <oliver@cpan.org> Date: Sat May 6 14:19:00 2017 +0100 update docs commitdeaeab2670Author: Oliver Gorwits <oliver@cpan.org> Date: Sat May 6 14:18:27 2017 +0100 SNMP only stanza has access to full check_acl features commit4a44fa5863Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 1 18:49:38 2017 +0100 add AND operator and negation support to ACLs
		
			
				
	
	
		
			176 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			176 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
| package App::Netdisco::Util::Permission;
 | ||
| 
 | ||
| use strict;
 | ||
| use warnings;
 | ||
| use Dancer qw/:syntax :script/;
 | ||
| 
 | ||
| use Scalar::Util 'blessed';
 | ||
| use NetAddr::IP::Lite ':lower';
 | ||
| use App::Netdisco::Util::DNS 'hostname_from_ip';
 | ||
| 
 | ||
| use base 'Exporter';
 | ||
| our @EXPORT = ();
 | ||
| our @EXPORT_OK = qw/check_acl/;
 | ||
| our %EXPORT_TAGS = (all => \@EXPORT_OK);
 | ||
| 
 | ||
| =head1 NAME
 | ||
| 
 | ||
| App::Netdisco::Util::Permission
 | ||
| 
 | ||
| =head1 DESCRIPTION
 | ||
| 
 | ||
| Helper subroutines to support parts of the Netdisco application.
 | ||
| 
 | ||
| There are no default exports, however the C<:all> tag will export all
 | ||
| subroutines.
 | ||
| 
 | ||
| =head1 EXPORT_OK
 | ||
| 
 | ||
| =head2 check_acl( $ip, \@config )
 | ||
| 
 | ||
| Given a Device or IP address, compares it to the items in C<< \@config >>
 | ||
| then returns true or false. You can control whether any item must match or
 | ||
| all must match, and items can be negated to invert the match logic.
 | ||
| 
 | ||
| There are several options for what C<< \@config >> can contain:
 | ||
| 
 | ||
| =over 4
 | ||
| 
 | ||
| =item *
 | ||
| 
 | ||
| Hostname, IP address, IP prefix (subnet)
 | ||
| 
 | ||
| =item *
 | ||
| 
 | ||
| IP address range, using a hyphen on the last octet/hextet, and no whitespace
 | ||
| 
 | ||
| =item *
 | ||
| 
 | ||
| Regular expression in YAML format (no enforced anchors) which will match the
 | ||
| device DNS name (using a fresh DNS lookup, so works on new discovery), e.g.:
 | ||
| 
 | ||
|  - !!perl/regexp ^sep0.*$
 | ||
| 
 | ||
| =item *
 | ||
| 
 | ||
| "C<property:regexp>" - matched against a device property, such as C<model> or
 | ||
| C<vendor> (with enforced begin/end regexp anchors).
 | ||
| 
 | ||
| =item *
 | ||
| 
 | ||
| "C<op:and>" to require all items to match (or not match) the provided IP or
 | ||
| device. Note that this includes IP address version mismatches (v4-v6).
 | ||
| 
 | ||
| =back
 | ||
| 
 | ||
| To negate any entry, prefix it with "C<!>", for example "C<!192.0.2.0/29>". In
 | ||
| that case, the item must I<not> match the device. This does not apply to
 | ||
| regular expressions (which you can achieve with nonmatching lookahead).
 | ||
| 
 | ||
| To match any device, use "C<any>". To match no devices we suggest using
 | ||
| "C<broadcast>" in the list.
 | ||
| 
 | ||
| =cut
 | ||
| 
 | ||
| sub check_acl {
 | ||
|   my ($thing, $config) = @_;
 | ||
|   my $real_ip = (
 | ||
|     (blessed $thing and $thing->can('ip')) ? $thing->ip : (
 | ||
|       (blessed $thing and $thing->can('addr')) ? $thing->addr : $thing ));
 | ||
|   return 0 if blessed $real_ip; # class we do not understand
 | ||
| 
 | ||
|   my $addr = NetAddr::IP::Lite->new($real_ip);
 | ||
|   my $name = hostname_from_ip($addr->addr) || '!!NO_HOSTNAME!!';
 | ||
|   my $all  = (scalar grep {m/^op:and$/} @$config);
 | ||
| 
 | ||
|   INLIST: foreach my $item (@$config) {
 | ||
|       next INLIST if $item eq 'op:and';
 | ||
| 
 | ||
|       if (ref qr// eq ref $item) {
 | ||
|           if ($name =~ $item) {
 | ||
|             return 1 if not $all;
 | ||
|           }
 | ||
|           else {
 | ||
|             return 0 if $all;
 | ||
|           }
 | ||
|           next INLIST;
 | ||
|       }
 | ||
| 
 | ||
|       my $neg = ($item =~ s/^!//);
 | ||
| 
 | ||
|       if ($item =~ m/^([^:]+)\s*:\s*([^:]+)$/) {
 | ||
|           my $prop  = $1;
 | ||
|           my $match = $2;
 | ||
| 
 | ||
|           # if not in storage, we can't do much with device properties
 | ||
|           next INLIST unless blessed $thing and $thing->in_storage;
 | ||
| 
 | ||
|           # lazy version of vendor: and model:
 | ||
|           if ($neg xor ($thing->can($prop) and defined $thing->$prop
 | ||
|               and $thing->$prop =~ m/^$match$/)) {
 | ||
|             return 1 if not $all;
 | ||
|           }
 | ||
|           else {
 | ||
|             return 0 if $all;
 | ||
|           }
 | ||
|           next INLIST;
 | ||
|       }
 | ||
| 
 | ||
|       if ($item =~ m/([a-f0-9]+)-([a-f0-9]+)$/i) {
 | ||
|           my $first = $1;
 | ||
|           my $last  = $2;
 | ||
| 
 | ||
|           if ($item =~ m/:/) {
 | ||
|               next INLIST if $addr->bits != 128 and not $all;
 | ||
| 
 | ||
|               $first = hex $first;
 | ||
|               $last  = hex $last;
 | ||
| 
 | ||
|               (my $header = $item) =~ s/:[^:]+$/:/;
 | ||
|               foreach my $part ($first .. $last) {
 | ||
|                   my $ip = NetAddr::IP::Lite->new($header . sprintf('%x',$part) . '/128')
 | ||
|                     or next;
 | ||
|                   if ($neg xor ($ip == $addr)) {
 | ||
|                     return 1 if not $all;
 | ||
|                     next INLIST;
 | ||
|                   }
 | ||
|               }
 | ||
|               return 0 if (not $neg and $all);
 | ||
|               return 1 if ($neg and not $all);
 | ||
|           }
 | ||
|           else {
 | ||
|               next INLIST if $addr->bits != 32 and not $all;
 | ||
| 
 | ||
|               (my $header = $item) =~ s/\.[^.]+$/./;
 | ||
|               foreach my $part ($first .. $last) {
 | ||
|                   my $ip = NetAddr::IP::Lite->new($header . $part . '/32')
 | ||
|                     or next;
 | ||
|                   if ($neg xor ($ip == $addr)) {
 | ||
|                     return 1 if not $all;
 | ||
|                     next INLIST;
 | ||
|                   }
 | ||
|               }
 | ||
|               return 0 if (not $neg and $all);
 | ||
|               return 1 if ($neg and not $all);
 | ||
|           }
 | ||
|           next INLIST;
 | ||
|       }
 | ||
| 
 | ||
|       my $ip = NetAddr::IP::Lite->new($item)
 | ||
|         or next INLIST;
 | ||
|       next INLIST if $ip->bits != $addr->bits and not $all;
 | ||
| 
 | ||
|       if ($neg xor ($ip->contains($addr))) {
 | ||
|         return 1 if not $all;
 | ||
|       }
 | ||
|       else {
 | ||
|         return 0 if $all;
 | ||
|       }
 | ||
|       next INLIST;
 | ||
|   }
 | ||
| 
 | ||
|   return ($all ? 1 : 0);
 | ||
| }
 | ||
| 
 | ||
| 1;
 |