##// END OF EJS Templates
Adds support for Git's smart HTTP protocol to Redmine.pm (#4905)....
Jean-Philippe Lang -
r9646:e199c4b823c5
parent child
Show More
@@ -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
@@ -1,429 +1,540
1 package Apache::Authn::Redmine;
1 package Apache::Authn::Redmine;
2
2
3 =head1 Apache::Authn::Redmine
3 =head1 Apache::Authn::Redmine
4
4
5 Redmine - a mod_perl module to authenticate webdav subversion users
5 Redmine - a mod_perl module to authenticate webdav subversion users
6 against redmine database
6 against redmine database
7
7
8 =head1 SYNOPSIS
8 =head1 SYNOPSIS
9
9
10 This module allow anonymous users to browse public project and
10 This module allow anonymous users to browse public project and
11 registred users to browse and commit their project. Authentication is
11 registred users to browse and commit their project. Authentication is
12 done against the redmine database or the LDAP configured in redmine.
12 done against the redmine database or the LDAP configured in redmine.
13
13
14 This method is far simpler than the one with pam_* and works with all
14 This method is far simpler than the one with pam_* and works with all
15 database without an hassle but you need to have apache/mod_perl on the
15 database without an hassle but you need to have apache/mod_perl on the
16 svn server.
16 svn server.
17
17
18 =head1 INSTALLATION
18 =head1 INSTALLATION
19
19
20 For this to automagically work, you need to have a recent reposman.rb
20 For this to automagically work, you need to have a recent reposman.rb
21 (after r860) and if you already use reposman, read the last section to
21 (after r860) and if you already use reposman, read the last section to
22 migrate.
22 migrate.
23
23
24 Sorry ruby users but you need some perl modules, at least mod_perl2,
24 Sorry ruby users but you need some perl modules, at least mod_perl2,
25 DBI and DBD::mysql (or the DBD driver for you database as it should
25 DBI and DBD::mysql (or the DBD driver for you database as it should
26 work on allmost all databases).
26 work on allmost all databases).
27
27
28 On debian/ubuntu you must do :
28 On debian/ubuntu you must do :
29
29
30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31
31
32 If your Redmine users use LDAP authentication, you will also need
32 If your Redmine users use LDAP authentication, you will also need
33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34
34
35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36
36
37 =head1 CONFIGURATION
37 =head1 CONFIGURATION
38
38
39 ## This module has to be in your perl path
39 ## This module has to be in your perl path
40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 PerlLoadModule Apache::Authn::Redmine
41 PerlLoadModule Apache::Authn::Redmine
42 <Location /svn>
42 <Location /svn>
43 DAV svn
43 DAV svn
44 SVNParentPath "/var/svn"
44 SVNParentPath "/var/svn"
45
45
46 AuthType Basic
46 AuthType Basic
47 AuthName redmine
47 AuthName redmine
48 Require valid-user
48 Require valid-user
49
49
50 PerlAccessHandler Apache::Authn::Redmine::access_handler
50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52
52
53 ## for mysql
53 ## for mysql
54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 ## for postgres
55 ## for postgres
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57
57
58 RedmineDbUser "redmine"
58 RedmineDbUser "redmine"
59 RedmineDbPass "password"
59 RedmineDbPass "password"
60 ## Optional where clause (fulltext search would be slow and
60 ## Optional where clause (fulltext search would be slow and
61 ## database dependant).
61 ## database dependant).
62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 ## Optional credentials cache size
63 ## Optional credentials cache size
64 # RedmineCacheCredsMax 50
64 # RedmineCacheCredsMax 50
65 </Location>
65 </Location>
66
66
67 To be able to browse repository inside redmine, you must add something
67 To be able to browse repository inside redmine, you must add something
68 like that :
68 like that :
69
69
70 <Location /svn-private>
70 <Location /svn-private>
71 DAV svn
71 DAV svn
72 SVNParentPath "/var/svn"
72 SVNParentPath "/var/svn"
73 Order deny,allow
73 Order deny,allow
74 Deny from all
74 Deny from all
75 # only allow reading orders
75 # only allow reading orders
76 <Limit GET PROPFIND OPTIONS REPORT>
76 <Limit GET PROPFIND OPTIONS REPORT>
77 Allow from redmine.server.ip
77 Allow from redmine.server.ip
78 </Limit>
78 </Limit>
79 </Location>
79 </Location>
80
80
81 and you will have to use this reposman.rb command line to create repository :
81 and you will have to use this reposman.rb command line to create repository :
82
82
83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84
84
85 =head1 REPOSITORIES NAMING
85 =head1 REPOSITORIES NAMING
86
86
87 A projet repository must be named with the projet identifier. In case
87 A projet repository must be named with the projet identifier. In case
88 of multiple repositories for the same project, use the project identifier
88 of multiple repositories for the same project, use the project identifier
89 and the repository identifier separated with a dot:
89 and the repository identifier separated with a dot:
90
90
91 /var/svn/foo
91 /var/svn/foo
92 /var/svn/foo.otherrepo
92 /var/svn/foo.otherrepo
93
93
94 =head1 MIGRATION FROM OLDER RELEASES
94 =head1 MIGRATION FROM OLDER RELEASES
95
95
96 If you use an older reposman.rb (r860 or before), you need to change
96 If you use an older reposman.rb (r860 or before), you need to change
97 rights on repositories to allow the apache user to read and write
97 rights on repositories to allow the apache user to read and write
98 S<them :>
98 S<them :>
99
99
100 sudo chown -R www-data /var/svn/*
100 sudo chown -R www-data /var/svn/*
101 sudo chmod -R u+w /var/svn/*
101 sudo chmod -R u+w /var/svn/*
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;
108 use warnings FATAL => 'all', NONFATAL => 'redefine';
185 use warnings FATAL => 'all', NONFATAL => 'redefine';
109
186
110 use DBI;
187 use DBI;
111 use Digest::SHA;
188 use Digest::SHA;
112 # optional module for LDAP authentication
189 # optional module for LDAP authentication
113 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
190 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
114
191
115 use Apache2::Module;
192 use Apache2::Module;
116 use Apache2::Access;
193 use Apache2::Access;
117 use Apache2::ServerRec qw();
194 use Apache2::ServerRec qw();
118 use Apache2::RequestRec qw();
195 use Apache2::RequestRec qw();
119 use Apache2::RequestUtil qw();
196 use Apache2::RequestUtil qw();
120 use Apache2::Const qw(:common :override :cmd_how);
197 use Apache2::Const qw(:common :override :cmd_how);
121 use APR::Pool ();
198 use APR::Pool ();
122 use APR::Table ();
199 use APR::Table ();
123
200
124 # use Apache2::Directive qw();
201 # use Apache2::Directive qw();
125
202
126 my @directives = (
203 my @directives = (
127 {
204 {
128 name => 'RedmineDSN',
205 name => 'RedmineDSN',
129 req_override => OR_AUTHCFG,
206 req_override => OR_AUTHCFG,
130 args_how => TAKE1,
207 args_how => TAKE1,
131 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
208 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
132 },
209 },
133 {
210 {
134 name => 'RedmineDbUser',
211 name => 'RedmineDbUser',
135 req_override => OR_AUTHCFG,
212 req_override => OR_AUTHCFG,
136 args_how => TAKE1,
213 args_how => TAKE1,
137 },
214 },
138 {
215 {
139 name => 'RedmineDbPass',
216 name => 'RedmineDbPass',
140 req_override => OR_AUTHCFG,
217 req_override => OR_AUTHCFG,
141 args_how => TAKE1,
218 args_how => TAKE1,
142 },
219 },
143 {
220 {
144 name => 'RedmineDbWhereClause',
221 name => 'RedmineDbWhereClause',
145 req_override => OR_AUTHCFG,
222 req_override => OR_AUTHCFG,
146 args_how => TAKE1,
223 args_how => TAKE1,
147 },
224 },
148 {
225 {
149 name => 'RedmineCacheCredsMax',
226 name => 'RedmineCacheCredsMax',
150 req_override => OR_AUTHCFG,
227 req_override => OR_AUTHCFG,
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 {
157 my ($self, $parms, $arg) = @_;
239 my ($self, $parms, $arg) = @_;
158 $self->{RedmineDSN} = $arg;
240 $self->{RedmineDSN} = $arg;
159 my $query = "SELECT
241 my $query = "SELECT
160 hashed_password, salt, auth_source_id, permissions
242 hashed_password, salt, auth_source_id, permissions
161 FROM projects, users, roles
243 FROM projects, users, roles
162 WHERE
244 WHERE
163 users.login=?
245 users.login=?
164 AND projects.identifier=?
246 AND projects.identifier=?
165 AND users.status=1
247 AND users.status=1
166 AND (
248 AND (
167 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
249 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
168 OR
250 OR
169 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
251 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
170 ) ";
252 ) ";
171 $self->{RedmineQuery} = trim($query);
253 $self->{RedmineQuery} = trim($query);
172 }
254 }
173
255
174 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
256 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
175 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
257 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
176 sub RedmineDbWhereClause {
258 sub RedmineDbWhereClause {
177 my ($self, $parms, $arg) = @_;
259 my ($self, $parms, $arg) = @_;
178 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
260 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
179 }
261 }
180
262
181 sub RedmineCacheCredsMax {
263 sub RedmineCacheCredsMax {
182 my ($self, $parms, $arg) = @_;
264 my ($self, $parms, $arg) = @_;
183 if ($arg) {
265 if ($arg) {
184 $self->{RedmineCachePool} = APR::Pool->new;
266 $self->{RedmineCachePool} = APR::Pool->new;
185 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
267 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
186 $self->{RedmineCacheCredsCount} = 0;
268 $self->{RedmineCacheCredsCount} = 0;
187 $self->{RedmineCacheCredsMax} = $arg;
269 $self->{RedmineCacheCredsMax} = $arg;
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;
194 return $string;
287 return $string;
195 }
288 }
196
289
197 sub set_val {
290 sub set_val {
198 my ($key, $self, $parms, $arg) = @_;
291 my ($key, $self, $parms, $arg) = @_;
199 $self->{$key} = $arg;
292 $self->{$key} = $arg;
200 }
293 }
201
294
202 Apache2::Module::add(__PACKAGE__, \@directives);
295 Apache2::Module::add(__PACKAGE__, \@directives);
203
296
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
210 unless ($r->some_auth_required) {
320 unless ($r->some_auth_required) {
211 $r->log_reason("No authentication has been configured");
321 $r->log_reason("No authentication has been configured");
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
220 $r->set_handlers(PerlAuthenHandler => [\&OK])
329 $r->set_handlers(PerlAuthenHandler => [\&OK])
221 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
330 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
222
331
223 return OK
332 return OK
224 }
333 }
225
334
226 sub authen_handler {
335 sub authen_handler {
227 my $r = shift;
336 my $r = shift;
228
337
229 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
338 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
230 return $res unless $res == OK;
339 return $res unless $res == OK;
231
340
232 if (is_member($r->user, $redmine_pass, $r)) {
341 if (is_member($r->user, $redmine_pass, $r)) {
233 return OK;
342 return OK;
234 } else {
343 } else {
235 $r->note_auth_failure();
344 $r->note_auth_failure();
236 return AUTH_REQUIRED;
345 return AUTH_REQUIRED;
237 }
346 }
238 }
347 }
239
348
240 # check if authentication is forced
349 # check if authentication is forced
241 sub is_authentication_forced {
350 sub is_authentication_forced {
242 my $r = shift;
351 my $r = shift;
243
352
244 my $dbh = connect_database($r);
353 my $dbh = connect_database($r);
245 my $sth = $dbh->prepare(
354 my $sth = $dbh->prepare(
246 "SELECT value FROM settings where settings.name = 'login_required';"
355 "SELECT value FROM settings where settings.name = 'login_required';"
247 );
356 );
248
357
249 $sth->execute();
358 $sth->execute();
250 my $ret = 0;
359 my $ret = 0;
251 if (my @row = $sth->fetchrow_array) {
360 if (my @row = $sth->fetchrow_array) {
252 if ($row[0] eq "1" || $row[0] eq "t") {
361 if ($row[0] eq "1" || $row[0] eq "t") {
253 $ret = 1;
362 $ret = 1;
254 }
363 }
255 }
364 }
256 $sth->finish();
365 $sth->finish();
257 undef $sth;
366 undef $sth;
258
367
259 $dbh->disconnect();
368 $dbh->disconnect();
260 undef $dbh;
369 undef $dbh;
261
370
262 $ret;
371 $ret;
263 }
372 }
264
373
265 sub is_public_project {
374 sub is_public_project {
266 my $project_id = shift;
375 my $project_id = shift;
267 my $r = shift;
376 my $r = shift;
268
377
269 if (is_authentication_forced($r)) {
378 if (is_authentication_forced($r)) {
270 return 0;
379 return 0;
271 }
380 }
272
381
273 my $dbh = connect_database($r);
382 my $dbh = connect_database($r);
274 my $sth = $dbh->prepare(
383 my $sth = $dbh->prepare(
275 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
384 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
276 );
385 );
277
386
278 $sth->execute($project_id);
387 $sth->execute($project_id);
279 my $ret = 0;
388 my $ret = 0;
280 if (my @row = $sth->fetchrow_array) {
389 if (my @row = $sth->fetchrow_array) {
281 if ($row[0] eq "1" || $row[0] eq "t") {
390 if ($row[0] eq "1" || $row[0] eq "t") {
282 $ret = 1;
391 $ret = 1;
283 }
392 }
284 }
393 }
285 $sth->finish();
394 $sth->finish();
286 undef $sth;
395 undef $sth;
287 $dbh->disconnect();
396 $dbh->disconnect();
288 undef $dbh;
397 undef $dbh;
289
398
290 $ret;
399 $ret;
291 }
400 }
292
401
293 sub anonymous_role_allows_browse_repository {
402 sub anonymous_role_allows_browse_repository {
294 my $r = shift;
403 my $r = shift;
295
404
296 my $dbh = connect_database($r);
405 my $dbh = connect_database($r);
297 my $sth = $dbh->prepare(
406 my $sth = $dbh->prepare(
298 "SELECT permissions FROM roles WHERE builtin = 2;"
407 "SELECT permissions FROM roles WHERE builtin = 2;"
299 );
408 );
300
409
301 $sth->execute();
410 $sth->execute();
302 my $ret = 0;
411 my $ret = 0;
303 if (my @row = $sth->fetchrow_array) {
412 if (my @row = $sth->fetchrow_array) {
304 if ($row[0] =~ /:browse_repository/) {
413 if ($row[0] =~ /:browse_repository/) {
305 $ret = 1;
414 $ret = 1;
306 }
415 }
307 }
416 }
308 $sth->finish();
417 $sth->finish();
309 undef $sth;
418 undef $sth;
310 $dbh->disconnect();
419 $dbh->disconnect();
311 undef $dbh;
420 undef $dbh;
312
421
313 $ret;
422 $ret;
314 }
423 }
315
424
316 # perhaps we should use repository right (other read right) to check public access.
425 # perhaps we should use repository right (other read right) to check public access.
317 # it could be faster BUT it doesn't work for the moment.
426 # it could be faster BUT it doesn't work for the moment.
318 # sub is_public_project_by_file {
427 # sub is_public_project_by_file {
319 # my $project_id = shift;
428 # my $project_id = shift;
320 # my $r = shift;
429 # my $r = shift;
321
430
322 # my $tree = Apache2::Directive::conftree();
431 # my $tree = Apache2::Directive::conftree();
323 # my $node = $tree->lookup('Location', $r->location);
432 # my $node = $tree->lookup('Location', $r->location);
324 # my $hash = $node->as_hash;
433 # my $hash = $node->as_hash;
325
434
326 # my $svnparentpath = $hash->{SVNParentPath};
435 # my $svnparentpath = $hash->{SVNParentPath};
327 # my $repos_path = $svnparentpath . "/" . $project_id;
436 # my $repos_path = $svnparentpath . "/" . $project_id;
328 # return 1 if (stat($repos_path))[2] & 00007;
437 # return 1 if (stat($repos_path))[2] & 00007;
329 # }
438 # }
330
439
331 sub is_member {
440 sub is_member {
332 my $redmine_user = shift;
441 my $redmine_user = shift;
333 my $redmine_pass = shift;
442 my $redmine_pass = shift;
334 my $r = shift;
443 my $r = shift;
335
444
336 my $dbh = connect_database($r);
445 my $dbh = connect_database($r);
337 my $project_id = get_project_identifier($r);
446 my $project_id = get_project_identifier($r);
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 = defined $read_only_methods{$r->method} ? "R" : "W";
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;
345 if ($cfg->{RedmineCacheCredsMax}) {
454 if ($cfg->{RedmineCacheCredsMax}) {
346 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
455 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
347 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
456 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
348 }
457 }
349 my $query = $cfg->{RedmineQuery};
458 my $query = $cfg->{RedmineQuery};
350 my $sth = $dbh->prepare($query);
459 my $sth = $dbh->prepare($query);
351 $sth->execute($redmine_user, $project_id);
460 $sth->execute($redmine_user, $project_id);
352
461
353 my $ret;
462 my $ret;
354 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
463 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
355
464
356 unless ($auth_source_id) {
465 unless ($auth_source_id) {
357 my $method = $r->method;
466 my $method = $r->method;
358 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
467 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
359 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
468 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
360 $ret = 1;
469 $ret = 1;
361 last;
470 last;
362 }
471 }
363 } elsif ($CanUseLDAPAuth) {
472 } elsif ($CanUseLDAPAuth) {
364 my $sthldap = $dbh->prepare(
473 my $sthldap = $dbh->prepare(
365 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
474 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
366 );
475 );
367 $sthldap->execute($auth_source_id);
476 $sthldap->execute($auth_source_id);
368 while (my @rowldap = $sthldap->fetchrow_array) {
477 while (my @rowldap = $sthldap->fetchrow_array) {
369 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
478 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
370 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
479 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
371 if ($bind_as =~ m/\$login/) {
480 if ($bind_as =~ m/\$login/) {
372 # replace $login with $redmine_user and use $redmine_pass
481 # replace $login with $redmine_user and use $redmine_pass
373 $bind_as =~ s/\$login/$redmine_user/g;
482 $bind_as =~ s/\$login/$redmine_user/g;
374 $bind_pw = $redmine_pass
483 $bind_pw = $redmine_pass
375 }
484 }
376 my $ldap = Authen::Simple::LDAP->new(
485 my $ldap = Authen::Simple::LDAP->new(
377 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
486 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
378 port => $rowldap[1],
487 port => $rowldap[1],
379 basedn => $rowldap[5],
488 basedn => $rowldap[5],
380 binddn => $bind_as,
489 binddn => $bind_as,
381 bindpw => $bind_pw,
490 bindpw => $bind_pw,
382 filter => "(".$rowldap[6]."=%s)"
491 filter => "(".$rowldap[6]."=%s)"
383 );
492 );
384 my $method = $r->method;
493 my $method = $r->method;
385 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
494 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
386
495
387 }
496 }
388 $sthldap->finish();
497 $sthldap->finish();
389 undef $sthldap;
498 undef $sthldap;
390 }
499 }
391 }
500 }
392 $sth->finish();
501 $sth->finish();
393 undef $sth;
502 undef $sth;
394 $dbh->disconnect();
503 $dbh->disconnect();
395 undef $dbh;
504 undef $dbh;
396
505
397 if ($cfg->{RedmineCacheCredsMax} and $ret) {
506 if ($cfg->{RedmineCacheCredsMax} and $ret) {
398 if (defined $usrprojpass) {
507 if (defined $usrprojpass) {
399 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
508 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
400 } else {
509 } else {
401 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
510 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
402 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
511 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
403 $cfg->{RedmineCacheCredsCount}++;
512 $cfg->{RedmineCacheCredsCount}++;
404 } else {
513 } else {
405 $cfg->{RedmineCacheCreds}->clear();
514 $cfg->{RedmineCacheCreds}->clear();
406 $cfg->{RedmineCacheCredsCount} = 0;
515 $cfg->{RedmineCacheCredsCount} = 0;
407 }
516 }
408 }
517 }
409 }
518 }
410
519
411 $ret;
520 $ret;
412 }
521 }
413
522
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 }
421
532
422 sub connect_database {
533 sub connect_database {
423 my $r = shift;
534 my $r = shift;
424
535
425 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
536 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
426 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
537 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
427 }
538 }
428
539
429 1;
540 1;
General Comments 0
You need to be logged in to leave comments. Login now