Adds support for Git's smart HTTP protocol to Redmine.pm (#4905).

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@9829 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2012-06-14 08:13:53 +00:00
parent 9eef74f09a
commit e199c4b823
2 changed files with 211 additions and 3 deletions

View File

@ -102,6 +102,83 @@ S<them :>
And you need to upgrade at least reposman.rb (after r860).
=head1 GIT SMART HTTP SUPPORT
Git's smart HTTP protocol (available since Git 1.7.0) will not work with the
above settings. Redmine.pm normally does access control depending on the HTTP
method used: read-only methods are OK for everyone in public projects and
members with read rights in private projects. The rest require membership with
commit rights in the project.
However, this scheme doesn't work for Git's smart HTTP protocol, as it will use
POST even for a simple clone. Instead, read-only requests must be detected using
the full URL (including the query string): anything that doesn't belong to the
git-receive-pack service is read-only.
To activate this mode of operation, add this line inside your <Location /git>
block:
RedmineGitSmartHttp yes
Here's a sample Apache configuration which integrates git-http-backend with
a MySQL database and this new option:
SetEnv GIT_PROJECT_ROOT /var/www/git/
SetEnv GIT_HTTP_EXPORT_ALL
ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
<Location /git>
Order allow,deny
Allow from all
AuthType Basic
AuthName Git
Require valid-user
PerlAccessHandler Apache::Authn::Redmine::access_handler
PerlAuthenHandler Apache::Authn::Redmine::authen_handler
# for mysql
RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1"
RedmineDbUser "redmine"
RedmineDbPass "xxx"
RedmineGitSmartHttp yes
</Location>
Make sure that all the names of the repositories under /var/www/git/ have a
matching identifier for some project: /var/www/git/myproject and
/var/www/git/myproject.git will work. You can put both bare and non-bare
repositories in /var/www/git, though bare repositories are strongly
recommended. You should create them with the rights of the user running Redmine,
like this:
cd /var/www/git
sudo -u user-running-redmine mkdir myproject
cd myproject
sudo -u user-running-redmine git init --bare
Once you have activated this option, you have three options when cloning a
repository:
- Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password
all the time.
- Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but
this could reveal accidentally your password to the console in some versions
of Git, and you would have to ensure that .git/config is not readable except
by the owner for each of your projects.
- Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc
file. This is the recommended solution, as you only have one file to protect
and passwords will not be leaked accidentally to the console.
IMPORTANT NOTE: It is *very important* that the file cannot be read by other
users, as it will contain your password in cleartext. To create the file, you
can use the following commands, replacing yourhost, youruser and yourpassword
with the right values:
touch ~/.netrc
chmod 600 ~/.netrc
echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc
=cut
use strict;
@ -151,6 +228,11 @@ my @directives = (
args_how => TAKE1,
errmsg => 'RedmineCacheCredsMax must be decimal number',
},
{
name => 'RedmineGitSmartHttp',
req_override => OR_AUTHCFG,
args_how => TAKE1,
},
);
sub RedmineDSN {
@ -188,6 +270,17 @@ sub RedmineCacheCredsMax {
}
}
sub RedmineGitSmartHttp {
my ($self, $parms, $arg) = @_;
$arg = lc $arg;
if ($arg eq "yes" || $arg eq "true") {
$self->{RedmineGitSmartHttp} = 1;
} else {
$self->{RedmineGitSmartHttp} = 0;
}
}
sub trim {
my $string = shift;
$string =~ s/\s{2,}/ /g;
@ -204,6 +297,23 @@ Apache2::Module::add(__PACKAGE__, \@directives);
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
sub request_is_read_only {
my ($r) = @_;
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
# Do we use Git's smart HTTP protocol, or not?
if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
my $uri = $r->unparsed_uri;
my $location = $r->location;
my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
return $is_read_only;
} else {
# Standard behaviour: check the HTTP method
my $method = $r->method;
return defined $read_only_methods{$method};
}
}
sub access_handler {
my $r = shift;
@ -212,8 +322,7 @@ sub access_handler {
return FORBIDDEN;
}
my $method = $r->method;
return OK unless defined $read_only_methods{$method};
return OK unless request_is_read_only($r);
my $project_id = get_project_identifier($r);
@ -338,7 +447,7 @@ sub is_member {
my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
my $access_mode = defined $read_only_methods{$r->method} ? "R" : "W";
my $access_mode = request_is_read_only($r) ? "R" : "W";
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
my $usrprojpass;
@ -414,7 +523,9 @@ sub is_member {
sub get_project_identifier {
my $r = shift;
my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
my $location = $r->location;
$location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
$identifier;
}

View File

@ -0,0 +1,97 @@
# Redmine - project management software
# Copyright (C) 2006-2012 Jean-Philippe Lang
#
# This program 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.
require File.expand_path('../test_case', __FILE__)
require 'tmpdir'
class RedminePmTest::RepositoryGitTest < RedminePmTest::TestCase
fixtures :projects, :users, :members, :roles, :member_roles
GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
def test_anonymous_read_on_public_repo_with_permission_should_succeed
assert_success "ls-remote", git_url
end
def test_anonymous_read_on_public_repo_without_permission_should_fail
Role.anonymous.remove_permission! :browse_repository
assert_failure "ls-remote", git_url
end
def test_invalid_credentials_should_fail
Project.find(1).update_attribute :is_public, false
with_credentials "dlopper", "foo" do
assert_success "ls-remote", git_url
end
with_credentials "dlopper", "wrong" do
assert_failure "ls-remote", git_url
end
end
def test_clone
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
assert_success "clone", git_url
end
end
end
def test_write_commands
Role.find(2).add_permission! :commit_access
filename = random_filename
Dir.mktmpdir do |dir|
assert_success "clone", git_url, dir
Dir.chdir(dir) do
f = File.new(File.join(dir, filename), "w")
f.write "test file content"
f.close
with_credentials "dlopper", "foo" do
assert_success "add", filename
assert_success "commit -a --message Committing_a_file"
assert_success "push", git_url, "--all"
end
end
end
Dir.mktmpdir do |dir|
assert_success "clone", git_url, dir
Dir.chdir(dir) do
assert File.exists?(File.join(dir, "#{filename}"))
end
end
end
protected
def execute(*args)
a = [GIT_BIN]
super a, *args
end
def git_url(path=nil)
host = ENV['REDMINE_TEST_DAV_SERVER'] || '127.0.0.1'
credentials = nil
if username && password
credentials = "#{username}:#{password}"
end
url = "http://#{credentials}@#{host}/git/ecookbook"
url << "/#{path}" if path
url
end
end