Revision 4 (by moose, 2006/07/10 14:36:31) weee
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;