File Coverage

blib/lib/PlugAuth/Plugin/FlatAuthz.pm
Criterion Covered Total %
statement 300 307 97.7
branch 68 84 80.9
condition 49 57 85.9
subroutine 45 45 100.0
pod 16 17 94.1
total 478 510 93.7


line stmt bran cond sub pod time code
1             package PlugAuth::Plugin::FlatAuthz;
2              
3             # ABSTRACT: Authorization using flat files for PlugAuth
4             our $VERSION = '0.38'; # VERSION
5              
6              
7 41     41   20623 use strict;
  41         120  
  41         1425  
8 41     41   265 use warnings;
  41         120  
  41         1389  
9 41     41   943 use 5.010001;
  41         174  
10 41     41   284 use Log::Log4perl qw( :easy );
  41         102  
  41         409  
11 41     41   48244 use Text::Glob qw( match_glob );
  41         47491  
  41         2806  
12 41     41   5151 use Clone qw( clone );
  41         35781  
  41         2501  
13 41     41   656 use Role::Tiny::With;
  41         302  
  41         2019  
14 41     41   669 use File::Touch;
  41         1155  
  41         2070  
15 41     41   285 use List::Util qw( uniq );
  41         106  
  41         60267  
16              
17             with 'PlugAuth::Role::Plugin';
18             with 'PlugAuth::Role::Authz';
19             with 'PlugAuth::Role::Refresh';
20             with 'PlugAuth::Role::Flat';
21              
22             our %all_users;
23             our %groupUser; # $groupUser{$group}{$user} is true iff $user is in $group
24             our %userGroups; # $userGroups{$user}{$group} is true iff $user is in $group
25             our %resourceActionGroup; # $resourceActionGroup{$resource}{$action}{$group} is true iff $group can do $action on $resource
26             our %actions; # All defined actions $actions{$action} = 1;
27             our %hostTag; # $hostTag{$host}{$tag} is true iff $user has tag $tag
28              
29              
30             sub refresh
31             {
32 691     691 1 3007 my($class) = @_;
33 691         3635 my $config = $class->global_config;
34 691 100       3752 if ( has_changed( $config->group_file ) )
35             {
36 105         1143 %all_users = map { $_ => 1 } __PACKAGE__->app->auth->all_users;
  418         1648  
37 105         1233 %groupUser = ();
38 105         614 my %data = __PACKAGE__->read_file( $config->group_file, nest => 1, lc_values => 1, lc_keys => 1 );
39 105         599 for my $k (keys %data)
40             {
41 280         576 my %users;
42 280         520 for my $v (keys %{ $data{$k} })
  280         1119  
43             {
44 557         2361 my @matches = match_glob( $v, keys %all_users);
45 557 100       79677 next unless @matches;
46 551         2331 @users{ @matches } = (1) x @matches;
47             }
48 280         1518 $groupUser{$k} = \%users;
49             }
50 105         1250 %userGroups = __PACKAGE__->_reverse_nest(%groupUser);
51             }
52 691 100       5109 if ( has_changed( $config->resource_file ) )
53             {
54 60         503 %all_users = map { $_ => 1 } __PACKAGE__->app->auth->all_users;
  250         831  
55 60         530 %resourceActionGroup = __PACKAGE__->read_file( $config->resource_file, nest => 2, lc_values => 1 );
56              
57 60         455 foreach my $resource (keys %resourceActionGroup)
58             {
59             # TODO: maybe #g for group
60 111 100       428 if($resource =~ /#u/) {
61 2         7 my $value = delete $resourceActionGroup{$resource};
62              
63 2         8 foreach my $user (keys %all_users)
64             {
65 3         9 my $new_resource = $resource;
66 3         35 my $new_value = clone $value;
67              
68 3         13 $new_resource =~ s/#u/$user/g;
69              
70 3         9 foreach my $users (values %$new_value)
71             {
72 3 50       12 if(defined $users->{'#u'})
73             {
74 3         6 delete $users->{'#u'};
75 3         9 $users->{$user} = 1;
76             }
77             }
78              
79 3         11 $resourceActionGroup{$new_resource} = $new_value;
80             }
81             }
82             }
83              
84 60         238 %actions = map { map { $_ => 1} keys %$_ } values %resourceActionGroup;
  112         361  
  129         514  
85             }
86 691         4906 my $h = $config->host_file(default => '');
87 691 100 100     14412 if ( ( $h ) && has_changed( $h ) )
88             {
89 22         207 %hostTag = __PACKAGE__->read_file( $h, nest => 1 );
90             }
91 691         4888 1;
92             }
93              
94             sub init
95             {
96             # When the user list has changed, the group files need to be reloaded, because
97             # each user has his/her own group, so we touch the group file
98              
99 46     46 0 178 my($self) = @_;
100            
101 46         356 $self->flat_init;
102            
103 46         588 my $touch = File::Touch->new(
104             mtime_only => 1,
105             no_create => 1,
106             );
107 46         2694 my @list = ($self->app->config->group_file(default => []));
108            
109             $self->app->on(user_list_changed => sub {
110 16     16   513 $touch->touch(@list);
111 46         4190 });
112             }
113              
114              
115             sub can_user_action_resource
116             {
117 447     447 1 1933 my ($class, $user,$action,$resource) = @_;
118 447         1711 $user = lc $user;
119 447         1082 my $found;
120             GROUP:
121 447         1309 for my $group ( $user, keys %{ $userGroups{$user} } )
  447         2844  
122             {
123             # check exact match on the resource so / will match
124 1025 100 100     6249 if($resourceActionGroup{$resource}
      100        
125             && $resourceActionGroup{$resource}{$action}
126             && $resourceActionGroup{$resource}{$action}{$group})
127             {
128 115         540 $found = "group: $group resource: $resource";
129 115         468 last GROUP;
130             }
131 910         3989 for my $subresource (__PACKAGE__->_prefixes($resource))
132             {
133             next unless $resourceActionGroup{$subresource}
134             && $resourceActionGroup{$subresource}{$action}
135 1806 100 100     10520 && $resourceActionGroup{$subresource}{$action}{$group};
      100        
136 87         389 $found = "group: $group resource: $subresource";
137 87         326 last GROUP;
138             }
139             }
140 447         2456 return $found;
141             }
142              
143              
144             sub match_resources {
145 16     16 1 69 my($class, $resourceregex) = @_;
146 16         374 return (grep /$resourceregex/, keys %resourceActionGroup);
147             }
148              
149             sub _reverse_nest {
150 105     105   355 my $class = shift;
151             # Given a nested hash ( a => b => c), return one with (b => a => c);
152 105         447 my %h = @_;
153 105         283 my %new;
154 105         722 while (my ($a,$bc) = each %h)
155             {
156 280         1229 while (my ($b,$c) = each %$bc)
157             {
158 778         3122 $new{$b}{$a} = $c;
159             }
160             }
161 105         1059 return %new;
162             }
163              
164             sub _prefixes {
165 910     910   1997 my $class = shift;
166             # Given a string "/a/b/c/d" return an array of prefixes :
167             # "/", "/a", "/a/b", /a/b/c", "/a/b/c/d"
168 910         2014 my $str = shift;
169 910         4047 my @p = split /\//, $str;
170 910         3130 my @prefixes = ( map { '/'. join '/', @p[1..$_] } 0..$#p );
  1966         8504  
171 910         3878 return @prefixes;
172             }
173              
174              
175             sub host_has_tag {
176 85     85 1 2788 my ($class, $host, $tag) = @_;
177 85         1135 return exists($hostTag{$host}{$tag});
178             }
179              
180              
181             sub actions {
182 12     12 1 125 return sort keys %actions;
183             }
184              
185              
186             sub groups_for_user {
187 45     45 1 290 my $class = shift;
188 45 50       172 my $user = shift or return ();
189 45         164 $user = lc $user;
190 45 100       211 return unless $all_users{$user};
191 43 100       101 return [ sort $user, keys %{ $userGroups{ $user } || {} } ];
  43         482  
192             }
193              
194              
195             sub all_groups {
196 28     28 1 347 return sort keys %groupUser;
197             }
198              
199              
200             sub users_in_group {
201 79     79 1 1304 my($class, $group) = @_;
202 79 50       377 return unless defined $group;
203 79         367 $group = lc $group;
204 79 100       448 return unless defined $groupUser{$group};
205 72         191 return [keys %{ $groupUser{$group} }];
  72         648  
206             }
207              
208              
209             sub create_group
210             {
211 22     22 1 95 my($self, $group, $users) = @_;
212              
213 22 50       102 unless(defined $group)
214             {
215 0         0 WARN "Group not provided";
216 0         0 return 0;
217             }
218              
219 22 100       109 if(defined $groupUser{$group})
220             {
221 1         11 WARN "Group $group already exists";
222 1         1748 return 0;
223             }
224              
225 21 100       90 $users = '' unless defined $users;
226              
227 21         120 my $filename = $self->global_config->group_file;
228              
229             my $ok = $self->lock_and_update_file($filename, sub {
230 41     41   418 use autodie;
  41         117  
  41         456  
231 21     21   72 my($fh) = @_;
232              
233 21         63 my $buffer = '';
234 21         333 while(! eof $fh)
235             {
236 41         107 my $line = <$fh>;
237 41         78 chomp $line;
238 41         144 $buffer .= "$line\n";
239             }
240 21         114 $buffer .= "$group : $users\n";
241            
242 21         84 $buffer;
243 21         516 });
244            
245 21 50       233 return 0 unless $ok;
246              
247 21         192 INFO "created group $group with members $users";
248 21         29278 return 1;
249             }
250              
251              
252             sub delete_group
253             {
254 6     6 1 30 my($self, $group) = @_;
255            
256 6         27 $group = lc $group;
257              
258 6 100 66     67 unless($group && defined $groupUser{$group})
259             {
260 1         12 WARN "Group $group does not exist";
261 1         1776 return 0;
262             }
263              
264 5         38 my $filename = $self->global_config->group_file;
265              
266             my $ok = $self->lock_and_update_file($filename, sub {
267 41     41   315840 use autodie;
  41         143  
  41         288  
268 5     5   25 my($fh) = @_;
269 5         21 my $buffer = '';
270 5         73 while(! eof $fh)
271             {
272 24         79 my $line = <$fh>;
273 24         56 chomp $line;
274 24         127 my($thisgroup, $password) = split /\s*:/, $line;
275 24 100 100     174 next if defined $thisgroup && lc $thisgroup eq $group;
276 19         80 $buffer .= "$line\n";
277             }
278              
279 5         30 $buffer;
280 5         146 });
281              
282 5 50       62 return 0 unless $ok;
283              
284 5         53 INFO "deleted group $group";
285 5         7848 return 1;
286             }
287              
288              
289             sub update_group
290             {
291 13     13 1 55 my($self, $group, $users) = @_;
292              
293 13         53 $group = lc $group;
294            
295 13 100 66     129 unless($group && defined $groupUser{$group})
296             {
297 1         9 WARN "Group $group does not exist";
298 1         1363 return 0;
299             }
300              
301 12 100       59 return 1 unless defined $users;
302              
303 11         73 my $filename = $self->global_config->group_file;
304              
305             my $ok = $self->lock_and_update_file($filename, sub {
306 41     41   321123 use autodie;
  41         125  
  41         303  
307 11     11   41 my($fh) = @_;
308              
309 11         40 my $buffer = '';
310 11         146 while(! eof $fh)
311             {
312 56         205 my $line = <$fh>;
313 56         144 chomp $line;
314 56         278 my($thisgroup) = split /\s*:/, $line;
315 56 100 100     373 $line =~ s{:.*$}{: $users} if defined $thisgroup && lc($thisgroup) eq $group;
316 56         323 $buffer .= "$line\n";
317             }
318 11         58 $buffer;
319 11         346 });
320            
321 11 50       123 return 0 unless $ok;
322 11         110 INFO "update group $group set members to $users";
323 11         16406 return 1;
324             }
325              
326              
327             sub add_user_to_group
328             {
329 17     17 1 74 my($self, $group, $user) = @_;
330 17         73 $group = lc $group;
331 17         55 $user = lc $user;
332              
333 17 100 66     162 unless($group && defined $groupUser{$group})
334             {
335 2         32 WARN "Group $group does not exist";
336 2         2846 return 0;
337             }
338            
339 15         40 my $new_user_list;
340 15         84 my $filename = $self->global_config->group_file;
341            
342             my $ok = $self->lock_and_update_file($filename, sub {
343 41     41   313006 use autodie;
  41         134  
  41         303  
344 15     15   58 my ($fh) = @_;
345            
346 15         50 my $buffer = '';
347 15         216 while(! eof $fh)
348             {
349 85         255 my $line = <$fh>;
350 85         166 chomp $line;
351 85         434 my($thisgroup, $users) = split /\s*:\s*/, $line;
352 85 100 100     429 if(defined $thisgroup && lc($thisgroup) eq $group)
353             {
354 15         201 $users = join ',', uniq (split(/\s*,\s*/, $users), $user);
355 15         93 $buffer .= "$thisgroup: $users\n";
356 15         84 $new_user_list = $users;
357             }
358             else
359             {
360 70         336 $buffer .= "$line\n";
361             }
362             }
363            
364 15         101 $buffer;
365 15         389 });
366            
367 15 50       187 return unless $ok;
368 15         187 INFO "update group $group set members to $new_user_list";
369 15         23897 return $new_user_list;
370             }
371              
372              
373             sub remove_user_from_group
374             {
375 6     6 1 155 my($self, $group, $user) = @_;
376 6         44 $group = lc $group;
377 6         27 $user = lc $user;
378              
379 6 50 33     85 unless($group && defined $groupUser{$group})
380             {
381 0         0 WARN "Group $group does not exist";
382 0         0 return 0;
383             }
384            
385 6         21 my $new_user_list;
386 6         51 my $filename = $self->global_config->group_file;
387            
388             my $ok = $self->lock_and_update_file($filename, sub {
389 41     41   332476 use autodie;
  41         143  
  41         306  
390 6     6   30 my ($fh) = @_;
391            
392 6         23 my $buffer = '';
393 6         97 while(! eof $fh)
394             {
395 34         115 my $line = <$fh>;
396 34         86 chomp $line;
397 34         225 my($thisgroup, $users) = split /\s*:\s*/, $line;
398 34 100 100     212 if(defined $thisgroup && lc($thisgroup) eq $group)
399             {
400 6         25 $users = join ',', grep { lc($_) ne $user } uniq @{ $self->users_in_group($group) };
  20         99  
  6         45  
401 6         46 $buffer .= "$thisgroup: $users\n";
402 6         44 $new_user_list = $users;
403             }
404             else
405             {
406 28         161 $buffer .= "$line\n";
407             }
408             }
409            
410 6         37 $buffer;
411 6         217 });
412            
413 6 50       96 return unless $ok;
414 6         76 INFO "update group $group set members to $new_user_list";
415 6         11283 return $new_user_list;
416             }
417              
418              
419             sub grant
420             {
421 17     17 1 88 my($self, $group, $action, $resource) = @_;
422            
423 17         79 $group = lc $group;
424              
425 17 100 100     235 unless($group && (defined $groupUser{$group} || defined $all_users{$group}))
      66        
426             {
427 1         16 WARN "Group (or user) $group does not exist";
428 1         1355 return 0;
429             }
430              
431 16         124 $resource =~ s{^/?}{/};
432              
433 16 50       123 if($resourceActionGroup{$resource}->{$action}->{$group})
434             {
435 0         0 WARN "grant already added $group $action $resource";
436 0         0 return 1;
437             }
438              
439 16         107 my $filename = $self->global_config->resource_file;
440              
441             my $ok = $self->lock_and_update_file($filename, sub {
442 41     41   334030 use autodie;
  41         139  
  41         416  
443 16     16   69 my($fh) = @_;
444              
445 16         51 my $buffer = '';
446            
447 16         223 while(! eof $fh)
448             {
449 28         135 my $line = <$fh>;
450 28         68 chomp $line;
451 28         175 $buffer .= "$line\n";
452             }
453 16         120 $buffer .= "$resource ($action) : $group\n";
454 16         76 $buffer;
455 16         472 });
456            
457 16 50       183 return 0 unless $ok;
458 16         876 INFO "grant $group $action $resource";
459 16         25993 return 1;
460             }
461              
462              
463             sub revoke
464             {
465 15     15 1 75 my($self, $group, $action, $resource) = @_;
466            
467 15         65 $group = lc $group;
468              
469 15 100 100     166 unless($group && (defined $groupUser{$group} || defined $all_users{$group}))
      66        
470             {
471 1         12 WARN "Group (or user) $group does not exist";
472 1         1986 return 0;
473             }
474              
475 14 100       94 $resource = '/' . $resource unless $resource =~ /\//;
476              
477 14 100       86 unless($resourceActionGroup{$resource}->{$action}->{$group})
478             {
479 1         9 WARN "Group (or user) $group not authorized to $action on $resource";
480 1         1528 return 0;
481             }
482            
483 13         101 my $filename = $self->global_config->resource_file;
484              
485             my $ok = $self->lock_and_update_file($filename, sub {
486 41     41   326162 use autodie;
  41         152  
  41         322  
487 13     13   52 my($fh) = @_;
488              
489 13         44 my $buffer = '';
490 13         179 while(! eof $fh)
491             {
492 44         169 my $line = <$fh>;
493 44         103 chomp $line;
494 44 50 100     653 if($line =~ /^#/)
    100 66        
495             {
496 0         0 $buffer .= "$line\n";
497             }
498             elsif($line =~ m{^\s*(.*?)\s*\((.*?)\)\s*:\s*(.*?)\s*$} && $1 eq $resource && $2 eq $action)
499             {
500 13         85 my(@groups) = grep { $_ ne $group } split /,/, $3;
  13         70  
501 13 50       120 $buffer .= "$resource ($action) : " . join(',', @groups) . "\n"
502             if @groups > 0;
503             }
504             else
505             {
506 31         193 $buffer .= "$line\n";
507             }
508             }
509 13         61 $buffer;
510 13         363 });
511            
512 13 50       172 return 0 unless $ok;
513 13         130 INFO "revoke $group $action $resource";
514 13         23270 return 1;
515             }
516              
517              
518             sub granted
519             {
520 3     3 1 11 my($self) = @_;
521            
522 3         26 my $filename = $self->global_config->resource_file;
523            
524 3         58 my @granted_list;
525            
526             my $ok = $self->lock_and_read_file($filename, sub {
527 41     41   335904 use autodie;
  41         138  
  41         310  
528 3     3   15 my($fh) = @_;
529            
530 3         85 while(! eof $fh)
531             {
532 10         49 my $line = <$fh>;
533 10 50       51 next if $line =~ /^#/;
534 10 100       185 push @granted_list, "$1 ($2): $3"
535             if $line =~ m{^\s*(.*?)\s*\((.*?)\)\s*:\s*(.*?)\s*$};
536             }
537 3         44 });
538            
539 3         62 return \@granted_list;
540             }
541              
542             1;
543              
544             __END__
545              
546             =pod
547              
548             =encoding UTF-8
549              
550             =head1 NAME
551              
552             PlugAuth::Plugin::FlatAuthz - Authorization using flat files for PlugAuth
553              
554             =head1 VERSION
555              
556             version 0.38
557              
558             =head1 SYNOPSIS
559              
560             In your /etc/PlugAuth.conf
561              
562             ---
563             url: http://localhost:1234
564             group_file: /etc/plugauth/group.txt
565             resource_file: /etc/plugauth/resource.txt
566             host_file: /etc/plugauth/host.txt
567              
568             touch the storage files:
569              
570             % touch /etc/plugauth/group.txt \
571             /etc/plugauth/resource.txt \
572             /etc/plugauth/host.txt
573              
574             Start PlugAuth:
575              
576             % plugauth start
577              
578             =head1 DESCRIPTION
579              
580             This is the default Authorization plugin for L<PlugAuth>. It is designed to work closely
581             with L<PlugAuth::Plugin::FlatAuth> which is the default Authentication plugin.
582              
583             This plugin provides storage for groups, hosts and access control for PlugAuth. In addition
584             it provides a mechanism for PlugAuth to alter the group, host and access control databases.
585              
586             =head1 CONFIGURATION
587              
588             =head2 group_file
589              
590             The group file looks similar to a standard UNIX /etc/group file. Entries can be changed using
591             either your favorite editor, or by using L<PlugAuth::Client>. In this example there is a group
592             both to which both bob and alice belong:
593              
594             both: alice, bob
595              
596             Group members can be specified using a glob (see L<Text::Glob>) which match against the set of all users:
597              
598             all: *
599              
600             Each user automatically gets his own group, so if there are users named bob and alice, this is
601             unnecessary:
602              
603             alice: alice
604             bob: bob
605              
606             =head2 resource_file
607              
608             Each line of resource.txt has a resource, an action (in parentheses), and then a list of users or groups.
609             The line grants permission for those groups to perform that action on that resource :
610              
611             /house/door (enter) : alice, bob
612             /house/backdoor (enter) : both
613             /house/window (break) : alice
614             /house (GET) : bob
615              
616             =head2 host_file
617              
618             The host file /etc/pluginauth/host.txt looks like this :
619              
620             192.168.1.99:trusted
621             192.168.1.100:trusted
622              
623             The IP addresses on the right represent hosts from which authorization should succeed.
624              
625             =head1 METHODS
626              
627             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>refresh
628              
629             Refresh the data (checks the files, and re-reads if necessary).
630              
631             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>can_user_action_resource( $user, $action, $resource )
632              
633             If $user can perform $action on $resource, return a string containing
634             the group and resource that permits this. Otherwise, return false.
635              
636             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>match_resources( $regex )
637              
638             Given a regex, return all resources that match that regex.
639              
640             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>host_has_tag( $host, $tag )
641              
642             Returns true if the given host has the given tag.
643              
644             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>actions
645              
646             Returns a list of actions.
647              
648             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>groups_for_user( $user )
649              
650             Returns the groups the given user belongs to as a list ref.
651             Returns undef if the user does not exist.
652              
653             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>all_groups
654              
655             Returns a list of all groups.
656              
657             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>users_in_group( $group )
658              
659             Return the list of users (as an array ref) that belong
660             to the given group. Each user belongs to a special
661             group that is the same as their user name and just
662             contains themselves, and this will be included in the
663             list.
664              
665             Returns undef if the group does not exist.
666              
667             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>create_group( $group, $users )
668              
669             Create a new group with the given users. $users is a comma
670             separated list of user names.
671              
672             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>delete_group( $group )
673              
674             Delete the given group.
675              
676             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>update_group( $group, $users )
677              
678             Update the given group, setting the set of users that belong to that
679             group. The existing group membership will be replaced with the new one.
680             $users is a comma separated list of user names.
681              
682             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>add_user_to_group( $group, $user )
683              
684             Add the given user to the given group.
685              
686             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>remove_user_from_group( $group, $user )
687              
688             Remove the given user from the given group
689              
690             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>grant( $group, $action, $resource )
691              
692             Grant the given group or user ($group) the authorization to perform the given
693             action ($action) on the given resource ($resource).
694              
695             =head2 PlugAuth::Plugin::FlatAuthz-E<gt>revoke( $group, $action, $resource )
696              
697             Revoke the given group or user ($group) the authorization to perform the given
698             action ($action) on the given resource ($resource).
699              
700             =head2 $plugin-E<gt>granted
701              
702             Returns a list of granted permissions
703              
704             =head1 SEE ALSO
705              
706             L<PlugAuth>, L<PlugAuth::Plugin::FlatAuth>
707              
708             =head1 AUTHOR
709              
710             Graham Ollis <gollis@sesda3.com>
711              
712             =head1 COPYRIGHT AND LICENSE
713              
714             This software is copyright (c) 2012 by NASA GSFC.
715              
716             This is free software; you can redistribute it and/or modify it under
717             the same terms as the Perl 5 programming language system itself.
718              
719             =cut