@@ -0,0 +1,97 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2012 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
15 | # along with this program; if not, write to the Free Software | |||
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
|
17 | ||||
|
18 | require File.expand_path('../test_case', __FILE__) | |||
|
19 | require 'tmpdir' | |||
|
20 | ||||
|
21 | class RedminePmTest::RepositoryGitTest < RedminePmTest::TestCase | |||
|
22 | fixtures :projects, :users, :members, :roles, :member_roles | |||
|
23 | ||||
|
24 | GIT_BIN = Redmine::Configuration['scm_git_command'] || "git" | |||
|
25 | ||||
|
26 | def test_anonymous_read_on_public_repo_with_permission_should_succeed | |||
|
27 | assert_success "ls-remote", git_url | |||
|
28 | end | |||
|
29 | ||||
|
30 | def test_anonymous_read_on_public_repo_without_permission_should_fail | |||
|
31 | Role.anonymous.remove_permission! :browse_repository | |||
|
32 | assert_failure "ls-remote", git_url | |||
|
33 | end | |||
|
34 | ||||
|
35 | def test_invalid_credentials_should_fail | |||
|
36 | Project.find(1).update_attribute :is_public, false | |||
|
37 | with_credentials "dlopper", "foo" do | |||
|
38 | assert_success "ls-remote", git_url | |||
|
39 | end | |||
|
40 | with_credentials "dlopper", "wrong" do | |||
|
41 | assert_failure "ls-remote", git_url | |||
|
42 | end | |||
|
43 | end | |||
|
44 | ||||
|
45 | def test_clone | |||
|
46 | Dir.mktmpdir do |dir| | |||
|
47 | Dir.chdir(dir) do | |||
|
48 | assert_success "clone", git_url | |||
|
49 | end | |||
|
50 | end | |||
|
51 | end | |||
|
52 | ||||
|
53 | def test_write_commands | |||
|
54 | Role.find(2).add_permission! :commit_access | |||
|
55 | filename = random_filename | |||
|
56 | ||||
|
57 | Dir.mktmpdir do |dir| | |||
|
58 | assert_success "clone", git_url, dir | |||
|
59 | Dir.chdir(dir) do | |||
|
60 | f = File.new(File.join(dir, filename), "w") | |||
|
61 | f.write "test file content" | |||
|
62 | f.close | |||
|
63 | ||||
|
64 | with_credentials "dlopper", "foo" do | |||
|
65 | assert_success "add", filename | |||
|
66 | assert_success "commit -a --message Committing_a_file" | |||
|
67 | assert_success "push", git_url, "--all" | |||
|
68 | end | |||
|
69 | end | |||
|
70 | end | |||
|
71 | ||||
|
72 | Dir.mktmpdir do |dir| | |||
|
73 | assert_success "clone", git_url, dir | |||
|
74 | Dir.chdir(dir) do | |||
|
75 | assert File.exists?(File.join(dir, "#{filename}")) | |||
|
76 | end | |||
|
77 | end | |||
|
78 | end | |||
|
79 | ||||
|
80 | protected | |||
|
81 | ||||
|
82 | def execute(*args) | |||
|
83 | a = [GIT_BIN] | |||
|
84 | super a, *args | |||
|
85 | end | |||
|
86 | ||||
|
87 | def git_url(path=nil) | |||
|
88 | host = ENV['REDMINE_TEST_DAV_SERVER'] || '127.0.0.1' | |||
|
89 | credentials = nil | |||
|
90 | if username && password | |||
|
91 | credentials = "#{username}:#{password}" | |||
|
92 | end | |||
|
93 | url = "http://#{credentials}@#{host}/git/ecookbook" | |||
|
94 | url << "/#{path}" if path | |||
|
95 | url | |||
|
96 | end | |||
|
97 | end |
@@ -102,6 +102,83 S<them :> | |||||
102 |
|
102 | |||
103 | And you need to upgrade at least reposman.rb (after r860). |
|
103 | And you need to upgrade at least reposman.rb (after r860). | |
104 |
|
104 | |||
|
105 | =head1 GIT SMART HTTP SUPPORT | |||
|
106 | ||||
|
107 | Git's smart HTTP protocol (available since Git 1.7.0) will not work with the | |||
|
108 | above settings. Redmine.pm normally does access control depending on the HTTP | |||
|
109 | method used: read-only methods are OK for everyone in public projects and | |||
|
110 | members with read rights in private projects. The rest require membership with | |||
|
111 | commit rights in the project. | |||
|
112 | ||||
|
113 | However, this scheme doesn't work for Git's smart HTTP protocol, as it will use | |||
|
114 | POST even for a simple clone. Instead, read-only requests must be detected using | |||
|
115 | the full URL (including the query string): anything that doesn't belong to the | |||
|
116 | git-receive-pack service is read-only. | |||
|
117 | ||||
|
118 | To activate this mode of operation, add this line inside your <Location /git> | |||
|
119 | block: | |||
|
120 | ||||
|
121 | RedmineGitSmartHttp yes | |||
|
122 | ||||
|
123 | Here's a sample Apache configuration which integrates git-http-backend with | |||
|
124 | a MySQL database and this new option: | |||
|
125 | ||||
|
126 | SetEnv GIT_PROJECT_ROOT /var/www/git/ | |||
|
127 | SetEnv GIT_HTTP_EXPORT_ALL | |||
|
128 | ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/ | |||
|
129 | <Location /git> | |||
|
130 | Order allow,deny | |||
|
131 | Allow from all | |||
|
132 | ||||
|
133 | AuthType Basic | |||
|
134 | AuthName Git | |||
|
135 | Require valid-user | |||
|
136 | ||||
|
137 | PerlAccessHandler Apache::Authn::Redmine::access_handler | |||
|
138 | PerlAuthenHandler Apache::Authn::Redmine::authen_handler | |||
|
139 | # for mysql | |||
|
140 | RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1" | |||
|
141 | RedmineDbUser "redmine" | |||
|
142 | RedmineDbPass "xxx" | |||
|
143 | RedmineGitSmartHttp yes | |||
|
144 | </Location> | |||
|
145 | ||||
|
146 | Make sure that all the names of the repositories under /var/www/git/ have a | |||
|
147 | matching identifier for some project: /var/www/git/myproject and | |||
|
148 | /var/www/git/myproject.git will work. You can put both bare and non-bare | |||
|
149 | repositories in /var/www/git, though bare repositories are strongly | |||
|
150 | recommended. You should create them with the rights of the user running Redmine, | |||
|
151 | like this: | |||
|
152 | ||||
|
153 | cd /var/www/git | |||
|
154 | sudo -u user-running-redmine mkdir myproject | |||
|
155 | cd myproject | |||
|
156 | sudo -u user-running-redmine git init --bare | |||
|
157 | ||||
|
158 | Once you have activated this option, you have three options when cloning a | |||
|
159 | repository: | |||
|
160 | ||||
|
161 | - Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password | |||
|
162 | all the time. | |||
|
163 | ||||
|
164 | - Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but | |||
|
165 | this could reveal accidentally your password to the console in some versions | |||
|
166 | of Git, and you would have to ensure that .git/config is not readable except | |||
|
167 | by the owner for each of your projects. | |||
|
168 | ||||
|
169 | - Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc | |||
|
170 | file. This is the recommended solution, as you only have one file to protect | |||
|
171 | and passwords will not be leaked accidentally to the console. | |||
|
172 | ||||
|
173 | IMPORTANT NOTE: It is *very important* that the file cannot be read by other | |||
|
174 | users, as it will contain your password in cleartext. To create the file, you | |||
|
175 | can use the following commands, replacing yourhost, youruser and yourpassword | |||
|
176 | with the right values: | |||
|
177 | ||||
|
178 | touch ~/.netrc | |||
|
179 | chmod 600 ~/.netrc | |||
|
180 | echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc | |||
|
181 | ||||
105 | =cut |
|
182 | =cut | |
106 |
|
183 | |||
107 | use strict; |
|
184 | use strict; | |
@@ -151,6 +228,11 my @directives = ( | |||||
151 | args_how => TAKE1, |
|
228 | args_how => TAKE1, | |
152 | errmsg => 'RedmineCacheCredsMax must be decimal number', |
|
229 | errmsg => 'RedmineCacheCredsMax must be decimal number', | |
153 | }, |
|
230 | }, | |
|
231 | { | |||
|
232 | name => 'RedmineGitSmartHttp', | |||
|
233 | req_override => OR_AUTHCFG, | |||
|
234 | args_how => TAKE1, | |||
|
235 | }, | |||
154 | ); |
|
236 | ); | |
155 |
|
237 | |||
156 | sub RedmineDSN { |
|
238 | sub RedmineDSN { | |
@@ -188,6 +270,17 sub RedmineCacheCredsMax { | |||||
188 | } |
|
270 | } | |
189 | } |
|
271 | } | |
190 |
|
272 | |||
|
273 | sub RedmineGitSmartHttp { | |||
|
274 | my ($self, $parms, $arg) = @_; | |||
|
275 | $arg = lc $arg; | |||
|
276 | ||||
|
277 | if ($arg eq "yes" || $arg eq "true") { | |||
|
278 | $self->{RedmineGitSmartHttp} = 1; | |||
|
279 | } else { | |||
|
280 | $self->{RedmineGitSmartHttp} = 0; | |||
|
281 | } | |||
|
282 | } | |||
|
283 | ||||
191 | sub trim { |
|
284 | sub trim { | |
192 | my $string = shift; |
|
285 | my $string = shift; | |
193 | $string =~ s/\s{2,}/ /g; |
|
286 | $string =~ s/\s{2,}/ /g; | |
@@ -204,6 +297,23 Apache2::Module::add(__PACKAGE__, \@directives); | |||||
204 |
|
297 | |||
205 | my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; |
|
298 | my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; | |
206 |
|
299 | |||
|
300 | sub request_is_read_only { | |||
|
301 | my ($r) = @_; | |||
|
302 | my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); | |||
|
303 | ||||
|
304 | # Do we use Git's smart HTTP protocol, or not? | |||
|
305 | if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) { | |||
|
306 | my $uri = $r->unparsed_uri; | |||
|
307 | my $location = $r->location; | |||
|
308 | my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o; | |||
|
309 | return $is_read_only; | |||
|
310 | } else { | |||
|
311 | # Standard behaviour: check the HTTP method | |||
|
312 | my $method = $r->method; | |||
|
313 | return defined $read_only_methods{$method}; | |||
|
314 | } | |||
|
315 | } | |||
|
316 | ||||
207 | sub access_handler { |
|
317 | sub access_handler { | |
208 | my $r = shift; |
|
318 | my $r = shift; | |
209 |
|
319 | |||
@@ -212,8 +322,7 sub access_handler { | |||||
212 | return FORBIDDEN; |
|
322 | return FORBIDDEN; | |
213 | } |
|
323 | } | |
214 |
|
324 | |||
215 | my $method = $r->method; |
|
325 | return OK unless request_is_read_only($r); | |
216 | return OK unless defined $read_only_methods{$method}; |
|
|||
217 |
|
326 | |||
218 | my $project_id = get_project_identifier($r); |
|
327 | my $project_id = get_project_identifier($r); | |
219 |
|
328 | |||
@@ -338,7 +447,7 sub is_member { | |||||
338 |
|
447 | |||
339 | my $pass_digest = Digest::SHA::sha1_hex($redmine_pass); |
|
448 | my $pass_digest = Digest::SHA::sha1_hex($redmine_pass); | |
340 |
|
449 | |||
341 |
my $access_mode = |
|
450 | my $access_mode = request_is_read_only($r) ? "R" : "W"; | |
342 |
|
451 | |||
343 | my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); |
|
452 | my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); | |
344 | my $usrprojpass; |
|
453 | my $usrprojpass; | |
@@ -414,7 +523,9 sub is_member { | |||||
414 | sub get_project_identifier { |
|
523 | sub get_project_identifier { | |
415 | my $r = shift; |
|
524 | my $r = shift; | |
416 |
|
525 | |||
|
526 | my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); | |||
417 | my $location = $r->location; |
|
527 | my $location = $r->location; | |
|
528 | $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}); | |||
418 | my ($identifier) = $r->uri =~ m{$location/*([^/.]+)}; |
|
529 | my ($identifier) = $r->uri =~ m{$location/*([^/.]+)}; | |
419 | $identifier; |
|
530 | $identifier; | |
420 | } |
|
531 | } |
General Comments 0
You need to be logged in to leave comments.
Login now