comparison extra/svn/Redmine.pm @ 0:513646585e45

* Import Redmine trunk SVN rev 3859
author Chris Cannam
date Fri, 23 Jul 2010 15:52:44 +0100
parents
children 94944d00e43c
comparison
equal deleted inserted replaced
-1:000000000000 0:513646585e45
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 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, $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[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;