Project

General

Profile

Defect #2692 ยป Redmine.pm

Derrick Rapp, 2009-03-17 02:53

 
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
                   u.hashed_password, 
152
                   u.auth_source_id, 
153
                   r.permissions
154
               FROM 
155
                   projects p, 
156
                   users u, 
157
                   members m, 
158
                   roles r
159
               WHERE 
160
                   p.identifier=?
161
               AND u.login=?
162
               AND u.status=1 
163
               AND m.project_id=p.id 
164
               AND m.user_id=u.id 
165
               AND r.id=m.role_id
166
               UNION
167
               SELECT 
168
                   u.hashed_password, 
169
                   u.auth_source_id, 
170
                   r.permissions
171
               FROM 
172
                   projects p, 
173
                   users u, 
174
                   roles r
175
               WHERE 
176
                   p.identifier=?
177
               AND p.is_public=1
178
               AND u.login=?
179
               AND u.status=1 
180
               AND r.id=1 ";
181
  $self->{RedmineQuery} = trim($query);
182
}
183
sub RedmineDbUser { set_val('RedmineDbUser', @_); }
184
sub RedmineDbPass { set_val('RedmineDbPass', @_); }
185
sub RedmineDbWhereClause { 
186
  my ($self, $parms, $arg) = @_;
187
  $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
188
}
189

    
190
sub RedmineCacheCredsMax { 
191
  my ($self, $parms, $arg) = @_;
192
  if ($arg) {
193
    $self->{RedmineCachePool} = APR::Pool->new;
194
    $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
195
    $self->{RedmineCacheCredsCount} = 0;
196
    $self->{RedmineCacheCredsMax} = $arg;
197
  }
198
}
199

    
200
sub trim {
201
  my $string = shift;
202
  $string =~ s/\s{2,}/ /g;
203
  return $string;
204
}
205

    
206
sub set_val {
207
  my ($key, $self, $parms, $arg) = @_;
208
  $self->{$key} = $arg;
209
}
210

    
211
Apache2::Module::add(__PACKAGE__, \@directives);
212

    
213

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

    
216
sub access_handler {
217
  my $r = shift;
218

    
219
  unless ($r->some_auth_required) {
220
      $r->log_reason("No authentication has been configured");
221
      return FORBIDDEN;
222
  }
223

    
224
  my $method = $r->method;
225
  return OK unless defined $read_only_methods{$method};
226

    
227
  my $project_id = get_project_identifier($r);
228

    
229
  $r->set_handlers(PerlAuthenHandler => [\&OK])
230
      if (is_public_project($project_id, $r) && !is_login_required($r));
231

    
232
  return OK
233
}
234

    
235
sub authen_handler {
236
  my $r = shift;
237
  
238
  my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
239
  return $res unless $res == OK;
240
  
241
  if (is_member($r->user, $redmine_pass, $r)) {
242
      return OK;
243
  } else {
244
      $r->note_auth_failure();
245
      return AUTH_REQUIRED;
246
  }
247
}
248

    
249
sub is_login_required {
250
    my $r = shift;
251

    
252
    my $dbh = connect_database($r);
253
    my $sth = $dbh->prepare(
254
        "SELECT value FROM settings WHERE settings.name='login_required';" 
255
    );
256

    
257
    $sth->execute();
258
    my @ret = $sth->fetchrow_array();
259
    $sth->finish();
260
    $dbh->disconnect();
261

    
262
    $ret[0];
263
}
264

    
265
sub is_public_project {
266
    my $project_id = shift;
267
    my $r = shift;
268

    
269
    my $dbh = connect_database($r);
270
    my $sth = $dbh->prepare(
271
        "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
272
    );
273

    
274
    $sth->execute($project_id);
275
    my $ret = $sth->fetchrow_array ? 1 : 0;
276
    $sth->finish();
277
    $dbh->disconnect();
278

    
279
    $ret;
280
}
281

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

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

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

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

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

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

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

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

    
320
    my $method = $r->method;
321
    if (defined $read_only_methods{$method} || $permissions =~ /:commit_access/) {
322
      unless ($auth_source_id) {
323
          if ($hashed_password eq $pass_digest) {
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] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $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
            $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass));
342
          }
343
          $sthldap->finish();
344
      }
345
    }
346
  }
347
  $sth->finish();
348
  $dbh->disconnect();
349

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

    
364
  $ret;
365
}
366

    
367
sub get_project_identifier {
368
    my $r = shift;
369
    
370
    my $location = $r->location;
371
    my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
372
    $identifier;
373
}
374

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

    
382
1;
    (1-1/1)