Adds a branch for unlimited project hierarchy.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/work@2148 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2008-12-19 20:51:35 +00:00
parent 7ae224f5c2
commit 04fea923a4
1272 changed files with 119640 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
package Apache::Authn::Redmine;
=head1 Apache::Authn::Redmine
Redmine - a mod_perl module to authenticate webdav subversion users
against redmine database
=head1 SYNOPSIS
This module allow anonymous users to browse public project and
registred users to browse and commit their project. Authentication is
done against the redmine database or the LDAP configured in redmine.
This method is far simpler than the one with pam_* and works with all
database without an hassle but you need to have apache/mod_perl on the
svn server.
=head1 INSTALLATION
For this to automagically work, you need to have a recent reposman.rb
(after r860) and if you already use reposman, read the last section to
migrate.
Sorry ruby users but you need some perl modules, at least mod_perl2,
DBI and DBD::mysql (or the DBD driver for you database as it should
work on allmost all databases).
On debian/ubuntu you must do :
aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
If your Redmine users use LDAP authentication, you will also need
Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
=head1 CONFIGURATION
## This module has to be in your perl path
## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
PerlLoadModule Apache::Authn::Redmine
<Location /svn>
DAV svn
SVNParentPath "/var/svn"
AuthType Basic
AuthName redmine
Require valid-user
PerlAccessHandler Apache::Authn::Redmine::access_handler
PerlAuthenHandler Apache::Authn::Redmine::authen_handler
## for mysql
RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
## for postgres
# RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
RedmineDbUser "redmine"
RedmineDbPass "password"
## Optional where clause (fulltext search would be slow and
## database dependant).
# RedmineDbWhereClause "and members.role_id IN (1,2)"
## Optional credentials cache size
# RedmineCacheCredsMax 50
</Location>
To be able to browse repository inside redmine, you must add something
like that :
<Location /svn-private>
DAV svn
SVNParentPath "/var/svn"
Order deny,allow
Deny from all
# only allow reading orders
<Limit GET PROPFIND OPTIONS REPORT>
Allow from redmine.server.ip
</Limit>
</Location>
and you will have to use this reposman.rb command line to create repository :
reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
=head1 MIGRATION FROM OLDER RELEASES
If you use an older reposman.rb (r860 or before), you need to change
rights on repositories to allow the apache user to read and write
S<them :>
sudo chown -R www-data /var/svn/*
sudo chmod -R u+w /var/svn/*
And you need to upgrade at least reposman.rb (after r860).
=cut
use strict;
use warnings FATAL => 'all', NONFATAL => 'redefine';
use DBI;
use Digest::SHA1;
# optional module for LDAP authentication
my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
use Apache2::Const qw(:common :override :cmd_how);
use APR::Pool ();
use APR::Table ();
# use Apache2::Directive qw();
my @directives = (
{
name => 'RedmineDSN',
req_override => OR_AUTHCFG,
args_how => TAKE1,
errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
},
{
name => 'RedmineDbUser',
req_override => OR_AUTHCFG,
args_how => TAKE1,
},
{
name => 'RedmineDbPass',
req_override => OR_AUTHCFG,
args_how => TAKE1,
},
{
name => 'RedmineDbWhereClause',
req_override => OR_AUTHCFG,
args_how => TAKE1,
},
{
name => 'RedmineCacheCredsMax',
req_override => OR_AUTHCFG,
args_how => TAKE1,
errmsg => 'RedmineCacheCredsMax must be decimal number',
},
);
sub RedmineDSN {
my ($self, $parms, $arg) = @_;
$self->{RedmineDSN} = $arg;
my $query = "SELECT
hashed_password, auth_source_id, permissions
FROM members, projects, users, roles
WHERE
projects.id=members.project_id
AND users.id=members.user_id
AND roles.id=members.role_id
AND users.status=1
AND login=?
AND identifier=? ";
$self->{RedmineQuery} = trim($query);
}
sub RedmineDbUser { set_val('RedmineDbUser', @_); }
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
sub RedmineDbWhereClause {
my ($self, $parms, $arg) = @_;
$self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
}
sub RedmineCacheCredsMax {
my ($self, $parms, $arg) = @_;
if ($arg) {
$self->{RedmineCachePool} = APR::Pool->new;
$self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
$self->{RedmineCacheCredsCount} = 0;
$self->{RedmineCacheCredsMax} = $arg;
}
}
sub trim {
my $string = shift;
$string =~ s/\s{2,}/ /g;
return $string;
}
sub set_val {
my ($key, $self, $parms, $arg) = @_;
$self->{$key} = $arg;
}
Apache2::Module::add(__PACKAGE__, \@directives);
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
sub access_handler {
my $r = shift;
unless ($r->some_auth_required) {
$r->log_reason("No authentication has been configured");
return FORBIDDEN;
}
my $method = $r->method;
return OK unless defined $read_only_methods{$method};
my $project_id = get_project_identifier($r);
$r->set_handlers(PerlAuthenHandler => [\&OK])
if is_public_project($project_id, $r);
return OK
}
sub authen_handler {
my $r = shift;
my ($res, $redmine_pass) = $r->get_basic_auth_pw();
return $res unless $res == OK;
if (is_member($r->user, $redmine_pass, $r)) {
return OK;
} else {
$r->note_auth_failure();
return AUTH_REQUIRED;
}
}
sub is_public_project {
my $project_id = shift;
my $r = shift;
my $dbh = connect_database($r);
my $sth = $dbh->prepare(
"SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
);
$sth->execute($project_id);
my $ret = $sth->fetchrow_array ? 1 : 0;
$sth->finish();
$dbh->disconnect();
$ret;
}
# perhaps we should use repository right (other read right) to check public access.
# it could be faster BUT it doesn't work for the moment.
# sub is_public_project_by_file {
# my $project_id = shift;
# my $r = shift;
# my $tree = Apache2::Directive::conftree();
# my $node = $tree->lookup('Location', $r->location);
# my $hash = $node->as_hash;
# my $svnparentpath = $hash->{SVNParentPath};
# my $repos_path = $svnparentpath . "/" . $project_id;
# return 1 if (stat($repos_path))[2] & 00007;
# }
sub is_member {
my $redmine_user = shift;
my $redmine_pass = shift;
my $r = shift;
my $dbh = connect_database($r);
my $project_id = get_project_identifier($r);
my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
my $usrprojpass;
if ($cfg->{RedmineCacheCredsMax}) {
$usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
}
my $query = $cfg->{RedmineQuery};
my $sth = $dbh->prepare($query);
$sth->execute($redmine_user, $project_id);
my $ret;
while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
unless ($auth_source_id) {
my $method = $r->method;
if ($hashed_password eq $pass_digest && (defined $read_only_methods{$method} || $permissions =~ /:commit_access/) ) {
$ret = 1;
last;
}
} elsif ($CanUseLDAPAuth) {
my $sthldap = $dbh->prepare(
"SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
);
$sthldap->execute($auth_source_id);
while (my @rowldap = $sthldap->fetchrow_array) {
my $ldap = Authen::Simple::LDAP->new(
host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
port => $rowldap[1],
basedn => $rowldap[5],
binddn => $rowldap[3] ? $rowldap[3] : "",
bindpw => $rowldap[4] ? $rowldap[4] : "",
filter => "(".$rowldap[6]."=%s)"
);
$ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass));
}
$sthldap->finish();
}
}
$sth->finish();
$dbh->disconnect();
if ($cfg->{RedmineCacheCredsMax} and $ret) {
if (defined $usrprojpass) {
$cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
} else {
if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
$cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
$cfg->{RedmineCacheCredsCount}++;
} else {
$cfg->{RedmineCacheCreds}->clear();
$cfg->{RedmineCacheCredsCount} = 0;
}
}
}
$ret;
}
sub get_project_identifier {
my $r = shift;
my $location = $r->location;
my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
$identifier;
}
sub connect_database {
my $r = shift;
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
}
1;
@@ -0,0 +1,24 @@
/* ssh views */
CREATE OR REPLACE VIEW ssh_users as
select login as username, hashed_password as password
from users
where status = 1;
/* nss views */
CREATE OR REPLACE VIEW nss_groups AS
select identifier AS name, (id + 5000) AS gid, 'x' AS password
from projects;
CREATE OR REPLACE VIEW nss_users AS
select login AS username, CONCAT_WS(' ', firstname, lastname) as realname, (id + 5000) AS uid, 'x' AS password
from users
where status = 1;
CREATE OR REPLACE VIEW nss_grouplist AS
select (members.project_id + 5000) AS gid, users.login AS username
from users, members
where users.id = members.user_id
and users.status = 1;
+289
View File
@@ -0,0 +1,289 @@
#!/usr/bin/env ruby
# == Synopsis
#
# reposman: manages your repositories with Redmine
#
# == Usage
#
# reposman [OPTIONS...] -s [DIR] -r [HOST]
#
# Examples:
# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net --scm subversion
# reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git
#
# == Arguments (mandatory)
#
# -s, --svn-dir=DIR use DIR as base directory for svn repositories
# -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples:
# -r redmine.example.net
# -r http://redmine.example.net
# -r https://example.net/redmine
#
# == Options
#
# -o, --owner=OWNER owner of the repository. using the rails login
# allow user to browse the repository within
# Redmine even for private project. If you want to share repositories
# through Redmine.pm, you need to use the apache owner.
# --scm=SCM the kind of SCM repository you want to create (and register) in
# Redmine (default: Subversion). reposman is able to create Git
# and Subversion repositories. For all other kind (Bazaar,
# Darcs, Filesystem, Mercurial) you must specify a --command option
# -u, --url=URL the base url Redmine will use to access your
# repositories. This option is used to automatically
# register the repositories in Redmine. The project
# identifier will be appended to this url. Examples:
# -u https://example.net/svn
# -u file:///var/svn/
# if this option isn't set, reposman won't register
# the repositories in Redmine
# -c, --command=COMMAND use this command instead of "svnadmin create" to
# create a repository. This option can be used to
# create repositories other than subversion and git kind.
# This command override the default creation for git and subversion.
# -f, --force force repository creation even if the project
# repository is already declared in Redmine
# -t, --test only show what should be done
# -h, --help show help and exit
# -v, --verbose verbose
# -V, --version print version and exit
# -q, --quiet no log
#
# == References
#
# You can find more information on the redmine's wiki : http://www.redmine.org/wiki/redmine/HowTos
require 'getoptlong'
require 'rdoc/usage'
require 'soap/wsdlDriver'
require 'find'
require 'etc'
Version = "1.1"
SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem )
opts = GetoptLong.new(
['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT],
['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT],
['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT],
['--scm', GetoptLong::REQUIRED_ARGUMENT],
['--test', '-t', GetoptLong::NO_ARGUMENT],
['--force', '-f', GetoptLong::NO_ARGUMENT],
['--verbose', '-v', GetoptLong::NO_ARGUMENT],
['--version', '-V', GetoptLong::NO_ARGUMENT],
['--help' , '-h', GetoptLong::NO_ARGUMENT],
['--quiet' , '-q', GetoptLong::NO_ARGUMENT]
)
$verbose = 0
$quiet = false
$redmine_host = ''
$repos_base = ''
$svn_owner = 'root'
$use_groupid = true
$svn_url = false
$test = false
$force = false
$scm = 'Subversion'
def log(text, options={})
level = options[:level] || 0
puts text unless $quiet or level > $verbose
exit 1 if options[:exit]
end
def system_or_raise(command)
raise "\"#{command}\" failed" unless system command
end
module SCM
module Subversion
def self.create(path)
system_or_raise "svnadmin create #{path}"
end
end
module Git
def self.create(path)
Dir.mkdir path
Dir.chdir(path) do
system_or_raise "git --bare init --shared"
system_or_raise "git update-server-info"
end
end
end
end
begin
opts.each do |opt, arg|
case opt
when '--svn-dir'; $repos_base = arg.dup
when '--redmine-host'; $redmine_host = arg.dup
when '--owner'; $svn_owner = arg.dup; $use_groupid = false;
when '--url'; $svn_url = arg.dup
when '--scm'; $scm = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm)
when '--command'; $command = arg.dup
when '--verbose'; $verbose += 1
when '--test'; $test = true
when '--force'; $force = true
when '--version'; puts Version; exit
when '--help'; RDoc::usage
when '--quiet'; $quiet = true
end
end
rescue
exit 1
end
if $test
log("running in test mode")
end
# Make sure command is overridden if SCM vendor is not handled internally (for the moment Subversion and Git)
if $command.nil?
begin
scm_module = SCM.const_get($scm)
rescue
log("Please use --command option to specify how to create a #{$scm} repository.", :exit => true)
end
end
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
if ($redmine_host.empty? or $repos_base.empty?)
RDoc::usage
end
unless File.directory?($repos_base)
log("directory '#{$repos_base}' doesn't exists", :exit => true)
end
log("querying Redmine for projects...", :level => 1);
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
$redmine_host.gsub!(/\/$/, '')
wsdl_url = "#{$redmine_host}/sys/service.wsdl";
begin
soap = SOAP::WSDLDriverFactory.new(wsdl_url).create_rpc_driver
rescue => e
log("Unable to connect to #{wsdl_url} : #{e}", :exit => true)
end
projects = soap.ProjectsWithRepositoryEnabled
if projects.nil?
log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true)
end
log("retrieved #{projects.size} projects", :level => 1)
def set_owner_and_rights(project, repos_path, &block)
if RUBY_PLATFORM =~ /mswin/
yield if block_given?
else
uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : 0)
right = project.is_public ? 0775 : 0770
yield if block_given?
Find.find(repos_path) do |f|
File.chmod right, f
File.chown uid, gid, f
end
end
end
def other_read_right?(file)
(File.stat(file).mode & 0007).zero? ? false : true
end
def owner_name(file)
RUBY_PLATFORM =~ /mswin/ ?
$svn_owner :
Etc.getpwuid( File.stat(file).uid ).name
end
projects.each do |project|
log("treating project #{project.name}", :level => 1)
if project.identifier.empty?
log("\tno identifier for project #{project.name}")
next
elsif not project.identifier.match(/^[a-z0-9\-]+$/)
log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
next;
end
repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
if File.directory?(repos_path)
# we must verify that repository has the good owner and the good
# rights before leaving
other_read = other_read_right?(repos_path)
owner = owner_name(repos_path)
next if project.is_public == other_read and owner == $svn_owner
if $test
log("\tchange mode on #{repos_path}")
next
end
begin
set_owner_and_rights(project, repos_path)
rescue Errno::EPERM => e
log("\tunable to change mode on #{repos_path} : #{e}\n")
next
end
log("\tmode change on #{repos_path}");
else
# if repository is already declared in redmine, we don't create
# unless user use -f with reposman
if $force == false and not project.repository.nil?
log("\trepository for project #{project.identifier} already exists in Redmine", :level => 1)
next
end
project.is_public ? File.umask(0002) : File.umask(0007)
if $test
log("\tcreate repository #{repos_path}")
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}") if $svn_url;
next
end
begin
set_owner_and_rights(project, repos_path) do
if scm_module.nil?
system_or_raise "#{$command} #{repos_path}"
else
scm_module.create(repos_path)
end
end
rescue => e
log("\tunable to create #{repos_path} : #{e}\n")
next
end
if $svn_url
ret = soap.RepositoryCreated project.identifier, $scm, "#{$svn_url}#{project.identifier}"
if ret > 0
log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}");
else
log("\trepository #{repos_path} not registered in Redmine. Look in your log to find why.");
end
end
log("\trepository #{repos_path} created");
end
end
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/perl
#
# redMine is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# modify to suit your repository base
my $repos_base = '/var/svn';
my $path = '/usr/bin/';
my %kwown_commands = map { $_ => 1 } qw/svnserve/;
umask 0002;
exec ('/usr/bin/svnserve', '-r', $repos_base, '-t');