Redmine.pm

Modified Redmine.pm based on version 1.2.1 - Tiemo Vorschuetz, 2011-09-07 09:58

Download (10.8 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
     PerlSetVar RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55
     ## for postgres
56
     # PerlSetVar RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57

    
58
     PerlSetVar RedmineDbUser "redmine"
59
     PerlSetVar RedmineDbPass "password"
60
     ## Optional where clause (fulltext search would be slow and
61
     ## database dependant).
62
     # PerlSetVar RedmineDbWhereClause "and members.role_id IN (1,2)"
63
     ## Optional credentials cache size
64
     # PerlSetVar 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 $cfg;
118

    
119
sub initialize {
120
  my $r = shift;
121
  my $ret = 1;
122
  
123
  $cfg->{RedmineDSN} = $r->dir_config('RedmineDSN');
124
  $cfg->{RedmineDbUser} = $r->dir_config('RedmineDbUser');
125
  $cfg->{RedmineDbPass} = $r->dir_config('RedmineDbPass'); 
126

    
127
  unless ($cfg->{RedmineDSN) {
128
	$r->log_reason("No DSN has been configured. Maybe PerlSetVar is missing at the beginning of the line");
129
	$ret = 0;
130
  } 
131
  
132
  unless ($cfg->{RedmineDbUser}) {
133
	$r->log_reason("No DSN user has been configured. Maybe PerlSetVar is missing at the beginning of the line");
134
	$ret = 0;
135
  }
136
  
137
  unless ($cfg->{RedmineDbPass}) {
138
	$r->log_reason("No DSN user password has been configured. Maybe PerlSetVar is missing at the beginning of the line");
139
	$ret = 0;
140
  }
141

    
142
  if ($r->dir_config('RedmineCacheCredsMax')) {
143
	  $cfg->{RedmineCachePool} = APR::Pool->new;
144
	  $cfg->{RedmineCacheCreds} = APR::Table::make($cfg->{RedmineCachePool}, $r->dir_config('RedmineCacheCredsMax'));
145
	  $cfg->{RedmineCacheCredsCount} = 0;
146
	  $cfg->{RedmineCacheCredsMax} = $r->dir_config('RedmineCacheCredsMax');
147
  }
148
  
149
  my $query = trim("SELECT 
150
                 hashed_password, auth_source_id, permissions
151
              FROM members, projects, users, roles, member_roles
152
              WHERE 
153
                projects.id=members.project_id
154
                AND member_roles.member_id=members.id
155
                AND users.id=members.user_id 
156
                AND roles.id=member_roles.role_id
157
                AND users.status=1 
158
                AND login=? 
159
                AND identifier=? ");
160

    
161
  $cfg->{RedmineQuery} = trim($query.($r->dir_config('RedmineDbWhereClause') ? $r->dir_config('RedmineDbWhereClause') : "")." ");
162

    
163
  return $ret;
164
}
165

    
166

    
167
sub trim {
168
  my $string = shift;
169
  $string =~ s/\s{2,}/ /g;
170
  return $string;
171
}
172

    
173

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

    
176
sub access_handler {
177
  my $r = shift;
178

    
179
  unless (initialize($r)) {
180
      $r->log_reason("Please check redmine connfiguration");
181
      return FORBIDDEN;
182
  }
183

    
184
  unless ($r->some_auth_required) {
185
      $r->log_reason("No authentication has been configured");
186
      return FORBIDDEN;
187
  }
188

    
189
  my $method = $r->method;
190
  return OK unless defined $read_only_methods{$method};
191

    
192
  my $project_id = get_project_identifier($r);
193

    
194
  $r->set_handlers(PerlAuthenHandler => [\&OK])
195
      if is_public_project($project_id, $r);
196

    
197
  return OK
198
}
199

    
200
sub authen_handler {
201
  my $r = shift;
202
  
203
  unless (initialize($r)) {
204
      $r->log_reason("Please check redmine connfiguration");
205
      return AUTH_REQUIRED;
206
  }
207

    
208
  my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
209
  return $res unless $res == OK;
210
  
211
  if (is_member($r->user, $redmine_pass, $r)) {
212
      return OK;
213
  } else {
214
      $r->note_auth_failure();
215
      return AUTH_REQUIRED;
216
  }
217
}
218

    
219
# check if authentication is forced
220
sub is_authentication_forced {
221
  my $r = shift;
222

    
223
  my $dbh = connect_database($r);
224
  my $sth = $dbh->prepare(
225
    "SELECT value FROM settings where settings.name = 'login_required';"
226
  );
227

    
228
  $sth->execute();
229
  my $ret = 0;
230
  if (my @row = $sth->fetchrow_array) {
231
    if ($row[0] eq "1" || $row[0] eq "t") {
232
      $ret = 1;
233
    }
234
  }
235
  $sth->finish();
236
  undef $sth;
237
  
238
  $dbh->disconnect();
239
  undef $dbh;
240

    
241
  $ret;
242
}
243

    
244
sub is_public_project {
245
    my $project_id = shift;
246
    my $r = shift;
247
    
248
    if (is_authentication_forced($r)) {
249
      return 0;
250
    }
251

    
252
    my $dbh = connect_database($r);
253
    my $sth = $dbh->prepare(
254
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
255
    );
256

    
257
    $sth->execute($project_id);
258
    my $ret = 0;
259
    if (my @row = $sth->fetchrow_array) {
260
    	if ($row[0] eq "1" || $row[0] eq "t") {
261
    		$ret = 1;
262
    	}
263
    }
264
    $sth->finish();
265
    undef $sth;
266
    $dbh->disconnect();
267
    undef $dbh;
268

    
269
    $ret;
270
}
271

    
272
# perhaps we should use repository right (other read right) to check public access.
273
# it could be faster BUT it doesn't work for the moment.
274
# sub is_public_project_by_file {
275
#     my $project_id = shift;
276
#     my $r = shift;
277

    
278
#     my $tree = Apache2::Directive::conftree();
279
#     my $node = $tree->lookup('Location', $r->location);
280
#     my $hash = $node->as_hash;
281

    
282
#     my $svnparentpath = $hash->{SVNParentPath};
283
#     my $repos_path = $svnparentpath . "/" . $project_id;
284
#     return 1 if (stat($repos_path))[2] & 00007;
285
# }
286

    
287
sub is_member {
288
  my $redmine_user = shift;
289
  my $redmine_pass = shift;
290
  my $r = shift;
291

    
292
  my $dbh         = connect_database($r);
293
  my $project_id  = get_project_identifier($r);
294

    
295
  my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
296

    
297
  my $usrprojpass;
298
  if ($cfg->{RedmineCacheCredsMax}) {
299
    $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
300
    return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
301
  }
302
  my $query = $cfg->{RedmineQuery};
303
  my $sth = $dbh->prepare($query);
304
  $sth->execute($redmine_user, $project_id);
305

    
306
  my $ret;
307
  while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
308

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

    
333
          }
334
          $sthldap->finish();
335
          undef $sthldap;
336
      }
337
  }
338
  $sth->finish();
339
  undef $sth;
340
  $dbh->disconnect();
341
  undef $dbh;
342

    
343
  if ($cfg->{RedmineCacheCredsMax} and $ret) {
344
    if (defined $usrprojpass) {
345
      $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
346
    } else {
347
      if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
348
        $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
349
        $cfg->{RedmineCacheCredsCount}++;
350
      } else {
351
        $cfg->{RedmineCacheCreds}->clear();
352
        $cfg->{RedmineCacheCredsCount} = 0;
353
      }
354
    }
355
  }
356

    
357
  $ret;
358
}
359

    
360
sub get_project_identifier {
361
    my $r = shift;
362
    
363
    my $location = $r->location;
364
    my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
365
    $identifier;
366
}
367

    
368
sub connect_database {
369
    return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
370
}
371

    
372
1;