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