Redmine.pm

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

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
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
117

    
118
my $cfg;
119

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

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

    
143
  if ($r->dir_config('RedmineCacheCredsMax')) {
144
	  $cfg->{RedmineCachePool} = APR::Pool->new;
145
	  $cfg->{RedmineCacheCreds} = APR::Table::make($cfg->{RedmineCachePool}, $r->dir_config('RedmineCacheCredsMax'));
146
	  $cfg->{RedmineCacheCredsCount} = 0;
147
	  $cfg->{RedmineCacheCredsMax} = $r->dir_config('RedmineCacheCredsMax');
148
  }
149
  
150
  my $query = trim("SELECT 
151
                 hashed_password, 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

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

    
164
  return $ret;
165
}
166

    
167

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

    
174

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

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

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

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

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

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

    
196
  return OK
197
}
198

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

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

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

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

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

    
240
  $ret;
241
}
242

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

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

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

    
268
    $ret;
269
}
270

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

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

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

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

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

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

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

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

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

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

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

    
356
  $ret;
357
}
358

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

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

    
371
1;