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 / .svn / text-base / Redmine.pm.svn-base @ 442:753f1380d6bc

History | View | Annotate | Download (10.6 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 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
sub RedmineDSN { 
148
  my ($self, $parms, $arg) = @_;
149
  $self->{RedmineDSN} = $arg;
150
  my $query = "SELECT 
151
                 hashed_password, salt, auth_source_id, permissions
152
              FROM members, projects, users, roles, member_roles
153
              WHERE 
154
                projects.id=members.project_id
155
                AND member_roles.member_id=members.id
156
                AND users.id=members.user_id 
157
                AND roles.id=member_roles.role_id
158
                AND users.status=1 
159
                AND login=? 
160
                AND identifier=? ";
161
  $self->{RedmineQuery} = trim($query);
162
}
163

    
164
sub RedmineDbUser { set_val('RedmineDbUser', @_); }
165
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
166
sub RedmineDbWhereClause { 
167
  my ($self, $parms, $arg) = @_;
168
  $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
169
}
170

    
171
sub RedmineCacheCredsMax { 
172
  my ($self, $parms, $arg) = @_;
173
  if ($arg) {
174
    $self->{RedmineCachePool} = APR::Pool->new;
175
    $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
176
    $self->{RedmineCacheCredsCount} = 0;
177
    $self->{RedmineCacheCredsMax} = $arg;
178
  }
179
}
180

    
181
sub trim {
182
  my $string = shift;
183
  $string =~ s/\s{2,}/ /g;
184
  return $string;
185
}
186

    
187
sub set_val {
188
  my ($key, $self, $parms, $arg) = @_;
189
  $self->{$key} = $arg;
190
}
191

    
192
Apache2::Module::add(__PACKAGE__, \@directives);
193

    
194

    
195
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
196

    
197
sub access_handler {
198
  my $r = shift;
199

    
200
  unless ($r->some_auth_required) {
201
      $r->log_reason("No authentication has been configured");
202
      return FORBIDDEN;
203
  }
204

    
205
  my $method = $r->method;
206
  return OK unless defined $read_only_methods{$method};
207

    
208
  my $project_id = get_project_identifier($r);
209

    
210
  $r->set_handlers(PerlAuthenHandler => [\&OK])
211
      if is_public_project($project_id, $r);
212

    
213
  return OK
214
}
215

    
216
sub authen_handler {
217
  my $r = shift;
218
  
219
  my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
220
  return $res unless $res == OK;
221
  
222
  if (is_member($r->user, $redmine_pass, $r)) {
223
      return OK;
224
  } else {
225
      $r->note_auth_failure();
226
      return AUTH_REQUIRED;
227
  }
228
}
229

    
230
# check if authentication is forced
231
sub is_authentication_forced {
232
  my $r = shift;
233

    
234
  my $dbh = connect_database($r);
235
  my $sth = $dbh->prepare(
236
    "SELECT value FROM settings where settings.name = 'login_required';"
237
  );
238

    
239
  $sth->execute();
240
  my $ret = 0;
241
  if (my @row = $sth->fetchrow_array) {
242
    if ($row[0] eq "1" || $row[0] eq "t") {
243
      $ret = 1;
244
    }
245
  }
246
  $sth->finish();
247
  undef $sth;
248
  
249
  $dbh->disconnect();
250
  undef $dbh;
251

    
252
  $ret;
253
}
254

    
255
sub is_public_project {
256
    my $project_id = shift;
257
    my $r = shift;
258
    
259
    if (is_authentication_forced($r)) {
260
      return 0;
261
    }
262

    
263
    my $dbh = connect_database($r);
264
    my $sth = $dbh->prepare(
265
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
266
    );
267

    
268
    $sth->execute($project_id);
269
    my $ret = 0;
270
    if (my @row = $sth->fetchrow_array) {
271
    	if ($row[0] eq "1" || $row[0] eq "t") {
272
    		$ret = 1;
273
    	}
274
    }
275
    $sth->finish();
276
    undef $sth;
277
    $dbh->disconnect();
278
    undef $dbh;
279

    
280
    $ret;
281
}
282

    
283
# perhaps we should use repository right (other read right) to check public access.
284
# it could be faster BUT it doesn't work for the moment.
285
# sub is_public_project_by_file {
286
#     my $project_id = shift;
287
#     my $r = shift;
288

    
289
#     my $tree = Apache2::Directive::conftree();
290
#     my $node = $tree->lookup('Location', $r->location);
291
#     my $hash = $node->as_hash;
292

    
293
#     my $svnparentpath = $hash->{SVNParentPath};
294
#     my $repos_path = $svnparentpath . "/" . $project_id;
295
#     return 1 if (stat($repos_path))[2] & 00007;
296
# }
297

    
298
sub is_member {
299
  my $redmine_user = shift;
300
  my $redmine_pass = shift;
301
  my $r = shift;
302

    
303
  my $dbh         = connect_database($r);
304
  my $project_id  = get_project_identifier($r);
305

    
306
  my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
307

    
308
  my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
309
  my $usrprojpass;
310
  if ($cfg->{RedmineCacheCredsMax}) {
311
    $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
312
    return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
313
  }
314
  my $query = $cfg->{RedmineQuery};
315
  my $sth = $dbh->prepare($query);
316
  $sth->execute($redmine_user, $project_id);
317

    
318
  my $ret;
319
  while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
320

    
321
      unless ($auth_source_id) {
322
	  			my $method = $r->method;
323
          my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest);
324
					if ($hashed_password eq $salted_password && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
325
              $ret = 1;
326
              last;
327
          }
328
      } elsif ($CanUseLDAPAuth) {
329
          my $sthldap = $dbh->prepare(
330
              "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
331
          );
332
          $sthldap->execute($auth_source_id);
333
          while (my @rowldap = $sthldap->fetchrow_array) {
334
            my $ldap = Authen::Simple::LDAP->new(
335
                host    =>      ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
336
                port    =>      $rowldap[1],
337
                basedn  =>      $rowldap[5],
338
                binddn  =>      $rowldap[3] ? $rowldap[3] : "",
339
                bindpw  =>      $rowldap[4] ? $rowldap[4] : "",
340
                filter  =>      "(".$rowldap[6]."=%s)"
341
            );
342
            my $method = $r->method;
343
            $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
344

    
345
          }
346
          $sthldap->finish();
347
          undef $sthldap;
348
      }
349
  }
350
  $sth->finish();
351
  undef $sth;
352
  $dbh->disconnect();
353
  undef $dbh;
354

    
355
  if ($cfg->{RedmineCacheCredsMax} and $ret) {
356
    if (defined $usrprojpass) {
357
      $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
358
    } else {
359
      if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
360
        $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
361
        $cfg->{RedmineCacheCredsCount}++;
362
      } else {
363
        $cfg->{RedmineCacheCreds}->clear();
364
        $cfg->{RedmineCacheCredsCount} = 0;
365
      }
366
    }
367
  }
368

    
369
  $ret;
370
}
371

    
372
sub get_project_identifier {
373
    my $r = shift;
374
    
375
    my $location = $r->location;
376
    my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
377
    $identifier;
378
}
379

    
380
sub connect_database {
381
    my $r = shift;
382
    
383
    my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
384
    return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
385
}
386

    
387
1;