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 / .svn / pristine / 58 / 58919c55a2dd0b48c7152f7b1adf06799d9cbcd6.svn-base @ 1297:0a574315af3e

History | View | Annotate | Download (16 KB)

1 1296:038ba2d95de8 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
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;