comparison .svn/pristine/58/58919c55a2dd0b48c7152f7b1adf06799d9cbcd6.svn-base @ 1295:622f24f53b42 redmine-2.3

Update to Redmine SVN revision 11972 on 2.3-stable branch
author Chris Cannam
date Fri, 14 Jun 2013 09:02:21 +0100
parents
children
comparison
equal deleted inserted replaced
1294:3e4c3460b6ca 1295:622f24f53b42
1 package Apache::Authn::Redmine;
2
3 =head1 Apache::Authn::Redmine
4
5 Redmine - a mod_perl module to authenticate webdav subversion users
6 against redmine database
7
8 =head1 SYNOPSIS
9
10 This module allow anonymous users to browse public project and
11 registred users to browse and commit their project. Authentication is
12 done against the redmine database or the LDAP configured in redmine.
13
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
16 svn server.
17
18 =head1 INSTALLATION
19
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
22 migrate.
23
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
26 work on allmost all databases).
27
28 On debian/ubuntu you must do :
29
30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31
32 If your Redmine users use LDAP authentication, you will also need
33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34
35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36
37 =head1 CONFIGURATION
38
39 ## This module has to be in your perl path
40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 PerlLoadModule Apache::Authn::Redmine
42 <Location /svn>
43 DAV svn
44 SVNParentPath "/var/svn"
45
46 AuthType Basic
47 AuthName redmine
48 Require valid-user
49
50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52
53 ## for mysql
54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 ## for postgres
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57
58 RedmineDbUser "redmine"
59 RedmineDbPass "password"
60 ## Optional where clause (fulltext search would be slow and
61 ## database dependant).
62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 ## Optional credentials cache size
64 # RedmineCacheCredsMax 50
65 </Location>
66
67 To be able to browse repository inside redmine, you must add something
68 like that :
69
70 <Location /svn-private>
71 DAV svn
72 SVNParentPath "/var/svn"
73 Order deny,allow
74 Deny from all
75 # only allow reading orders
76 <Limit GET PROPFIND OPTIONS REPORT>
77 Allow from redmine.server.ip
78 </Limit>
79 </Location>
80
81 and you will have to use this reposman.rb command line to create repository :
82
83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84
85 =head1 REPOSITORIES NAMING
86
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
89 and the repository identifier separated with a dot:
90
91 /var/svn/foo
92 /var/svn/foo.otherrepo
93
94 =head1 MIGRATION FROM OLDER RELEASES
95
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
98 S<them :>
99
100 sudo chown -R www-data /var/svn/*
101 sudo chmod -R u+w /var/svn/*
102
103 And you need to upgrade at least reposman.rb (after r860).
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
182 =cut
183
184 use strict;
185 use warnings FATAL => 'all', NONFATAL => 'redefine';
186
187 use DBI;
188 use Digest::SHA;
189 # optional module for LDAP authentication
190 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
191
192 use Apache2::Module;
193 use Apache2::Access;
194 use Apache2::ServerRec qw();
195 use Apache2::RequestRec qw();
196 use Apache2::RequestUtil qw();
197 use Apache2::Const qw(:common :override :cmd_how);
198 use APR::Pool ();
199 use APR::Table ();
200
201 # use Apache2::Directive qw();
202
203 my @directives = (
204 {
205 name => 'RedmineDSN',
206 req_override => OR_AUTHCFG,
207 args_how => TAKE1,
208 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
209 },
210 {
211 name => 'RedmineDbUser',
212 req_override => OR_AUTHCFG,
213 args_how => TAKE1,
214 },
215 {
216 name => 'RedmineDbPass',
217 req_override => OR_AUTHCFG,
218 args_how => TAKE1,
219 },
220 {
221 name => 'RedmineDbWhereClause',
222 req_override => OR_AUTHCFG,
223 args_how => TAKE1,
224 },
225 {
226 name => 'RedmineCacheCredsMax',
227 req_override => OR_AUTHCFG,
228 args_how => TAKE1,
229 errmsg => 'RedmineCacheCredsMax must be decimal number',
230 },
231 {
232 name => 'RedmineGitSmartHttp',
233 req_override => OR_AUTHCFG,
234 args_how => TAKE1,
235 },
236 );
237
238 sub RedmineDSN {
239 my ($self, $parms, $arg) = @_;
240 $self->{RedmineDSN} = $arg;
241 my $query = "SELECT
242 users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
243 FROM projects, users, roles
244 WHERE
245 users.login=?
246 AND projects.identifier=?
247 AND users.status=1
248 AND (
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)
250 OR
251 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
252 )
253 AND roles.permissions IS NOT NULL";
254 $self->{RedmineQuery} = trim($query);
255 }
256
257 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
258 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
259 sub RedmineDbWhereClause {
260 my ($self, $parms, $arg) = @_;
261 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
262 }
263
264 sub RedmineCacheCredsMax {
265 my ($self, $parms, $arg) = @_;
266 if ($arg) {
267 $self->{RedmineCachePool} = APR::Pool->new;
268 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
269 $self->{RedmineCacheCredsCount} = 0;
270 $self->{RedmineCacheCredsMax} = $arg;
271 }
272 }
273
274 sub RedmineGitSmartHttp {
275 my ($self, $parms, $arg) = @_;
276 $arg = lc $arg;
277
278 if ($arg eq "yes" || $arg eq "true") {
279 $self->{RedmineGitSmartHttp} = 1;
280 } else {
281 $self->{RedmineGitSmartHttp} = 0;
282 }
283 }
284
285 sub trim {
286 my $string = shift;
287 $string =~ s/\s{2,}/ /g;
288 return $string;
289 }
290
291 sub set_val {
292 my ($key, $self, $parms, $arg) = @_;
293 $self->{$key} = $arg;
294 }
295
296 Apache2::Module::add(__PACKAGE__, \@directives);
297
298
299 my %read_only_methods = map { $_ => 1 } qw/GET HEAD PROPFIND REPORT OPTIONS/;
300
301 sub request_is_read_only {
302 my ($r) = @_;
303 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
304
305 # Do we use Git's smart HTTP protocol, or not?
306 if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
307 my $uri = $r->unparsed_uri;
308 my $location = $r->location;
309 my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
310 return $is_read_only;
311 } else {
312 # Standard behaviour: check the HTTP method
313 my $method = $r->method;
314 return defined $read_only_methods{$method};
315 }
316 }
317
318 sub access_handler {
319 my $r = shift;
320
321 unless ($r->some_auth_required) {
322 $r->log_reason("No authentication has been configured");
323 return FORBIDDEN;
324 }
325
326 return OK unless request_is_read_only($r);
327
328 my $project_id = get_project_identifier($r);
329
330 $r->set_handlers(PerlAuthenHandler => [\&OK])
331 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
332
333 return OK
334 }
335
336 sub authen_handler {
337 my $r = shift;
338
339 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
340 return $res unless $res == OK;
341
342 if (is_member($r->user, $redmine_pass, $r)) {
343 return OK;
344 } else {
345 $r->note_auth_failure();
346 return DECLINED;
347 }
348 }
349
350 # check if authentication is forced
351 sub is_authentication_forced {
352 my $r = shift;
353
354 my $dbh = connect_database($r);
355 my $sth = $dbh->prepare(
356 "SELECT value FROM settings where settings.name = 'login_required';"
357 );
358
359 $sth->execute();
360 my $ret = 0;
361 if (my @row = $sth->fetchrow_array) {
362 if ($row[0] eq "1" || $row[0] eq "t") {
363 $ret = 1;
364 }
365 }
366 $sth->finish();
367 undef $sth;
368
369 $dbh->disconnect();
370 undef $dbh;
371
372 $ret;
373 }
374
375 sub is_public_project {
376 my $project_id = shift;
377 my $r = shift;
378
379 if (is_authentication_forced($r)) {
380 return 0;
381 }
382
383 my $dbh = connect_database($r);
384 my $sth = $dbh->prepare(
385 "SELECT is_public FROM projects WHERE projects.identifier = ? AND projects.status <> 9;"
386 );
387
388 $sth->execute($project_id);
389 my $ret = 0;
390 if (my @row = $sth->fetchrow_array) {
391 if ($row[0] eq "1" || $row[0] eq "t") {
392 $ret = 1;
393 }
394 }
395 $sth->finish();
396 undef $sth;
397 $dbh->disconnect();
398 undef $dbh;
399
400 $ret;
401 }
402
403 sub anonymous_role_allows_browse_repository {
404 my $r = shift;
405
406 my $dbh = connect_database($r);
407 my $sth = $dbh->prepare(
408 "SELECT permissions FROM roles WHERE builtin = 2;"
409 );
410
411 $sth->execute();
412 my $ret = 0;
413 if (my @row = $sth->fetchrow_array) {
414 if ($row[0] =~ /:browse_repository/) {
415 $ret = 1;
416 }
417 }
418 $sth->finish();
419 undef $sth;
420 $dbh->disconnect();
421 undef $dbh;
422
423 $ret;
424 }
425
426 # perhaps we should use repository right (other read right) to check public access.
427 # it could be faster BUT it doesn't work for the moment.
428 # sub is_public_project_by_file {
429 # my $project_id = shift;
430 # my $r = shift;
431
432 # my $tree = Apache2::Directive::conftree();
433 # my $node = $tree->lookup('Location', $r->location);
434 # my $hash = $node->as_hash;
435
436 # my $svnparentpath = $hash->{SVNParentPath};
437 # my $repos_path = $svnparentpath . "/" . $project_id;
438 # return 1 if (stat($repos_path))[2] & 00007;
439 # }
440
441 sub is_member {
442 my $redmine_user = shift;
443 my $redmine_pass = shift;
444 my $r = shift;
445
446 my $dbh = connect_database($r);
447 my $project_id = get_project_identifier($r);
448
449 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
450
451 my $access_mode = request_is_read_only($r) ? "R" : "W";
452
453 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
454 my $usrprojpass;
455 if ($cfg->{RedmineCacheCredsMax}) {
456 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
457 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
458 }
459 my $query = $cfg->{RedmineQuery};
460 my $sth = $dbh->prepare($query);
461 $sth->execute($redmine_user, $project_id);
462
463 my $ret;
464 while (my ($hashed_password, $salt, $auth_source_id, $permissions, $project_status) = $sth->fetchrow_array) {
465 if ($project_status eq "9" || ($project_status ne "1" && $access_mode eq "W")) {
466 last;
467 }
468
469 unless ($auth_source_id) {
470 my $method = $r->method;
471 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
472 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
473 $ret = 1;
474 last;
475 }
476 } elsif ($CanUseLDAPAuth) {
477 my $sthldap = $dbh->prepare(
478 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
479 );
480 $sthldap->execute($auth_source_id);
481 while (my @rowldap = $sthldap->fetchrow_array) {
482 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
483 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
484 if ($bind_as =~ m/\$login/) {
485 # replace $login with $redmine_user and use $redmine_pass
486 $bind_as =~ s/\$login/$redmine_user/g;
487 $bind_pw = $redmine_pass
488 }
489 my $ldap = Authen::Simple::LDAP->new(
490 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
491 port => $rowldap[1],
492 basedn => $rowldap[5],
493 binddn => $bind_as,
494 bindpw => $bind_pw,
495 filter => "(".$rowldap[6]."=%s)"
496 );
497 my $method = $r->method;
498 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
499
500 }
501 $sthldap->finish();
502 undef $sthldap;
503 }
504 }
505 $sth->finish();
506 undef $sth;
507 $dbh->disconnect();
508 undef $dbh;
509
510 if ($cfg->{RedmineCacheCredsMax} and $ret) {
511 if (defined $usrprojpass) {
512 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
513 } else {
514 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
515 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
516 $cfg->{RedmineCacheCredsCount}++;
517 } else {
518 $cfg->{RedmineCacheCreds}->clear();
519 $cfg->{RedmineCacheCredsCount} = 0;
520 }
521 }
522 }
523
524 $ret;
525 }
526
527 sub get_project_identifier {
528 my $r = shift;
529
530 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
531 my $location = $r->location;
532 $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
533 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
534 $identifier;
535 }
536
537 sub connect_database {
538 my $r = shift;
539
540 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
541 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
542 }
543
544 1;