To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / extra / svn / Redmine.pm @ 1298:4f746d8966dd

History | View | Annotate | Download (16 KB)

1 0:513646585e45 Chris
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 909:cbb26bc654de Chris
53 0:513646585e45 Chris
     ## 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 1115:433d4f72a19b Chris
=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 0:513646585e45 Chris
=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 1115:433d4f72a19b Chris
=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 0:513646585e45 Chris
=cut
183
184
use strict;
185
use warnings FATAL => 'all', NONFATAL => 'redefine';
186
187
use DBI;
188 929:5f33065ddc4b Chris
use Digest::SHA;
189 0:513646585e45 Chris
# 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 1115:433d4f72a19b Chris
  {
232
    name => 'RedmineGitSmartHttp',
233
    req_override => OR_AUTHCFG,
234
    args_how => TAKE1,
235
  },
236 0:513646585e45 Chris
);
237
238 909:cbb26bc654de Chris
sub RedmineDSN {
239 0:513646585e45 Chris
  my ($self, $parms, $arg) = @_;
240
  $self->{RedmineDSN} = $arg;
241
  my $query = "SELECT
242 1115:433d4f72a19b Chris
                 users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
243 909:cbb26bc654de Chris
              FROM projects, users, roles
244 0:513646585e45 Chris
              WHERE
245 909:cbb26bc654de Chris
                users.login=?
246
                AND projects.identifier=?
247 0:513646585e45 Chris
                AND users.status=1
248 909:cbb26bc654de Chris
                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 1115:433d4f72a19b Chris
                )
253
                AND roles.permissions IS NOT NULL";
254 0:513646585e45 Chris
  $self->{RedmineQuery} = trim($query);
255
}
256
257
sub RedmineDbUser { set_val('RedmineDbUser', @_); }
258
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
259 909:cbb26bc654de Chris
sub RedmineDbWhereClause {
260 0:513646585e45 Chris
  my ($self, $parms, $arg) = @_;
261
  $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
262
}
263
264 909:cbb26bc654de Chris
sub RedmineCacheCredsMax {
265 0:513646585e45 Chris
  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 1115:433d4f72a19b Chris
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 0:513646585e45 Chris
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 1115:433d4f72a19b Chris
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 0:513646585e45 Chris
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 1115:433d4f72a19b Chris
  return OK unless request_is_read_only($r);
327 0:513646585e45 Chris
328
  my $project_id = get_project_identifier($r);
329
330
  $r->set_handlers(PerlAuthenHandler => [\&OK])
331 909:cbb26bc654de Chris
      if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
332 0:513646585e45 Chris
333
  return OK
334
}
335
336
sub authen_handler {
337
  my $r = shift;
338 909:cbb26bc654de Chris
339 0:513646585e45 Chris
  my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
340
  return $res unless $res == OK;
341 909:cbb26bc654de Chris
342 0:513646585e45 Chris
  if (is_member($r->user, $redmine_pass, $r)) {
343
      return OK;
344
  } else {
345
      $r->note_auth_failure();
346 1115:433d4f72a19b Chris
      return DECLINED;
347 0:513646585e45 Chris
  }
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 909:cbb26bc654de Chris
369 0:513646585e45 Chris
  $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 909:cbb26bc654de Chris
379 0:513646585e45 Chris
    if (is_authentication_forced($r)) {
380
      return 0;
381
    }
382
383
    my $dbh = connect_database($r);
384
    my $sth = $dbh->prepare(
385 1115:433d4f72a19b Chris
        "SELECT is_public FROM projects WHERE projects.identifier = ? AND projects.status <> 9;"
386 0:513646585e45 Chris
    );
387
388
    $sth->execute($project_id);
389
    my $ret = 0;
390
    if (my @row = $sth->fetchrow_array) {
391 1295:622f24f53b42 Chris
      if ($row[0] eq "1" || $row[0] eq "t") {
392
        $ret = 1;
393
      }
394 0:513646585e45 Chris
    }
395
    $sth->finish();
396
    undef $sth;
397
    $dbh->disconnect();
398
    undef $dbh;
399
400
    $ret;
401
}
402
403 909:cbb26bc654de Chris
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 0:513646585e45 Chris
# 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 929:5f33065ddc4b Chris
  my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
450 0:513646585e45 Chris
451 1115:433d4f72a19b Chris
  my $access_mode = request_is_read_only($r) ? "R" : "W";
452 909:cbb26bc654de Chris
453 0:513646585e45 Chris
  my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
454
  my $usrprojpass;
455
  if ($cfg->{RedmineCacheCredsMax}) {
456 909:cbb26bc654de Chris
    $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
457 0:513646585e45 Chris
    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 1115:433d4f72a19b Chris
  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 0:513646585e45 Chris
469
      unless ($auth_source_id) {
470 1295:622f24f53b42 Chris
          my $method = $r->method;
471 929:5f33065ddc4b Chris
          my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
472 1295:622f24f53b42 Chris
          if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
473 0:513646585e45 Chris
              $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 1115:433d4f72a19b Chris
            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 0:513646585e45 Chris
            my $ldap = Authen::Simple::LDAP->new(
490 37:94944d00e43c chris
                host    =>      ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
491 0:513646585e45 Chris
                port    =>      $rowldap[1],
492
                basedn  =>      $rowldap[5],
493 1115:433d4f72a19b Chris
                binddn  =>      $bind_as,
494
                bindpw  =>      $bind_pw,
495 0:513646585e45 Chris
                filter  =>      "(".$rowldap[6]."=%s)"
496
            );
497
            my $method = $r->method;
498 909:cbb26bc654de Chris
            $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
499 0:513646585e45 Chris
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 909:cbb26bc654de Chris
      $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
513 0:513646585e45 Chris
    } else {
514
      if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
515 909:cbb26bc654de Chris
        $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
516 0:513646585e45 Chris
        $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 909:cbb26bc654de Chris
530 1115:433d4f72a19b Chris
    my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
531 0:513646585e45 Chris
    my $location = $r->location;
532 1115:433d4f72a19b Chris
    $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
533
    my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
534 0:513646585e45 Chris
    $identifier;
535
}
536
537
sub connect_database {
538
    my $r = shift;
539 909:cbb26bc654de Chris
540 0:513646585e45 Chris
    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;