Project

General

Profile

RE: Redmine.pm will not authenticate ยป Redmine.Modified.For.Project.Name.Use.pm

Eric Desgranges, 2010-11-19 05:18

 
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, 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 projects.name=? ";
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.name = ?;"
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, $auth_source_id, $permissions) = $sth->fetchrow_array) {
320

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

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

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

    
368
  $ret;
369
}
370

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

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

    
386
1;
    (1-1/1)