##// END OF EJS Templates
Merged r9755 from trunk....
Jean-Philippe Lang -
r9573:5f23ebc31c8f
parent child
Show More
@@ -1,422 +1,429
1 1 package Apache::Authn::Redmine;
2 2
3 3 =head1 Apache::Authn::Redmine
4 4
5 5 Redmine - a mod_perl module to authenticate webdav subversion users
6 6 against redmine database
7 7
8 8 =head1 SYNOPSIS
9 9
10 10 This module allow anonymous users to browse public project and
11 11 registred users to browse and commit their project. Authentication is
12 12 done against the redmine database or the LDAP configured in redmine.
13 13
14 14 This method is far simpler than the one with pam_* and works with all
15 15 database without an hassle but you need to have apache/mod_perl on the
16 16 svn server.
17 17
18 18 =head1 INSTALLATION
19 19
20 20 For this to automagically work, you need to have a recent reposman.rb
21 21 (after r860) and if you already use reposman, read the last section to
22 22 migrate.
23 23
24 24 Sorry ruby users but you need some perl modules, at least mod_perl2,
25 25 DBI and DBD::mysql (or the DBD driver for you database as it should
26 26 work on allmost all databases).
27 27
28 28 On debian/ubuntu you must do :
29 29
30 30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31 31
32 32 If your Redmine users use LDAP authentication, you will also need
33 33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34 34
35 35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36 36
37 37 =head1 CONFIGURATION
38 38
39 39 ## This module has to be in your perl path
40 40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 41 PerlLoadModule Apache::Authn::Redmine
42 42 <Location /svn>
43 43 DAV svn
44 44 SVNParentPath "/var/svn"
45 45
46 46 AuthType Basic
47 47 AuthName redmine
48 48 Require valid-user
49 49
50 50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52 52
53 53 ## for mysql
54 54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 55 ## for postgres
56 56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57 57
58 58 RedmineDbUser "redmine"
59 59 RedmineDbPass "password"
60 60 ## Optional where clause (fulltext search would be slow and
61 61 ## database dependant).
62 62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 63 ## Optional credentials cache size
64 64 # RedmineCacheCredsMax 50
65 65 </Location>
66 66
67 67 To be able to browse repository inside redmine, you must add something
68 68 like that :
69 69
70 70 <Location /svn-private>
71 71 DAV svn
72 72 SVNParentPath "/var/svn"
73 73 Order deny,allow
74 74 Deny from all
75 75 # only allow reading orders
76 76 <Limit GET PROPFIND OPTIONS REPORT>
77 77 Allow from redmine.server.ip
78 78 </Limit>
79 79 </Location>
80 80
81 81 and you will have to use this reposman.rb command line to create repository :
82 82
83 83 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84 84
85 85 =head1 REPOSITORIES NAMING
86 86
87 87 A projet repository must be named with the projet identifier. In case
88 88 of multiple repositories for the same project, use the project identifier
89 89 and the repository identifier separated with a dot:
90 90
91 91 /var/svn/foo
92 92 /var/svn/foo.otherrepo
93 93
94 94 =head1 MIGRATION FROM OLDER RELEASES
95 95
96 96 If you use an older reposman.rb (r860 or before), you need to change
97 97 rights on repositories to allow the apache user to read and write
98 98 S<them :>
99 99
100 100 sudo chown -R www-data /var/svn/*
101 101 sudo chmod -R u+w /var/svn/*
102 102
103 103 And you need to upgrade at least reposman.rb (after r860).
104 104
105 105 =cut
106 106
107 107 use strict;
108 108 use warnings FATAL => 'all', NONFATAL => 'redefine';
109 109
110 110 use DBI;
111 111 use Digest::SHA;
112 112 # optional module for LDAP authentication
113 113 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
114 114
115 115 use Apache2::Module;
116 116 use Apache2::Access;
117 117 use Apache2::ServerRec qw();
118 118 use Apache2::RequestRec qw();
119 119 use Apache2::RequestUtil qw();
120 120 use Apache2::Const qw(:common :override :cmd_how);
121 121 use APR::Pool ();
122 122 use APR::Table ();
123 123
124 124 # use Apache2::Directive qw();
125 125
126 126 my @directives = (
127 127 {
128 128 name => 'RedmineDSN',
129 129 req_override => OR_AUTHCFG,
130 130 args_how => TAKE1,
131 131 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
132 132 },
133 133 {
134 134 name => 'RedmineDbUser',
135 135 req_override => OR_AUTHCFG,
136 136 args_how => TAKE1,
137 137 },
138 138 {
139 139 name => 'RedmineDbPass',
140 140 req_override => OR_AUTHCFG,
141 141 args_how => TAKE1,
142 142 },
143 143 {
144 144 name => 'RedmineDbWhereClause',
145 145 req_override => OR_AUTHCFG,
146 146 args_how => TAKE1,
147 147 },
148 148 {
149 149 name => 'RedmineCacheCredsMax',
150 150 req_override => OR_AUTHCFG,
151 151 args_how => TAKE1,
152 152 errmsg => 'RedmineCacheCredsMax must be decimal number',
153 153 },
154 154 );
155 155
156 156 sub RedmineDSN {
157 157 my ($self, $parms, $arg) = @_;
158 158 $self->{RedmineDSN} = $arg;
159 159 my $query = "SELECT
160 160 hashed_password, salt, auth_source_id, permissions
161 161 FROM projects, users, roles
162 162 WHERE
163 163 users.login=?
164 164 AND projects.identifier=?
165 165 AND users.status=1
166 166 AND (
167 167 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
168 168 OR
169 169 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
170 170 ) ";
171 171 $self->{RedmineQuery} = trim($query);
172 172 }
173 173
174 174 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
175 175 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
176 176 sub RedmineDbWhereClause {
177 177 my ($self, $parms, $arg) = @_;
178 178 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
179 179 }
180 180
181 181 sub RedmineCacheCredsMax {
182 182 my ($self, $parms, $arg) = @_;
183 183 if ($arg) {
184 184 $self->{RedmineCachePool} = APR::Pool->new;
185 185 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
186 186 $self->{RedmineCacheCredsCount} = 0;
187 187 $self->{RedmineCacheCredsMax} = $arg;
188 188 }
189 189 }
190 190
191 191 sub trim {
192 192 my $string = shift;
193 193 $string =~ s/\s{2,}/ /g;
194 194 return $string;
195 195 }
196 196
197 197 sub set_val {
198 198 my ($key, $self, $parms, $arg) = @_;
199 199 $self->{$key} = $arg;
200 200 }
201 201
202 202 Apache2::Module::add(__PACKAGE__, \@directives);
203 203
204 204
205 205 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
206 206
207 207 sub access_handler {
208 208 my $r = shift;
209 209
210 210 unless ($r->some_auth_required) {
211 211 $r->log_reason("No authentication has been configured");
212 212 return FORBIDDEN;
213 213 }
214 214
215 215 my $method = $r->method;
216 216 return OK unless defined $read_only_methods{$method};
217 217
218 218 my $project_id = get_project_identifier($r);
219 219
220 220 $r->set_handlers(PerlAuthenHandler => [\&OK])
221 221 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
222 222
223 223 return OK
224 224 }
225 225
226 226 sub authen_handler {
227 227 my $r = shift;
228 228
229 229 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
230 230 return $res unless $res == OK;
231 231
232 232 if (is_member($r->user, $redmine_pass, $r)) {
233 233 return OK;
234 234 } else {
235 235 $r->note_auth_failure();
236 236 return AUTH_REQUIRED;
237 237 }
238 238 }
239 239
240 240 # check if authentication is forced
241 241 sub is_authentication_forced {
242 242 my $r = shift;
243 243
244 244 my $dbh = connect_database($r);
245 245 my $sth = $dbh->prepare(
246 246 "SELECT value FROM settings where settings.name = 'login_required';"
247 247 );
248 248
249 249 $sth->execute();
250 250 my $ret = 0;
251 251 if (my @row = $sth->fetchrow_array) {
252 252 if ($row[0] eq "1" || $row[0] eq "t") {
253 253 $ret = 1;
254 254 }
255 255 }
256 256 $sth->finish();
257 257 undef $sth;
258 258
259 259 $dbh->disconnect();
260 260 undef $dbh;
261 261
262 262 $ret;
263 263 }
264 264
265 265 sub is_public_project {
266 266 my $project_id = shift;
267 267 my $r = shift;
268 268
269 269 if (is_authentication_forced($r)) {
270 270 return 0;
271 271 }
272 272
273 273 my $dbh = connect_database($r);
274 274 my $sth = $dbh->prepare(
275 275 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
276 276 );
277 277
278 278 $sth->execute($project_id);
279 279 my $ret = 0;
280 280 if (my @row = $sth->fetchrow_array) {
281 281 if ($row[0] eq "1" || $row[0] eq "t") {
282 282 $ret = 1;
283 283 }
284 284 }
285 285 $sth->finish();
286 286 undef $sth;
287 287 $dbh->disconnect();
288 288 undef $dbh;
289 289
290 290 $ret;
291 291 }
292 292
293 293 sub anonymous_role_allows_browse_repository {
294 294 my $r = shift;
295 295
296 296 my $dbh = connect_database($r);
297 297 my $sth = $dbh->prepare(
298 298 "SELECT permissions FROM roles WHERE builtin = 2;"
299 299 );
300 300
301 301 $sth->execute();
302 302 my $ret = 0;
303 303 if (my @row = $sth->fetchrow_array) {
304 304 if ($row[0] =~ /:browse_repository/) {
305 305 $ret = 1;
306 306 }
307 307 }
308 308 $sth->finish();
309 309 undef $sth;
310 310 $dbh->disconnect();
311 311 undef $dbh;
312 312
313 313 $ret;
314 314 }
315 315
316 316 # perhaps we should use repository right (other read right) to check public access.
317 317 # it could be faster BUT it doesn't work for the moment.
318 318 # sub is_public_project_by_file {
319 319 # my $project_id = shift;
320 320 # my $r = shift;
321 321
322 322 # my $tree = Apache2::Directive::conftree();
323 323 # my $node = $tree->lookup('Location', $r->location);
324 324 # my $hash = $node->as_hash;
325 325
326 326 # my $svnparentpath = $hash->{SVNParentPath};
327 327 # my $repos_path = $svnparentpath . "/" . $project_id;
328 328 # return 1 if (stat($repos_path))[2] & 00007;
329 329 # }
330 330
331 331 sub is_member {
332 332 my $redmine_user = shift;
333 333 my $redmine_pass = shift;
334 334 my $r = shift;
335 335
336 336 my $dbh = connect_database($r);
337 337 my $project_id = get_project_identifier($r);
338 338
339 339 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
340 340
341 341 my $access_mode = defined $read_only_methods{$r->method} ? "R" : "W";
342 342
343 343 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
344 344 my $usrprojpass;
345 345 if ($cfg->{RedmineCacheCredsMax}) {
346 346 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
347 347 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
348 348 }
349 349 my $query = $cfg->{RedmineQuery};
350 350 my $sth = $dbh->prepare($query);
351 351 $sth->execute($redmine_user, $project_id);
352 352
353 353 my $ret;
354 354 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
355 355
356 356 unless ($auth_source_id) {
357 357 my $method = $r->method;
358 358 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
359 359 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
360 360 $ret = 1;
361 361 last;
362 362 }
363 363 } elsif ($CanUseLDAPAuth) {
364 364 my $sthldap = $dbh->prepare(
365 365 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
366 366 );
367 367 $sthldap->execute($auth_source_id);
368 368 while (my @rowldap = $sthldap->fetchrow_array) {
369 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
370 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
371 if ($bind_as =~ m/\$login/) {
372 # replace $login with $redmine_user and use $redmine_pass
373 $bind_as =~ s/\$login/$redmine_user/g;
374 $bind_pw = $redmine_pass
375 }
369 376 my $ldap = Authen::Simple::LDAP->new(
370 377 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
371 378 port => $rowldap[1],
372 379 basedn => $rowldap[5],
373 binddn => $rowldap[3] ? $rowldap[3] : "",
374 bindpw => $rowldap[4] ? $rowldap[4] : "",
380 binddn => $bind_as,
381 bindpw => $bind_pw,
375 382 filter => "(".$rowldap[6]."=%s)"
376 383 );
377 384 my $method = $r->method;
378 385 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
379 386
380 387 }
381 388 $sthldap->finish();
382 389 undef $sthldap;
383 390 }
384 391 }
385 392 $sth->finish();
386 393 undef $sth;
387 394 $dbh->disconnect();
388 395 undef $dbh;
389 396
390 397 if ($cfg->{RedmineCacheCredsMax} and $ret) {
391 398 if (defined $usrprojpass) {
392 399 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
393 400 } else {
394 401 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
395 402 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
396 403 $cfg->{RedmineCacheCredsCount}++;
397 404 } else {
398 405 $cfg->{RedmineCacheCreds}->clear();
399 406 $cfg->{RedmineCacheCredsCount} = 0;
400 407 }
401 408 }
402 409 }
403 410
404 411 $ret;
405 412 }
406 413
407 414 sub get_project_identifier {
408 415 my $r = shift;
409 416
410 417 my $location = $r->location;
411 418 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
412 419 $identifier;
413 420 }
414 421
415 422 sub connect_database {
416 423 my $r = shift;
417 424
418 425 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
419 426 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
420 427 }
421 428
422 429 1;
General Comments 0
You need to be logged in to leave comments. Login now