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