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 @ 1562:3107eacaddf7

History | View | Annotate | Download (16 KB)

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;