File Coverage

blib/lib/PlugAuth/Plugin/FlatAuthz.pm
Criterion Covered Total %
statement 300 307 97.7
branch 68 84 80.9
condition 43 57 75.4
subroutine 45 45 100.0
pod 16 17 94.1
total 472 510 92.5


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.35'; # VERSION
5              
6              
7 41     41   23470 use strict;
  41         105  
  41         1556  
8 41     41   217 use warnings;
  41         70  
  41         1365  
9 41     41   1170 use 5.010001;
  41         143  
10 41     41   220 use Log::Log4perl qw( :easy );
  41         64  
  41         353  
11 41     41   34633 use Text::Glob qw( match_glob );
  41         50459  
  41         3059  
12 41     41   6232 use Clone qw( clone );
  41         126286  
  41         3123  
13 41     41   1176 use Role::Tiny::With;
  41         448  
  41         2327  
14 41     41   987 use File::Touch;
  41         13749  
  41         2459  
15 41     41   242 use List::MoreUtils qw( uniq );
  41         76  
  41         716  
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 1656 my($class) = @_;
33 691         2817 my $config = $class->global_config;
34 691 100       3042 if ( has_changed( $config->group_file ) )
35             {
36 105         1032 %all_users = map { $_ => 1 } __PACKAGE__->app->auth->all_users;
  418         1120  
37 105         1192 %groupUser = ();
38 105         529 my %data = __PACKAGE__->read_file( $config->group_file, nest => 1, lc_values => 1, lc_keys => 1 );
39 105         419 for my $k (keys %data)
40             {
41 280         393 my %users;
42 280         332 for my $v (keys %{ $data{$k} })
  280         923  
43             {
44 557         2107 my @matches = match_glob( $v, keys %all_users);
45 557 100       63249 next unless @matches;
46 551         1850 @users{ @matches } = (1) x @matches;
47             }
48 280         794 $groupUser{$k} = \%users;
49             }
50 105         1137 %userGroups = __PACKAGE__->_reverse_nest(%groupUser);
51             }
52 691 100       3692 if ( has_changed( $config->resource_file ) )
53             {
54 60         459 %all_users = map { $_ => 1 } __PACKAGE__->app->auth->all_users;
  250         651  
55 60         499 %resourceActionGroup = __PACKAGE__->read_file( $config->resource_file, nest => 2, lc_values => 1 );
56              
57 60         333 foreach my $resource (keys %resourceActionGroup)
58             {
59             # TODO: maybe #g for group
60 111 100       366 if($resource =~ /#u/) {
61 2         6 my $value = delete $resourceActionGroup{$resource};
62              
63 2         8 foreach my $user (keys %all_users)
64             {
65 3         5 my $new_resource = $resource;
66 3         28 my $new_value = clone $value;
67              
68 3         9 $new_resource =~ s/#u/$user/g;
69              
70 3         8 foreach my $users (values %$new_value)
71             {
72 3 50       10 if(defined $users->{'#u'})
73             {
74 3         5 delete $users->{'#u'};
75 3         7 $users->{$user} = 1;
76             }
77             }
78              
79 3         18 $resourceActionGroup{$new_resource} = $new_value;
80             }
81             }
82             }
83              
84 60         192 %actions = map { map { $_ => 1} keys %$_ } values %resourceActionGroup;
  112         235  
  129         378  
85             }
86 691         3684 my $h = $config->host_file(default => '');
87 691 100 100     9520 if ( ( $h ) && has_changed( $h ) )
88             {
89 22         187 %hostTag = __PACKAGE__->read_file( $h, nest => 1 );
90             }
91 691         3484 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 205 my($self) = @_;
100            
101 46         301 $self->flat_init;
102            
103 46         528 my $touch = File::Touch->new(
104             mtime_only => 1,
105             no_create => 1,
106             );
107 46         1935 my @list = ($self->app->config->group_file(default => []));
108            
109             $self->app->on(user_list_changed => sub {
110 16     16   350 $touch->touch(@list);
111 46         3012 });
112             }
113              
114              
115             sub can_user_action_resource
116             {
117 447     447 1 1226 my ($class, $user,$action,$resource) = @_;
118 447         1392 $user = lc $user;
119 447         721 my $found;
120             GROUP:
121 447         743 for my $group ( $user, keys %{ $userGroups{$user} } )
  447         2717  
122             {
123             # check exact match on the resource so / will match
124 1027 100 66     4908 if($resourceActionGroup{$resource}
      66        
125             && $resourceActionGroup{$resource}{$action}
126             && $resourceActionGroup{$resource}{$action}{$group})
127             {
128 115         455 $found = "group: $group resource: $resource";
129 115         332 last GROUP;
130             }
131 912         2967 for my $subresource (__PACKAGE__->_prefixes($resource))
132             {
133             next unless $resourceActionGroup{$subresource}
134             && $resourceActionGroup{$subresource}{$action}
135 1810 100 66     8358 && $resourceActionGroup{$subresource}{$action}{$group};
      66        
136 87         317 $found = "group: $group resource: $subresource";
137 87         253 last GROUP;
138             }
139             }
140 447         1800 return $found;
141             }
142              
143              
144             sub match_resources {
145 16     16 1 34 my($class, $resourceregex) = @_;
146 16         291 return (grep /$resourceregex/, keys %resourceActionGroup);
147             }
148              
149             sub _reverse_nest {
150 105     105   288 my $class = shift;
151             # Given a nested hash ( a => b => c), return one with (b => a => c);
152 105         350 my %h = @_;
153 105         168 my %new;
154 105         561 while (my ($a,$bc) = each %h)
155             {
156 280         880 while (my ($b,$c) = each %$bc)
157             {
158 778         2570 $new{$b}{$a} = $c;
159             }
160             }
161 105         988 return %new;
162             }
163              
164             sub _prefixes {
165 912     912   1383 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 912         1173 my $str = shift;
169 912         3697 my @p = split /\//, $str;
170 912         2032 my @prefixes = ( map { '/'. join '/', @p[1..$_] } 0..$#p );
  1970         6004  
171 912         2922 return @prefixes;
172             }
173              
174              
175             sub host_has_tag {
176 85     85 1 1841 my ($class, $host, $tag) = @_;
177 85         1020 return exists($hostTag{$host}{$tag});
178             }
179              
180              
181             sub actions {
182 12     12 1 113 return sort keys %actions;
183             }
184              
185              
186             sub groups_for_user {
187 45     45 1 210 my $class = shift;
188 45 50       166 my $user = shift or return ();
189 45         125 $user = lc $user;
190 45 100       170 return unless $all_users{$user};
191 43 100       73 return [ sort $user, keys %{ $userGroups{ $user } || {} } ];
  43         498  
192             }
193              
194              
195             sub all_groups {
196 28     28 1 316 return sort keys %groupUser;
197             }
198              
199              
200             sub users_in_group {
201 79     79 1 964 my($class, $group) = @_;
202 79 50       338 return unless defined $group;
203 79         324 $group = lc $group;
204 79 100       441 return unless defined $groupUser{$group};
205 72         129 return [keys %{ $groupUser{$group} }];
  72         578  
206             }
207              
208              
209             sub create_group
210             {
211 22     22 1 60 my($self, $group, $users) = @_;
212              
213 22 50       103 unless(defined $group)
214             {
215 0         0 WARN "Group not provided";
216 0         0 return 0;
217             }
218              
219 22 100       99 if(defined $groupUser{$group})
220             {
221 1         7 WARN "Group $group already exists";
222 1         947 return 0;
223             }
224              
225 21 100       169 $users = '' unless defined $users;
226              
227 21         119 my $filename = $self->global_config->group_file;
228              
229             my $ok = $self->lock_and_update_file($filename, sub {
230 41     41   80015 use autodie;
  41         126  
  41         433  
231 21     21   47 my($fh) = @_;
232              
233 21         51 my $buffer = '';
234 21         375 while(! eof $fh)
235             {
236 41         78 my $line = <$fh>;
237 41         40 chomp $line;
238 41         126 $buffer .= "$line\n";
239             }
240 21         88 $buffer .= "$group : $users\n";
241            
242 21         70 $buffer;
243 21         392 });
244            
245 21 50       225 return 0 unless $ok;
246              
247 21         158 INFO "created group $group with members $users";
248 21         14526 return 1;
249             }
250              
251              
252             sub delete_group
253             {
254 6     6 1 21 my($self, $group) = @_;
255            
256 6         21 $group = lc $group;
257              
258 6 100 66     73 unless($group && defined $groupUser{$group})
259             {
260 1         12 WARN "Group $group does not exist";
261 1         1313 return 0;
262             }
263              
264 5         33 my $filename = $self->global_config->group_file;
265              
266             my $ok = $self->lock_and_update_file($filename, sub {
267 41     41   233599 use autodie;
  41         94  
  41         254  
268 5     5   16 my($fh) = @_;
269 5         16 my $buffer = '';
270 5         104 while(! eof $fh)
271             {
272 24         57 my $line = <$fh>;
273 24         32 chomp $line;
274 24         111 my($thisgroup, $password) = split /\s*:/, $line;
275 24 100 100     157 next if defined $thisgroup && lc $thisgroup eq $group;
276 19         74 $buffer .= "$line\n";
277             }
278              
279 5         24 $buffer;
280 5         135 });
281              
282 5 50       62 return 0 unless $ok;
283              
284 5         38 INFO "deleted group $group";
285 5         3849 return 1;
286             }
287              
288              
289             sub update_group
290             {
291 13     13 1 43 my($self, $group, $users) = @_;
292              
293 13         48 $group = lc $group;
294            
295 13 100 66     144 unless($group && defined $groupUser{$group})
296             {
297 1         13 WARN "Group $group does not exist";
298 1         1610 return 0;
299             }
300              
301 12 100       48 return 1 unless defined $users;
302              
303 11         59 my $filename = $self->global_config->group_file;
304              
305             my $ok = $self->lock_and_update_file($filename, sub {
306 41     41   218254 use autodie;
  41         201  
  41         257  
307 11     11   29 my($fh) = @_;
308              
309 11         28 my $buffer = '';
310 11         178 while(! eof $fh)
311             {
312 56         118 my $line = <$fh>;
313 56         90 chomp $line;
314 56         223 my($thisgroup) = split /\s*:/, $line;
315 56 100 100     372 $line =~ s{:.*$}{: $users} if defined $thisgroup && lc($thisgroup) eq $group;
316 56         241 $buffer .= "$line\n";
317             }
318 11         46 $buffer;
319 11         253 });
320            
321 11 50       123 return 0 unless $ok;
322 11         110 INFO "update group $group set members to $users";
323 11         9422 return 1;
324             }
325              
326              
327             sub add_user_to_group
328             {
329 17     17 1 49 my($self, $group, $user) = @_;
330 17         52 $group = lc $group;
331 17         38 $user = lc $user;
332              
333 17 100 66     179 unless($group && defined $groupUser{$group})
334             {
335 2         16 WARN "Group $group does not exist";
336 2         1744 return 0;
337             }
338            
339 15         27 my $new_user_list;
340 15         92 my $filename = $self->global_config->group_file;
341            
342             my $ok = $self->lock_and_update_file($filename, sub {
343 41     41   218062 use autodie;
  41         96  
  41         257  
344 15     15   32 my ($fh) = @_;
345            
346 15         30 my $buffer = '';
347 15         195 while(! eof $fh)
348             {
349 85         156 my $line = <$fh>;
350 85         96 chomp $line;
351 85         342 my($thisgroup, $users) = split /\s*:\s*/, $line;
352 85 100 100     410 if(defined $thisgroup && lc($thisgroup) eq $group)
353             {
354 15         214 $users = join ',', uniq (split(/\s*,\s*/, $users), $user);
355 15         85 $buffer .= "$thisgroup: $users\n";
356 15         52 $new_user_list = $users;
357             }
358             else
359             {
360 70         257 $buffer .= "$line\n";
361             }
362             }
363            
364 15         50 $buffer;
365 15         302 });
366            
367 15 50       189 return unless $ok;
368 15         120 INFO "update group $group set members to $new_user_list";
369 15         16856 return $new_user_list;
370             }
371              
372              
373             sub remove_user_from_group
374             {
375 6     6 1 32 my($self, $group, $user) = @_;
376 6         27 $group = lc $group;
377 6         42 $user = lc $user;
378              
379 6 50 33     63 unless($group && defined $groupUser{$group})
380             {
381 0         0 WARN "Group $group does not exist";
382 0         0 return 0;
383             }
384            
385 6         13 my $new_user_list;
386 6         47 my $filename = $self->global_config->group_file;
387            
388             my $ok = $self->lock_and_update_file($filename, sub {
389 41     41   217641 use autodie;
  41         89  
  41         256  
390 6     6   14 my ($fh) = @_;
391            
392 6         18 my $buffer = '';
393 6         81 while(! eof $fh)
394             {
395 34         56 my $line = <$fh>;
396 34         38 chomp $line;
397 34         119 my($thisgroup, $users) = split /\s*:\s*/, $line;
398 34 100 100     143 if(defined $thisgroup && lc($thisgroup) eq $group)
399             {
400 6         11 $users = join ',', grep { lc($_) ne $user } uniq @{ $self->users_in_group($group) };
  20         61  
  6         29  
401 6         31 $buffer .= "$thisgroup: $users\n";
402 6         31 $new_user_list = $users;
403             }
404             else
405             {
406 28         102 $buffer .= "$line\n";
407             }
408             }
409            
410 6         22 $buffer;
411 6         140 });
412            
413 6 50       79 return unless $ok;
414 6         53 INFO "update group $group set members to $new_user_list";
415 6         6527 return $new_user_list;
416             }
417              
418              
419             sub grant
420             {
421 17     17 1 48 my($self, $group, $action, $resource) = @_;
422            
423 17         62 $group = lc $group;
424              
425 17 100 100     180 unless($group && (defined $groupUser{$group} || defined $all_users{$group}))
      33        
426             {
427 1         7 WARN "Group (or user) $group does not exist";
428 1         844 return 0;
429             }
430              
431 16         96 $resource =~ s{^/?}{/};
432              
433 16 50       98 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         68 my $filename = $self->global_config->resource_file;
440              
441             my $ok = $self->lock_and_update_file($filename, sub {
442 41     41   226349 use autodie;
  41         89  
  41         253  
443 16     16   31 my($fh) = @_;
444              
445 16         39 my $buffer = '';
446            
447 16         252 while(! eof $fh)
448             {
449 28         60 my $line = <$fh>;
450 28         43 chomp $line;
451 28         109 $buffer .= "$line\n";
452             }
453 16         97 $buffer .= "$resource ($action) : $group\n";
454 16         49 $buffer;
455 16         298 });
456            
457 16 50       155 return 0 unless $ok;
458 16         145 INFO "grant $group $action $resource";
459 16         9708 return 1;
460             }
461              
462              
463             sub revoke
464             {
465 15     15 1 43 my($self, $group, $action, $resource) = @_;
466            
467 15         45 $group = lc $group;
468              
469 15 100 100     168 unless($group && (defined $groupUser{$group} || defined $all_users{$group}))
      33        
470             {
471 1         11 WARN "Group (or user) $group does not exist";
472 1         1711 return 0;
473             }
474              
475 14 100       94 $resource = '/' . $resource unless $resource =~ /\//;
476              
477 14 100       78 unless($resourceActionGroup{$resource}->{$action}->{$group})
478             {
479 1         11 WARN "Group (or user) $group not authorized to $action on $resource";
480 1         1313 return 0;
481             }
482            
483 13         62 my $filename = $self->global_config->resource_file;
484              
485             my $ok = $self->lock_and_update_file($filename, sub {
486 41     41   216627 use autodie;
  41         97  
  41         241  
487 13     13   30 my($fh) = @_;
488              
489 13         28 my $buffer = '';
490 13         177 while(! eof $fh)
491             {
492 44         109 my $line = <$fh>;
493 44         66 chomp $line;
494 44 50 100     559 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         62 my(@groups) = grep { $_ ne $group } split /,/, $3;
  13         55  
501 13 50       93 $buffer .= "$resource ($action) : " . join(',', @groups) . "\n"
502             if @groups > 0;
503             }
504             else
505             {
506 31         158 $buffer .= "$line\n";
507             }
508             }
509 13         50 $buffer;
510 13         259 });
511            
512 13 50       170 return 0 unless $ok;
513 13         106 INFO "revoke $group $action $resource";
514 13         10289 return 1;
515             }
516              
517              
518             sub granted
519             {
520 3     3 1 8 my($self) = @_;
521            
522 3         17 my $filename = $self->global_config->resource_file;
523            
524 3         30 my @granted_list;
525            
526             my $ok = $self->lock_and_read_file($filename, sub {
527 41     41   220644 use autodie;
  41         98  
  41         287  
528 3     3   6 my($fh) = @_;
529            
530 3         65 while(! eof $fh)
531             {
532 10         28 my $line = <$fh>;
533 10 50       32 next if $line =~ /^#/;
534 10 100       149 push @granted_list, "$1 ($2): $3"
535             if $line =~ m{^\s*(.*?)\s*\((.*?)\)\s*:\s*(.*?)\s*$};
536             }
537 3         37 });
538            
539 3         40 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.35
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