##// END OF EJS Templates
Makes Redmine.pm work with multiple repositories per project (#779)....
Jean-Philippe Lang -
r9136:f071b39d268a
parent child
Show More
@@ -1,413 +1,422
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 =head1 REPOSITORIES NAMING
86
87 A projet repository must be named with the projet identifier. In case
88 of multiple repositories for the same project, use the project identifier
89 and the repository identifier separated with a dot:
90
91 /var/svn/foo
92 /var/svn/foo.otherrepo
93
85 94 =head1 MIGRATION FROM OLDER RELEASES
86 95
87 96 If you use an older reposman.rb (r860 or before), you need to change
88 97 rights on repositories to allow the apache user to read and write
89 98 S<them :>
90 99
91 100 sudo chown -R www-data /var/svn/*
92 101 sudo chmod -R u+w /var/svn/*
93 102
94 103 And you need to upgrade at least reposman.rb (after r860).
95 104
96 105 =cut
97 106
98 107 use strict;
99 108 use warnings FATAL => 'all', NONFATAL => 'redefine';
100 109
101 110 use DBI;
102 111 use Digest::SHA;
103 112 # optional module for LDAP authentication
104 113 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
105 114
106 115 use Apache2::Module;
107 116 use Apache2::Access;
108 117 use Apache2::ServerRec qw();
109 118 use Apache2::RequestRec qw();
110 119 use Apache2::RequestUtil qw();
111 120 use Apache2::Const qw(:common :override :cmd_how);
112 121 use APR::Pool ();
113 122 use APR::Table ();
114 123
115 124 # use Apache2::Directive qw();
116 125
117 126 my @directives = (
118 127 {
119 128 name => 'RedmineDSN',
120 129 req_override => OR_AUTHCFG,
121 130 args_how => TAKE1,
122 131 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
123 132 },
124 133 {
125 134 name => 'RedmineDbUser',
126 135 req_override => OR_AUTHCFG,
127 136 args_how => TAKE1,
128 137 },
129 138 {
130 139 name => 'RedmineDbPass',
131 140 req_override => OR_AUTHCFG,
132 141 args_how => TAKE1,
133 142 },
134 143 {
135 144 name => 'RedmineDbWhereClause',
136 145 req_override => OR_AUTHCFG,
137 146 args_how => TAKE1,
138 147 },
139 148 {
140 149 name => 'RedmineCacheCredsMax',
141 150 req_override => OR_AUTHCFG,
142 151 args_how => TAKE1,
143 152 errmsg => 'RedmineCacheCredsMax must be decimal number',
144 153 },
145 154 );
146 155
147 156 sub RedmineDSN {
148 157 my ($self, $parms, $arg) = @_;
149 158 $self->{RedmineDSN} = $arg;
150 159 my $query = "SELECT
151 160 hashed_password, salt, auth_source_id, permissions
152 161 FROM projects, users, roles
153 162 WHERE
154 163 users.login=?
155 164 AND projects.identifier=?
156 165 AND users.status=1
157 166 AND (
158 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)
159 168 OR
160 169 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
161 170 ) ";
162 171 $self->{RedmineQuery} = trim($query);
163 172 }
164 173
165 174 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
166 175 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
167 176 sub RedmineDbWhereClause {
168 177 my ($self, $parms, $arg) = @_;
169 178 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
170 179 }
171 180
172 181 sub RedmineCacheCredsMax {
173 182 my ($self, $parms, $arg) = @_;
174 183 if ($arg) {
175 184 $self->{RedmineCachePool} = APR::Pool->new;
176 185 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
177 186 $self->{RedmineCacheCredsCount} = 0;
178 187 $self->{RedmineCacheCredsMax} = $arg;
179 188 }
180 189 }
181 190
182 191 sub trim {
183 192 my $string = shift;
184 193 $string =~ s/\s{2,}/ /g;
185 194 return $string;
186 195 }
187 196
188 197 sub set_val {
189 198 my ($key, $self, $parms, $arg) = @_;
190 199 $self->{$key} = $arg;
191 200 }
192 201
193 202 Apache2::Module::add(__PACKAGE__, \@directives);
194 203
195 204
196 205 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
197 206
198 207 sub access_handler {
199 208 my $r = shift;
200 209
201 210 unless ($r->some_auth_required) {
202 211 $r->log_reason("No authentication has been configured");
203 212 return FORBIDDEN;
204 213 }
205 214
206 215 my $method = $r->method;
207 216 return OK unless defined $read_only_methods{$method};
208 217
209 218 my $project_id = get_project_identifier($r);
210 219
211 220 $r->set_handlers(PerlAuthenHandler => [\&OK])
212 221 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
213 222
214 223 return OK
215 224 }
216 225
217 226 sub authen_handler {
218 227 my $r = shift;
219 228
220 229 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
221 230 return $res unless $res == OK;
222 231
223 232 if (is_member($r->user, $redmine_pass, $r)) {
224 233 return OK;
225 234 } else {
226 235 $r->note_auth_failure();
227 236 return AUTH_REQUIRED;
228 237 }
229 238 }
230 239
231 240 # check if authentication is forced
232 241 sub is_authentication_forced {
233 242 my $r = shift;
234 243
235 244 my $dbh = connect_database($r);
236 245 my $sth = $dbh->prepare(
237 246 "SELECT value FROM settings where settings.name = 'login_required';"
238 247 );
239 248
240 249 $sth->execute();
241 250 my $ret = 0;
242 251 if (my @row = $sth->fetchrow_array) {
243 252 if ($row[0] eq "1" || $row[0] eq "t") {
244 253 $ret = 1;
245 254 }
246 255 }
247 256 $sth->finish();
248 257 undef $sth;
249 258
250 259 $dbh->disconnect();
251 260 undef $dbh;
252 261
253 262 $ret;
254 263 }
255 264
256 265 sub is_public_project {
257 266 my $project_id = shift;
258 267 my $r = shift;
259 268
260 269 if (is_authentication_forced($r)) {
261 270 return 0;
262 271 }
263 272
264 273 my $dbh = connect_database($r);
265 274 my $sth = $dbh->prepare(
266 275 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
267 276 );
268 277
269 278 $sth->execute($project_id);
270 279 my $ret = 0;
271 280 if (my @row = $sth->fetchrow_array) {
272 281 if ($row[0] eq "1" || $row[0] eq "t") {
273 282 $ret = 1;
274 283 }
275 284 }
276 285 $sth->finish();
277 286 undef $sth;
278 287 $dbh->disconnect();
279 288 undef $dbh;
280 289
281 290 $ret;
282 291 }
283 292
284 293 sub anonymous_role_allows_browse_repository {
285 294 my $r = shift;
286 295
287 296 my $dbh = connect_database($r);
288 297 my $sth = $dbh->prepare(
289 298 "SELECT permissions FROM roles WHERE builtin = 2;"
290 299 );
291 300
292 301 $sth->execute();
293 302 my $ret = 0;
294 303 if (my @row = $sth->fetchrow_array) {
295 304 if ($row[0] =~ /:browse_repository/) {
296 305 $ret = 1;
297 306 }
298 307 }
299 308 $sth->finish();
300 309 undef $sth;
301 310 $dbh->disconnect();
302 311 undef $dbh;
303 312
304 313 $ret;
305 314 }
306 315
307 316 # perhaps we should use repository right (other read right) to check public access.
308 317 # it could be faster BUT it doesn't work for the moment.
309 318 # sub is_public_project_by_file {
310 319 # my $project_id = shift;
311 320 # my $r = shift;
312 321
313 322 # my $tree = Apache2::Directive::conftree();
314 323 # my $node = $tree->lookup('Location', $r->location);
315 324 # my $hash = $node->as_hash;
316 325
317 326 # my $svnparentpath = $hash->{SVNParentPath};
318 327 # my $repos_path = $svnparentpath . "/" . $project_id;
319 328 # return 1 if (stat($repos_path))[2] & 00007;
320 329 # }
321 330
322 331 sub is_member {
323 332 my $redmine_user = shift;
324 333 my $redmine_pass = shift;
325 334 my $r = shift;
326 335
327 336 my $dbh = connect_database($r);
328 337 my $project_id = get_project_identifier($r);
329 338
330 339 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
331 340
332 341 my $access_mode = defined $read_only_methods{$r->method} ? "R" : "W";
333 342
334 343 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
335 344 my $usrprojpass;
336 345 if ($cfg->{RedmineCacheCredsMax}) {
337 346 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
338 347 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
339 348 }
340 349 my $query = $cfg->{RedmineQuery};
341 350 my $sth = $dbh->prepare($query);
342 351 $sth->execute($redmine_user, $project_id);
343 352
344 353 my $ret;
345 354 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
346 355
347 356 unless ($auth_source_id) {
348 357 my $method = $r->method;
349 358 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
350 359 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
351 360 $ret = 1;
352 361 last;
353 362 }
354 363 } elsif ($CanUseLDAPAuth) {
355 364 my $sthldap = $dbh->prepare(
356 365 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
357 366 );
358 367 $sthldap->execute($auth_source_id);
359 368 while (my @rowldap = $sthldap->fetchrow_array) {
360 369 my $ldap = Authen::Simple::LDAP->new(
361 370 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
362 371 port => $rowldap[1],
363 372 basedn => $rowldap[5],
364 373 binddn => $rowldap[3] ? $rowldap[3] : "",
365 374 bindpw => $rowldap[4] ? $rowldap[4] : "",
366 375 filter => "(".$rowldap[6]."=%s)"
367 376 );
368 377 my $method = $r->method;
369 378 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
370 379
371 380 }
372 381 $sthldap->finish();
373 382 undef $sthldap;
374 383 }
375 384 }
376 385 $sth->finish();
377 386 undef $sth;
378 387 $dbh->disconnect();
379 388 undef $dbh;
380 389
381 390 if ($cfg->{RedmineCacheCredsMax} and $ret) {
382 391 if (defined $usrprojpass) {
383 392 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
384 393 } else {
385 394 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
386 395 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
387 396 $cfg->{RedmineCacheCredsCount}++;
388 397 } else {
389 398 $cfg->{RedmineCacheCreds}->clear();
390 399 $cfg->{RedmineCacheCredsCount} = 0;
391 400 }
392 401 }
393 402 }
394 403
395 404 $ret;
396 405 }
397 406
398 407 sub get_project_identifier {
399 408 my $r = shift;
400 409
401 410 my $location = $r->location;
402 my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
411 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
403 412 $identifier;
404 413 }
405 414
406 415 sub connect_database {
407 416 my $r = shift;
408 417
409 418 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
410 419 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
411 420 }
412 421
413 422 1;
General Comments 0
You need to be logged in to leave comments. Login now