File Coverage

blib/lib/SVN/Access.pm
Criterion Covered Total %
statement 189 235 80.4
branch 105 136 77.2
condition 14 21 66.6
subroutine 24 25 96.0
pod 17 18 94.4
total 349 435 80.2


line stmt bran cond sub pod time code
1             package SVN::Access;
2              
3 1     1   13231 use SVN::Access::Group;
  1         2  
  1         20  
4 1     1   301 use SVN::Access::Resource;
  1         2  
  1         23  
5              
6 1     1   497 use open ':encoding(utf8)';
  1         793  
  1         4  
7              
8 1     1   8415 use 5.006001;
  1         3  
9 1     1   3 use strict;
  1         0  
  1         20  
10 1     1   3 use warnings;
  1         2  
  1         2543  
11              
12             our $VERSION = '0.11';
13              
14             sub new {
15 11     11 1 3141 my ($class, %attr) = @_;
16 11         22 my $self = bless(\%attr, $class);
17            
18             # it's important that we have this.
19 11 50       28 die "No ACL file specified!" unless $attr{acl_file};
20              
21 11 100       100 if (-e $attr{acl_file}) {
22             # parse the file in.
23 10         19 $self->parse_acl;
24             } else {
25             # empty acl.
26 1         3 $attr{acl} = {};
27             }
28 11         27 return $self;
29             }
30              
31             sub parse_acl {
32 10     10 0 11 my ($self) = @_;
33 10 50       169 open(ACL, '<', $self->{acl_file}) or die "Can't open SVN Access file " . $self->{acl_file} . ": $!";
34 10         296 my $current_resource;
35             my $statement;
36 10         112 while (my $line = ) {
37             # ignore comments (properly defined)
38 158 100       1212 next if $line =~ /^#/;
39 154 100       292 $line =~ s/\s*#.*$// unless $self->{pedantic};
40              
41             # get rid of trailing whitespace.
42 154         639 $line =~ s/[\s\r\n]+$//;
43 154 100       325 next unless $line;
44              
45             # handle line continuations
46 116 100       270 if ($line =~ /^\s+(.+)$/) {
47 4         13 $statement .= " $1";
48             } else {
49 112         128 $statement = $line;
50             }
51              
52             # lookahead and see if the next line is a line continuation
53 116         578 my $pos = tell(ACL);
54 116         325 my $nextline = ;
55 116         929 seek(ACL, $pos, 0); # rewind the filehandle to where we were.
56 116 100 100     445 if ($nextline && $nextline =~ /^[ \t]+\S/) {
57 4         39 next;
58             }
59              
60 112 50       138 next unless $statement;
61              
62 112 100       241 if ($statement =~ /^\[\s*(.+?)\s*\]$/) {
63             # this statement is defining a new resource.
64 37         85 $current_resource = $1;
65 37 100       90 unless ($current_resource =~ /^(?:groups|aliases)$/) {
66 22         38 $self->add_resource($current_resource);
67             }
68             } else {
69             # both groups and resources need this parsed.
70 75         326 my ($k, $v) = $statement =~ /^(.+?)\s*=\s*(.*?)$/;
71              
72             # if the previous split didn't work, there's a syntax error
73 75 100       117 unless ($k) {
74 2         10 warn "Unrecognized line $statement\n";
75 2         15 next;
76             }
77              
78 73 100       112 if ($current_resource eq "groups") {
    100          
79             # this is a group
80 23         90 $self->add_group($k, split(/\s*,\s*/, $v));
81             } elsif ($current_resource eq "aliases") {
82             # aliases are simple k=v, so let's just store them in ourselves.
83 7         19 $self->add_alias($k, $v);
84             } else {
85             # this is a generic resource
86 43 100       99 unless ($v =~ /^[rw\s]*$/) {
87 1         15 warn "Invalid character in authz rule $v\n";
88             }
89 43 50       84 if (my $resource = $self->resource($current_resource)) {
90 43         75 $resource->authorize($k => $v);
91             } else {
92 0         0 $self->add_resource($current_resource, $k, $v);
93             }
94             }
95             }
96              
97 110         1283 $statement = undef;
98             }
99            
100             # make sure this isn't empty.
101 10 100       23 unless (ref($self->{acl}->{aliases}) eq "HASH") {
102 5         8 $self->{acl}->{aliases} = {};
103             }
104            
105 10         73 close (ACL);
106             }
107              
108             sub verify_acl {
109 8     8 1 862 my ($self) = @_;
110              
111             # Check for references to undefined groups (Thanks Jesse!)
112 8         9 my (%groups, @errors);
113 8 100       12 if ($self->groups) {
114             # gather groups first, in case there are forward refs
115 5         7 foreach my $group ($self->groups) {
116 9         20 $groups{$group->name}++;
117             # check for loops
118 9     3   41 local $SIG{__WARN__} = sub { push @errors, @_; };
  3         6  
119 9         20 my @g = $self->resolve('@' . $group->name);
120             }
121 5         10 foreach my $group ($self->groups) {
122 9         14 foreach my $k ($group->members) {
123 25 100       58 if ( $k =~ /^@(.*)/ ) {
    100          
124 5 100       13 unless ( $groups{$1} ) {
125 1         3 push(@errors, "[error] An authz rule (" . $group->name. ") refers to group '$1', which is undefined");
126             }
127             } elsif ( $k =~ /^&(.*)/ ) {
128 2 100       3 unless ( $self->aliases->{$1} ) {
129 1         4 push(@errors, "[error] An authz rule (" . $group->name . ") refers to alias '$1', which is undefined");
130             }
131             }
132             }
133             }
134             }
135              
136 8         12 foreach my $resource ($self->resources) {
137 22 100 66     88 if (defined($resource) && $resource->authorized) {
138 21         16 foreach my $k (keys %{$resource->authorized}) {
  21         35  
139 41 100       346 if ( $k =~ /^@(.*)/ ) {
    100          
140 9 100       28 unless ( $groups{$1} ) {
141 3         9 push(@errors, "[error] An authz rule (" . $resource->name . ") refers to group '\@$1', which is undefined");
142             }
143             } elsif ( $k =~ /^&(.*)/ ) {
144 2 100       4 unless ( $self->aliases->{$1} ) {
145 1         3 push(@errors, "[error] An authz rule (" . $resource->name . ") refers to alias '\&$1', which is undefined");
146             }
147             }
148             }
149             }
150             }
151              
152 8         23 chomp @errors;
153 8 100       45 return scalar(@errors) ? join("\n", @errors) : undef;
154             }
155              
156             sub write_acl {
157 6     6 1 10 my ($self, $out) = @_;
158              
159             # verify the ACL has no errors before writing it out
160 6 100       14 if (my $error = $self->verify_acl) {
161 1         9 die "Error found in ACL:\n$error\n";
162             }
163              
164 5 50 33     23 if (ref \$out eq "GLOB" or ref $out) {
165 0         0 *ACL = $out;
166             }
167             else {
168 5 50       11 $out = $self->{acl_file} unless $out;
169 5 50       329 open (ACL, '>', $out) or warn "Can't open ACL file " . $out . " for writing: $!\n";
170             }
171            
172             # aliases now supported!
173 5 100       196 if (scalar(keys %{$self->aliases})) {
  5         11  
174 2         9 print ACL "[aliases]\n";
175 2         2 foreach my $alias (keys %{$self->aliases}) {
  2         4  
176 2         6 print ACL $alias . " = " . $self->aliases->{$alias} . "\n";
177             }
178 2         6 print ACL "\n";
179             }
180            
181             # groups now second to aliases
182 5 100       8 if ($self->groups) {
183 3         8 print ACL "[groups]\n";
184 3         4 foreach my $group ($self->groups) {
185 3         7 print ACL $group->name . " = " . join(', ', $group->members) . "\n";
186             }
187 3         5 print ACL "\n";
188             }
189            
190 5         8 foreach my $resource ($self->resources) {
191 14 100 66     41 if (defined($resource) && $resource->authorized) {
192 13         25 print ACL "[" . $resource->name . "]\n";
193 13         24 while (my ($k, $v) = (each %{$resource->authorized})) {
  36         68  
194 23         285 print ACL "$k = $v\n";
195             }
196 13         84 print ACL "\n";
197             }
198             }
199            
200 5         188 close(ACL);
201             }
202              
203             sub write_pretty {
204 0     0 1 0 my ($self) = @_;
205              
206             # verify the ACL has no errors before writing it out
207 0 0       0 if (my $error = $self->verify_acl) {
208 0         0 die "Error found in ACL:\n$error\n";
209             }
210              
211 0         0 my $max_len = 0;
212              
213             # Compile a list of names that will appear on the left side
214 0         0 my @names;
215 0 0       0 if (scalar(keys %{$self->aliases})) {
  0         0  
216 0         0 foreach my $alias (keys %{$self->aliases}) {
  0         0  
217 0         0 push(@names, $alias);
218             }
219             }
220            
221 0 0       0 if ($self->groups) {
222 0         0 for ($self->groups) {
223 0         0 push(@names, $_->name);
224             }
225             }
226 0 0       0 if ($self->resources) {
227 0         0 for ($self->resources) {
228 0         0 push(@names, keys(%{$_->authorized}));
  0         0  
229             }
230             }
231              
232             # Go through that list looking for the longest name
233 0         0 for (@names) {
234 0 0       0 $max_len = length($_) >= $max_len ? length($_) : $max_len;
235             }
236              
237 0 0       0 open (ACL, '>', $self->{acl_file}) or warn "Can't open ACL file " . $self->{acl_file} . " for writing: $!\n";
238            
239             # aliases now fully supported!
240 0 0       0 if (scalar(keys %{$self->aliases})) {
  0         0  
241 0         0 print ACL "[aliases]\n";
242 0         0 foreach my $alias (keys %{$self->aliases}) {
  0         0  
243 0         0 print ACL $alias . " " x ($max_len - length($alias)) . " = " . $self->aliases->{$alias} . "\n";
244             }
245 0         0 print "\n";
246             }
247            
248             # groups now second?
249 0 0       0 if ($self->groups) {
250 0         0 print ACL "[groups]\n";
251 0         0 foreach my $group ($self->groups) {
252 0         0 print ACL $group->name . " " x ($max_len - length($group->name)) . " = " . join(', ', $group->members) . "\n";
253             }
254 0         0 print "\n";
255             }
256            
257 0         0 foreach my $resource ($self->resources) {
258 0 0 0     0 if (defined($resource) && $resource->authorized) {
259 0         0 print ACL "[" . $resource->name . "]\n";
260 0         0 while (my ($k, $v) = (each %{$resource->authorized})) {
  0         0  
261 0         0 print ACL "$k" . " " x ($max_len - length($k)) . " = $v\n";
262             }
263 0         0 print ACL "\n";
264             }
265             }
266 0         0 close(ACL);
267             }
268              
269             sub add_alias {
270 9     9 1 13 my ($self, $alias_name, $aliased) = @_;
271 9         32 $self->{acl}->{aliases}->{$alias_name} = $aliased;
272             }
273              
274             sub remove_alias {
275 2     2 1 7 my ($self, $alias_name) = @_;
276 2         5 delete $self->{acl}->{aliases}->{$alias_name};
277             }
278              
279             sub alias {
280 7     7 1 10 my ($self, $alias_name) = @_;
281 7 100       14 if (exists ($self->{acl}->{aliases}->{$alias_name})) {
282 5         18 return $self->{acl}->{aliases}->{$alias_name};
283             }
284 2         5 return undef;
285             }
286              
287             sub aliases {
288 14     14 1 16 my ($self) = @_;
289             # give em something if we got nothing!
290 14 100       28 unless (ref($self->{acl}->{aliases}) eq "HASH") {
291 1         2 $self->{acl}->{aliases} = {};
292             }
293 14         49 return $self->{acl}->{aliases};
294             }
295              
296             sub add_resource {
297 30     30 1 55 my ($self, $resource_name, @access) = @_;
298 30 100       51 if ($resource_name eq "name") {
299 1         2 $resource_name = shift(@access);
300             }
301            
302 30         27 my @acl;
303 30         45 foreach my $entry (@access) {
304 16 100       22 next if $entry eq "authorized";
305            
306 15 100       27 if (ref($entry) eq "HASH") {
    50          
307             # unpack the hashref to a list.
308 1         3 foreach my $key (keys %$entry) {
309 1         3 push(@acl, $key, $entry->{$key});
310             }
311             } elsif (ref($entry) eq "ARRAY") {
312 0         0 push(@acl, @$entry);
313             } else {
314 14         28 push(@acl, $entry);
315             }
316             }
317            
318 30 50       47 if ($self->resource($resource_name)) {
    50          
319 0         0 die "Can't add new resource $resource_name: resource already exists!\n";
320             } elsif ($resource_name !~ /^(?:\S+\:)?\/.*$/) { # Thanks Matt
321 0         0 die "Invalid resource format in $resource_name! (format 'repo:/path')!\n";
322             } else {
323 30         82 my $resource = SVN::Access::Resource->new(
324             name => $resource_name,
325             authorized => \@acl,
326             );
327 30         24 push(@{$self->{acl}->{resources}}, $resource);
  30         54  
328 30         50 return $resource;
329             }
330             }
331              
332             sub remove_resource {
333 9     9 1 316 my ($self, $resource_name) = @_;
334 9         7 my @resources;
335 9         13 foreach my $resource ($self->resources) {
336 38 100       61 push(@resources, $resource) unless $resource->name eq $resource_name;
337             }
338 9 100       26 $self->{acl}->{resources} = scalar(@resources) ? \@resources : undef;
339             }
340              
341             sub resources {
342 105     105 1 81 my ($self) = @_;
343 105 100       177 if (ref($self->{acl}->{resources}) eq "ARRAY") {
344 91         64 return (@{$self->{acl}->{resources}});
  91         186  
345             } else {
346 14         22 return (undef);
347             }
348             }
349              
350             sub resource {
351 82     82 1 109 my ($self, $resource_name) = @_;
352 82         101 foreach my $resource ($self->resources) {
353 216 100 100     489 return $resource if defined($resource) && $resource->name eq $resource_name;
354             }
355 30         140 return undef;
356             }
357              
358             sub add_group {
359 24     24 1 56 my ($self, $group_name, @initial_members) = @_;
360              
361             # get rid of the @ symbol.
362 24 100       63 $group_name =~ s/\@//g unless $self->{pedantic};
363              
364 24 50       35 if ($self->group($group_name)) {
365 0         0 die "Can't add new group $group_name: group already exists!\n";
366             } else {
367 24         66 my $group = SVN::Access::Group->new(
368             name => $group_name,
369             members => \@initial_members,
370             );
371 24         22 push(@{$self->{acl}->{groups}}, $group);
  24         45  
372 24         30 return $group;
373             }
374             }
375              
376             sub remove_group {
377 1     1 1 1 my ($self, $group_name) = @_;
378 1         2 my @groups;
379              
380             # get rid of the @ symbol.
381 1         2 $group_name =~ s/\@//g;
382 1         4 foreach my $group ($self->groups) {
383 1 50       3 push(@groups, $group) unless $group->name eq $group_name;
384             }
385              
386 1 50       5 $self->{acl}->{groups} = scalar(@groups) ? \@groups : undef;
387             }
388              
389             sub groups {
390 105     105 1 78 my ($self) = @_;
391 105 100       165 if (ref($self->{acl}->{groups}) eq "ARRAY") {
392 89         67 return (@{$self->{acl}->{groups}});
  89         169  
393             } else {
394 16         36 return (undef);
395             }
396             }
397              
398             sub group {
399 77     77 1 2939 my ($self, $group_name) = @_;
400 77         96 foreach my $group ($self->groups) {
401 142 100 100     317 return $group if defined($group) && $group->name eq $group_name;
402             }
403 25         40 return undef;
404             }
405              
406             sub resolve {
407 67     67 1 1476 my $self = shift;
408 67         42 my @res;
409 67 100       104 my $seen = (ref $_[$#_] eq "ARRAY" ? pop @_ : []);
410              
411 67         89 foreach my $e (@_) {
412 67 100       140 if ($e =~ /^\@(.+)/) {
    100          
413             # check for loops
414 28 100       57 if (grep($_ eq $e, @$seen)) {
415 6         45 warn "Error: group loop detected ",join(", ", @$seen, $e),"\n";
416 6         24 return undef;
417             }
418 22         28 push @$seen, $e;
419 22 100       29 push @res, map $self->resolve($_, $seen),
420             $self->group($1)->members()
421             if $self->group($1);
422 22         35 pop @$seen;
423             } elsif ($e =~ /^\&(.+)/) {
424 3 100       8 push @res, map $self->resolve($_), $self->alias($1)
425             if $self->alias($1);
426             } else {
427 36         38 push @res, $e;
428             }
429             }
430              
431 61         135 return @res;
432             }
433              
434             1;
435             __END__