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 @ 912:5e80956cc792

History | View | Annotate | Download (11.2 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
=head1 MIGRATION FROM OLDER RELEASES
86
87
If you use an older reposman.rb (r860 or before), you need to change
88
rights on repositories to allow the apache user to read and write
89
S<them :>
90
91
  sudo chown -R www-data /var/svn/*
92
  sudo chmod -R u+w /var/svn/*
93
94
And you need to upgrade at least reposman.rb (after r860).
95
96
=cut
97
98
use strict;
99
use warnings FATAL => 'all', NONFATAL => 'redefine';
100
101
use DBI;
102
use Digest::SHA1;
103
# optional module for LDAP authentication
104
my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
105
106
use Apache2::Module;
107
use Apache2::Access;
108
use Apache2::ServerRec qw();
109
use Apache2::RequestRec qw();
110
use Apache2::RequestUtil qw();
111
use Apache2::Const qw(:common :override :cmd_how);
112
use APR::Pool ();
113
use APR::Table ();
114
115
# use Apache2::Directive qw();
116
117
my @directives = (
118
  {
119
    name => 'RedmineDSN',
120
    req_override => OR_AUTHCFG,
121
    args_how => TAKE1,
122
    errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
123
  },
124
  {
125
    name => 'RedmineDbUser',
126
    req_override => OR_AUTHCFG,
127
    args_how => TAKE1,
128
  },
129
  {
130
    name => 'RedmineDbPass',
131
    req_override => OR_AUTHCFG,
132
    args_how => TAKE1,
133
  },
134
  {
135
    name => 'RedmineDbWhereClause',
136
    req_override => OR_AUTHCFG,
137
    args_how => TAKE1,
138
  },
139
  {
140
    name => 'RedmineCacheCredsMax',
141
    req_override => OR_AUTHCFG,
142
    args_how => TAKE1,
143
    errmsg => 'RedmineCacheCredsMax must be decimal number',
144
  },
145
);
146
147 909:cbb26bc654de Chris
sub RedmineDSN {
148 0:513646585e45 Chris
  my ($self, $parms, $arg) = @_;
149
  $self->{RedmineDSN} = $arg;
150
  my $query = "SELECT
151 245:051f544170fe Chris
                 hashed_password, salt, auth_source_id, permissions
152 909:cbb26bc654de Chris
              FROM projects, users, roles
153 0:513646585e45 Chris
              WHERE
154 909:cbb26bc654de Chris
                users.login=?
155
                AND projects.identifier=?
156 0:513646585e45 Chris
                AND users.status=1
157 909:cbb26bc654de Chris
                AND (
158
                  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)
159
                  OR
160
                  (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
161
                ) ";
162 0:513646585e45 Chris
  $self->{RedmineQuery} = trim($query);
163
}
164
165
sub RedmineDbUser { set_val('RedmineDbUser', @_); }
166
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
167 909:cbb26bc654de Chris
sub RedmineDbWhereClause {
168 0:513646585e45 Chris
  my ($self, $parms, $arg) = @_;
169
  $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
170
}
171
172 909:cbb26bc654de Chris
sub RedmineCacheCredsMax {
173 0:513646585e45 Chris
  my ($self, $parms, $arg) = @_;
174
  if ($arg) {
175
    $self->{RedmineCachePool} = APR::Pool->new;
176
    $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
177
    $self->{RedmineCacheCredsCount} = 0;
178
    $self->{RedmineCacheCredsMax} = $arg;
179
  }
180
}
181
182
sub trim {
183
  my $string = shift;
184
  $string =~ s/\s{2,}/ /g;
185
  return $string;
186
}
187
188
sub set_val {
189
  my ($key, $self, $parms, $arg) = @_;
190
  $self->{$key} = $arg;
191
}
192
193
Apache2::Module::add(__PACKAGE__, \@directives);
194
195
196
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
197
198
sub access_handler {
199
  my $r = shift;
200
201
  unless ($r->some_auth_required) {
202
      $r->log_reason("No authentication has been configured");
203
      return FORBIDDEN;
204
  }
205
206
  my $method = $r->method;
207
  return OK unless defined $read_only_methods{$method};
208
209
  my $project_id = get_project_identifier($r);
210
211
  $r->set_handlers(PerlAuthenHandler => [\&OK])
212 909:cbb26bc654de Chris
      if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
213 0:513646585e45 Chris
214
  return OK
215
}
216
217
sub authen_handler {
218
  my $r = shift;
219 909:cbb26bc654de Chris
220 0:513646585e45 Chris
  my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
221
  return $res unless $res == OK;
222 909:cbb26bc654de Chris
223 0:513646585e45 Chris
  if (is_member($r->user, $redmine_pass, $r)) {
224
      return OK;
225
  } else {
226
      $r->note_auth_failure();
227
      return AUTH_REQUIRED;
228
  }
229
}
230
231
# check if authentication is forced
232
sub is_authentication_forced {
233
  my $r = shift;
234
235
  my $dbh = connect_database($r);
236
  my $sth = $dbh->prepare(
237
    "SELECT value FROM settings where settings.name = 'login_required';"
238
  );
239
240
  $sth->execute();
241
  my $ret = 0;
242
  if (my @row = $sth->fetchrow_array) {
243
    if ($row[0] eq "1" || $row[0] eq "t") {
244
      $ret = 1;
245
    }
246
  }
247
  $sth->finish();
248
  undef $sth;
249 909:cbb26bc654de Chris
250 0:513646585e45 Chris
  $dbh->disconnect();
251
  undef $dbh;
252
253
  $ret;
254
}
255
256
sub is_public_project {
257
    my $project_id = shift;
258
    my $r = shift;
259 909:cbb26bc654de Chris
260 0:513646585e45 Chris
    if (is_authentication_forced($r)) {
261
      return 0;
262
    }
263
264
    my $dbh = connect_database($r);
265
    my $sth = $dbh->prepare(
266
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
267
    );
268
269
    $sth->execute($project_id);
270
    my $ret = 0;
271
    if (my @row = $sth->fetchrow_array) {
272
    	if ($row[0] eq "1" || $row[0] eq "t") {
273
    		$ret = 1;
274
    	}
275
    }
276
    $sth->finish();
277
    undef $sth;
278
    $dbh->disconnect();
279
    undef $dbh;
280
281
    $ret;
282
}
283
284 909:cbb26bc654de Chris
sub anonymous_role_allows_browse_repository {
285
  my $r = shift;
286
287
  my $dbh = connect_database($r);
288
  my $sth = $dbh->prepare(
289
      "SELECT permissions FROM roles WHERE builtin = 2;"
290
  );
291
292
  $sth->execute();
293
  my $ret = 0;
294
  if (my @row = $sth->fetchrow_array) {
295
    if ($row[0] =~ /:browse_repository/) {
296
      $ret = 1;
297
    }
298
  }
299
  $sth->finish();
300
  undef $sth;
301
  $dbh->disconnect();
302
  undef $dbh;
303
304
  $ret;
305
}
306
307 0:513646585e45 Chris
# perhaps we should use repository right (other read right) to check public access.
308
# it could be faster BUT it doesn't work for the moment.
309
# sub is_public_project_by_file {
310
#     my $project_id = shift;
311
#     my $r = shift;
312
313
#     my $tree = Apache2::Directive::conftree();
314
#     my $node = $tree->lookup('Location', $r->location);
315
#     my $hash = $node->as_hash;
316
317
#     my $svnparentpath = $hash->{SVNParentPath};
318
#     my $repos_path = $svnparentpath . "/" . $project_id;
319
#     return 1 if (stat($repos_path))[2] & 00007;
320
# }
321
322
sub is_member {
323
  my $redmine_user = shift;
324
  my $redmine_pass = shift;
325
  my $r = shift;
326
327
  my $dbh         = connect_database($r);
328
  my $project_id  = get_project_identifier($r);
329
330
  my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
331
332 909:cbb26bc654de Chris
  my $access_mode = defined $read_only_methods{$r->method} ? "R" : "W";
333
334 0:513646585e45 Chris
  my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
335
  my $usrprojpass;
336
  if ($cfg->{RedmineCacheCredsMax}) {
337 909:cbb26bc654de Chris
    $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
338 0:513646585e45 Chris
    return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
339
  }
340
  my $query = $cfg->{RedmineQuery};
341
  my $sth = $dbh->prepare($query);
342
  $sth->execute($redmine_user, $project_id);
343
344
  my $ret;
345 245:051f544170fe Chris
  while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
346 0:513646585e45 Chris
347
      unless ($auth_source_id) {
348 245:051f544170fe Chris
	  			my $method = $r->method;
349
          my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest);
350 909:cbb26bc654de Chris
					if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
351 0:513646585e45 Chris
              $ret = 1;
352
              last;
353
          }
354
      } elsif ($CanUseLDAPAuth) {
355
          my $sthldap = $dbh->prepare(
356
              "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
357
          );
358
          $sthldap->execute($auth_source_id);
359
          while (my @rowldap = $sthldap->fetchrow_array) {
360
            my $ldap = Authen::Simple::LDAP->new(
361 37:94944d00e43c chris
                host    =>      ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
362 0:513646585e45 Chris
                port    =>      $rowldap[1],
363
                basedn  =>      $rowldap[5],
364
                binddn  =>      $rowldap[3] ? $rowldap[3] : "",
365
                bindpw  =>      $rowldap[4] ? $rowldap[4] : "",
366
                filter  =>      "(".$rowldap[6]."=%s)"
367
            );
368
            my $method = $r->method;
369 909:cbb26bc654de Chris
            $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
370 0:513646585e45 Chris
371
          }
372
          $sthldap->finish();
373
          undef $sthldap;
374
      }
375
  }
376
  $sth->finish();
377
  undef $sth;
378
  $dbh->disconnect();
379
  undef $dbh;
380
381
  if ($cfg->{RedmineCacheCredsMax} and $ret) {
382
    if (defined $usrprojpass) {
383 909:cbb26bc654de Chris
      $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
384 0:513646585e45 Chris
    } else {
385
      if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
386 909:cbb26bc654de Chris
        $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
387 0:513646585e45 Chris
        $cfg->{RedmineCacheCredsCount}++;
388
      } else {
389
        $cfg->{RedmineCacheCreds}->clear();
390
        $cfg->{RedmineCacheCredsCount} = 0;
391
      }
392
    }
393
  }
394
395
  $ret;
396
}
397
398
sub get_project_identifier {
399
    my $r = shift;
400 909:cbb26bc654de Chris
401 0:513646585e45 Chris
    my $location = $r->location;
402
    my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
403
    $identifier;
404
}
405
406
sub connect_database {
407
    my $r = shift;
408 909:cbb26bc654de Chris
409 0:513646585e45 Chris
    my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
410
    return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
411
}
412
413
1;