package SVNACL;
use base SVNACL::Base;
use SVNACL::Group;
use SVNACL::User;
use SVNACL::Repository;
use SVNACL::Repository::Path;
=pod
load svnaccess and svnusers, one is a ini format, the other a uname:pwhash format.K
# svnaccess
[/] <-- root
# repos
[repo-name:/path]
user = access (rw)
@group = access
* = access
# groups
[groups]
group = list, of, users, @or, @other, @groups
=cut
use strict;
use warnings;
use File::Spec::Functions;
use Digest;
our @SUPPORTED_DIGESTS = qw/Crypt MD5 SHA-1/;
sub supported_digests {
return @SUPPORTED_DIGESTS;
}
sub new {
my $self = shift->SUPER::new(@_);
$self->{digest} ||= 'Crypt'; # I support Crypt, MD5 and SHA-1
$self->{svn_root} ||= catdir('/', 'var', 'svn');
$self->{svn_conf} ||= catdir($self->{svn_root}, 'conf');
$self->{svnusers} ||= 'svnusers';
$self->{svnaccess} ||= 'svnaccess';
$self->{svnuserdata} ||= 'svnuserdata';
$self->{svn_parent} ||= catdir($self->{svn_root}, 'repos');
$self->{svn_repos} ||= [];
$self->{svnusers_path} ||= catfile($self->{svn_conf}, $self->{svnusers});
$self->{svnuserdata_path} ||= catfile($self->{svn_conf}, $self->{svnuserdata});
$self->{svnaccess_path} ||= catfile($self->{svn_conf}, $self->{svnaccess});
$self->{users} = {};
$self->{groups} = {};
if(!scalar @{$self->{svn_repos}} && -d $self->{svn_parent}) {
local *D;
if(opendir D, $self->{svn_parent}) {
for(readdir D) {
next if /^\./;
push @{$self->{svn_repos}}, $_;
}
}
close D;
}
if(!$self->_load_users() || !$self->_load_access()) { # depend on || short circuit, so load_acces isnt called if load_users failed.
die $self->{error} if exists $self->{error};
return $self;
}
$self->{'acl_loaded'} = 1;
# setup the default svnaccess stuff.
if(!$self->group('admin')) {
$self->newgroup('admin');
}
if(!$self->repository('/')) {
my $repo = $self->newrepository('/');
my $path = $repo->newpath('/');
my $admin = $self->group('admin');
$path->addgroup($admin, 'rw');
}
for(@{$self->{svn_repos}}) {
# print "repo: $_\n";
if(!$self->repository($_)) {
# print "!$_\n";
my $repo = $self->newrepository($_);
my $path = $repo->newpath("/");
my $admin = $self->newgroup('admin-' . $repo->name);
$admin->addgroup($self->group('admin'));
$path->addgroup($admin, 'rw');
}
}
return $self;
}
sub destroy {
my $self = shift;
if($self->{'acl_loaded'} == 1) {
$self->_save_users();
$self->_save_access();
if($self->{error}) {
die "failed to save Subversion ACL data: " . $self->{error} . "\n";
}
}
$self->SUPER::destroy();
}
sub repos {
return shift->{svn_repos};
}
sub parent_path {
return shift->{svn_parent};
}
sub user {
my $self = shift;
my $name = shift;
return $self->{users}{$name} if exists $self->{users}{$name};
return;
}
sub group {
my $self = shift;
my $name = shift;
return $self->{groups}{$name} if exists $self->{groups}{$name};
return;
}
sub repository {
my $self = shift;
my $repo = shift;
return $self->{repos}{$repo} if exists $self->{repos}{$repo};
return;
}
sub users {
return values %{shift->{users}};
}
sub groups {
return values %{shift->{groups}};
}
sub repositories {
return values %{shift->{repos}};
}
sub newuser {
my $self = shift;
my $name = shift;
my $pass = shift;
my $email = shift;
return if exists $self->{users}{$name};
my $user = new SVNACL::User(acl => $self, name => $name, digest => $self->{digest});
$user->set_password($pass);
$user->set_email($email);
$self->adduser($user);
return $user;
}
sub newgroup {
my $self = shift;
my $name = shift;
return if exists $self->{groups}{$name};
my $group = new SVNACL::Group(acl => $self, name => $name);
$self->addgroup($group);
return $group;
}
sub newrepository {
my $self = shift;
my $name = shift;
return if exists $self->{repos}{$name};
my $repo = new SVNACL::Repository(acl => $self, name => $name);
$self->addrepository($repo);
return $repo;
}
sub adduser {
my $self = shift;
my $user = shift;
$self->{users}{$user->name} = $user if !exists $self->{users}{$user->name};
}
sub addgroup {
my $self = shift;
my $group = shift;
$self->{groups}{$group->name} = $group if !exists $self->{groups}{$group->name};
}
sub addrepository {
my $self = shift;
my $repo = shift;
if(!exists $self->{repos}{$repo->name}) {
$repo->{acl} = $self;
$self->{repos}{$repo->name} = $repo;
}
}
sub deluser {
my $self = shift;
my $user = shift;
delete $self->{users}{$user->name} if exists $self->{users}{$user->name};
}
sub delgroup {
my $self = shift;
my $group = shift;
delete $self->{groups}{$group->name} if exists $self->{groups}{$group->name};
}
sub delrepository {
my $self = shift;
my $repo = shift;
delete $self->{repos}{$repo->name} if exists $self->{repos}{$repo->name};
}
=pod
[/] group applies to the tree
=cut
sub pathaccess {
my $self = shift;
my $path = shift;
my $user = shift;
my $how = shift;
# check $how(rwa) $user can access $path
}
sub repositoryaccess {
my $self = shift;
my $repo = shift;
my $user = shift;
my $how = shift;
# check $how(rwa) $user can access $repo
}
sub admin {
my $self = shift;
my $user = shift; # User || Group
my $repo = shift;
my $path = shift;
my %hash = @_;
$repo = ref $repo ? $repo : $self->repository($repo);
if( defined $path ) {
$path = ref $path ? $path : $self->repository( $path );
return $path->admin( $user );
}
else {
return $repo->admin( $user );
}
}
sub _groupaccess {
my $self = shift;
my $repo = shift;
my $path = shift;
my $user = shift;
my $group = shift;
$group = ((ref($group) eq 'SVNACL::Group') ? $group->name : $group);
$user = ((ref($user) eq 'SVNACL::User') ? $user->name : $user);
my $gp = $self->group($group);
return if !$gp;
my @list;
if($gp->user($user)) {
if($path->name eq '/') {
push @list, $repo;
}
else {
push @list, $path;
}
}
for $gp ($gp->groups) {
push @list, $self->_groupaccess($repo, $path, $user, $gp);
}
return @list;
}
sub useraccess {
my $self = shift;
my $user = shift;
my $access = shift;
my $name = ((ref($user) eq 'SVNACL::User') ? $user->name : $user);
# find all things that user $name has access to
my @list;
for my $repo ( $self->repositories ) {
# print "repo:".$repo->name."\n";
PATH:
for my $path ( $repo->paths ) {
# print "path:".$path->name."\n";
for my $u ( $path->users ) {
# print "user:$u\n";
if($name eq $u) {
if($path->name eq '/') {
push @list, $repo;
}
else {
push @list, $path;
}
next PATH;
}
}
for my $g ( $path->groups ) {
push @list, $self->_groupaccess($repo, $path, $name, $g);
}
}
}
return @list;
}
sub _load_users {
my $self = shift;
local *F;
if(!open F, $self->{'svnusers_path'}) {
$self->{error} = "failed to open svnusers file: $!";
return;
}
my @users = <F>;
close F;
# load user data: this is an extension to the authz system.
if(!open F, $self->{'svnuserdata_path'}) {
$self->{error} = "failed to open svnuserdata file: $!";
return;
}
my @data = <F>;
close F;
my %data;
for(@data) {
chomp; next if /^\s*$/;
# user:email:reserved0..N
my @cols = qw/email/;
my @fields = split ':';
my $name = shift @fields;
$data{$name} = { map { shift @cols => $_ } @fields };
}
my $u = new SVNACL::User(acl => $self, name => '*', hash => 'foobar', digest => $self->{digest});
$self->adduser($u);
for(@users) {
chomp; next if /^\s*$/;
my ($user, $hash) = split ':';
$user = new SVNACL::User(acl => $self, name => $user, hash => $hash, digest => $self->{digest}, profile => $data{$user});
$self->adduser($user);
}
return 1;
}
sub _load_access {
my $self = shift;
my %hash;
local *F;
if(!open F, $self->{'svnaccess_path'}) {
$self->{error} = "failed to open svnaccess file: $!";
return;
}
my @lines = <F>;
close F;
my $repo;
my $path;
my $groups = 0;
my @lines2 = @lines;
for(@lines2) {
chomp;
next if /^\s*$/;
if(/^\s*\[(.*)\]/) {
if($1 eq 'groups') {
$groups = 1;
}
else {
$groups = 0;
}
} elsif(/^\s*([^\s=]+)\s*=\s*(.*)\s*$/) {
if($groups) {
my $key = $1;
# print "group:$1, u:$2\n";
my @users = split /\s*,\s*/, $2;
my $group = $self->group($key);
$group = $self->newgroup($key) if !defined $group;
if($group) {
for(sort @users) {
# print "group:user: $_\n";
if(s/^@(.*)$/$1/) {
my $g = $self->group($_);
$group->addgroup($g) if $g;
}
else {
my $u = $self->user($_);
$group->adduser($u) if $u;
}
}
}
}
}
}
$groups = 0;
for(@lines) {
chomp;
next if /^\s*$/;
if(/^\s*\[([^\]]+)\]/) {
my $sect = $1;
if($sect eq 'groups') {
$groups = 1;
next;
}
else {
$groups = 0;
}
($repo,$path) = split ':', $sect;
$path = '/' if !defined $path;
my $r = $self->repository($repo);
# print "repo:$repo -- $r\n";
if(!defined $r) {
# print "new repo...\n";
$r = $self->newrepository($repo);
}
# print "repo:$repo, path:$path\n";
if($r) {
my $agn = canonpath($path); # clean up path.
$agn =~ s|/|-|g;
$agn = substr($agn, 1, length($agn)-1) if index($agn, '-') == 0; # strip preceding - if it exists
$agn = substr($agn, 0, length($agn)-2) if rindex($agn, '-') == length($agn)-1; # strip trailing - if it exists
my $agnn = 'admin';
$agnn .= "-$repo" if $repo ne '/';
$agnn .= "-$agn" if $agn ne '';
my $g = $self->group($agnn);
if(!$g) {
$g = $self->newgroup($agnn);
my @l = split('-', $agnn);
while( my $j = scalar(@l) ) {
pop @l;
my $pg = join('-', @l);
}
#my $pg = join('-', (split('-', $agnn))[0..-2]);
$g->addgroup( $self->group('admin-' . $repo) );
}
$path = $r->newpath($path);
$path->addgroup($g, 'rw');
$repo = $r;
# print "repo:".$repo->name." path:".$path->name."\n";
}
}
elsif(/^\s*\@\s*([^\s=]+)\s*=\s*(.*)\s*$/) {
my $key = $1;
my $val = $2;
my $group = $self->group($key);
if($group) {
$path->addgroup($group, $val);
}
}
elsif(/^\s*([^\s=]+)\s*=\s*(.*)\s*$/) {
#print "name:$1 val:$2\n";
if(!$groups) {
my $key = $1;
my $val = $2;
my $user = $self->user($key);
if($user) {
# print "got user: $user\n";
$user = $path->adduser($user, $val);
} else {
# print "!got user: $key\n";
}
}
else {
#nothing;
#print "$1:$2\n";
}
}
}
my $g = $self->group('users') || $self->newgroup('users');
for my $u ($self->users) {
next if $u->name =~ /\*/;
$g->adduser($u);
}
return 1;
}
sub pu {
my $self = shift;
my $name = shift;
my $user = $self->user($name);
#print "pu:$name\n<br>";
return join(', ', map { "@".$_->name } $user->groups, $user->users);
}
sub pg {
my $self = shift;
my $user = $self->group(shift());
return join(', ', map { "@".$_->name } $user->groups);
}
sub _save_access {
my $self = shift;
local *F;
if(!open F, ">".$self->{'svnaccess_path'}) {
$self->{error} = "failed to open svnaccess file: $!";
die $self->{error};
return;
}
print F "[groups]\n";
for my $g (sort { $a->name cmp $b->name } $self->groups) { # sort { $a->name cmp $b->name }
print F $g->name . " = ";
my @items = map { "@".$_->name } $g->groups;
push @items, map { $_->name } $g->users;
print F join(', ', @items) . "\n";
}
print F "\n";
my $root = $self->repository("/");
$self->delrepository($root);
my $path = $root->path('/');
print F "[/]\n";
for my $g ($path->groups) {
print F "@". $g . " = " . $path->group($g) . "\n";
}
for my $u ($path->users) {
print F $u . " = " . $path->user($u) . "\n";
}
print F "\n";
for my $r (sort { $a->name cmp $b->name } $self->repositories) {
for my $p (sort { $a->name cmp $b->name } $r->paths) {
print F "[" . $r->name . ":" . $p->name . "]\n";
for my $g (sort { $a cmp $b } $p->groups) {
print F "@". $g . " = " . $p->group($g) . "\n";
}
for my $u (sort { $a cmp $b } $p->users) {
print F $u . " = " . $p->user($u) . "\n";
}
print F "\n";
}
}
# print "save access\n";
return 1;
}
sub _save_users {
my $self = shift;
local *F;
if(!open F, ">".$self->{'svnusers_path'}) {
$self->{error} = "failed to open svnaccess file: $!";
die $self->{error};
return 0;
}
for(sort { $a->name cmp $b->name } $self->users()) {
next if $_->name eq '*';
print F $_->name() . ":" . $_->hash() . "\n";
}
close F;
# print "save users\n";
return 1;
}
1;