##// END OF EJS Templates
Merged r7834 from trunk (#9566)....
Jean-Philippe Lang -
r7999:f252108f3063
parent child
Show More
@@ -1,410 +1,411
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 FROM members, projects, users, roles, member_roles
152 FROM projects, users, roles
153 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
154 users.login=?
155 AND projects.identifier=?
158 156 AND users.status=1
159 AND login=?
160 AND identifier=? ";
157 AND (
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 OR
160 (roles.builtin=1 AND cast(projects.is_public as CHAR) IN ('t', '1'))
161 ) ";
161 162 $self->{RedmineQuery} = trim($query);
162 163 }
163 164
164 165 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
165 166 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
166 167 sub RedmineDbWhereClause {
167 168 my ($self, $parms, $arg) = @_;
168 169 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
169 170 }
170 171
171 172 sub RedmineCacheCredsMax {
172 173 my ($self, $parms, $arg) = @_;
173 174 if ($arg) {
174 175 $self->{RedmineCachePool} = APR::Pool->new;
175 176 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
176 177 $self->{RedmineCacheCredsCount} = 0;
177 178 $self->{RedmineCacheCredsMax} = $arg;
178 179 }
179 180 }
180 181
181 182 sub trim {
182 183 my $string = shift;
183 184 $string =~ s/\s{2,}/ /g;
184 185 return $string;
185 186 }
186 187
187 188 sub set_val {
188 189 my ($key, $self, $parms, $arg) = @_;
189 190 $self->{$key} = $arg;
190 191 }
191 192
192 193 Apache2::Module::add(__PACKAGE__, \@directives);
193 194
194 195
195 196 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
196 197
197 198 sub access_handler {
198 199 my $r = shift;
199 200
200 201 unless ($r->some_auth_required) {
201 202 $r->log_reason("No authentication has been configured");
202 203 return FORBIDDEN;
203 204 }
204 205
205 206 my $method = $r->method;
206 207 return OK unless defined $read_only_methods{$method};
207 208
208 209 my $project_id = get_project_identifier($r);
209 210
210 211 $r->set_handlers(PerlAuthenHandler => [\&OK])
211 212 if is_public_project($project_id, $r) && anonymous_role_allows_browse_repository($r);
212 213
213 214 return OK
214 215 }
215 216
216 217 sub authen_handler {
217 218 my $r = shift;
218 219
219 220 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
220 221 return $res unless $res == OK;
221 222
222 223 if (is_member($r->user, $redmine_pass, $r)) {
223 224 return OK;
224 225 } else {
225 226 $r->note_auth_failure();
226 227 return AUTH_REQUIRED;
227 228 }
228 229 }
229 230
230 231 # check if authentication is forced
231 232 sub is_authentication_forced {
232 233 my $r = shift;
233 234
234 235 my $dbh = connect_database($r);
235 236 my $sth = $dbh->prepare(
236 237 "SELECT value FROM settings where settings.name = 'login_required';"
237 238 );
238 239
239 240 $sth->execute();
240 241 my $ret = 0;
241 242 if (my @row = $sth->fetchrow_array) {
242 243 if ($row[0] eq "1" || $row[0] eq "t") {
243 244 $ret = 1;
244 245 }
245 246 }
246 247 $sth->finish();
247 248 undef $sth;
248 249
249 250 $dbh->disconnect();
250 251 undef $dbh;
251 252
252 253 $ret;
253 254 }
254 255
255 256 sub is_public_project {
256 257 my $project_id = shift;
257 258 my $r = shift;
258 259
259 260 if (is_authentication_forced($r)) {
260 261 return 0;
261 262 }
262 263
263 264 my $dbh = connect_database($r);
264 265 my $sth = $dbh->prepare(
265 266 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
266 267 );
267 268
268 269 $sth->execute($project_id);
269 270 my $ret = 0;
270 271 if (my @row = $sth->fetchrow_array) {
271 272 if ($row[0] eq "1" || $row[0] eq "t") {
272 273 $ret = 1;
273 274 }
274 275 }
275 276 $sth->finish();
276 277 undef $sth;
277 278 $dbh->disconnect();
278 279 undef $dbh;
279 280
280 281 $ret;
281 282 }
282 283
283 284 sub anonymous_role_allows_browse_repository {
284 285 my $r = shift;
285 286
286 287 my $dbh = connect_database($r);
287 288 my $sth = $dbh->prepare(
288 289 "SELECT permissions FROM roles WHERE builtin = 2;"
289 290 );
290 291
291 292 $sth->execute();
292 293 my $ret = 0;
293 294 if (my @row = $sth->fetchrow_array) {
294 295 if ($row[0] =~ /:browse_repository/) {
295 296 $ret = 1;
296 297 }
297 298 }
298 299 $sth->finish();
299 300 undef $sth;
300 301 $dbh->disconnect();
301 302 undef $dbh;
302 303
303 304 $ret;
304 305 }
305 306
306 307 # perhaps we should use repository right (other read right) to check public access.
307 308 # it could be faster BUT it doesn't work for the moment.
308 309 # sub is_public_project_by_file {
309 310 # my $project_id = shift;
310 311 # my $r = shift;
311 312
312 313 # my $tree = Apache2::Directive::conftree();
313 314 # my $node = $tree->lookup('Location', $r->location);
314 315 # my $hash = $node->as_hash;
315 316
316 317 # my $svnparentpath = $hash->{SVNParentPath};
317 318 # my $repos_path = $svnparentpath . "/" . $project_id;
318 319 # return 1 if (stat($repos_path))[2] & 00007;
319 320 # }
320 321
321 322 sub is_member {
322 323 my $redmine_user = shift;
323 324 my $redmine_pass = shift;
324 325 my $r = shift;
325 326
326 327 my $dbh = connect_database($r);
327 328 my $project_id = get_project_identifier($r);
328 329
329 330 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
330 331
331 332 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
332 333 my $usrprojpass;
333 334 if ($cfg->{RedmineCacheCredsMax}) {
334 335 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
335 336 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
336 337 }
337 338 my $query = $cfg->{RedmineQuery};
338 339 my $sth = $dbh->prepare($query);
339 340 $sth->execute($redmine_user, $project_id);
340 341
341 342 my $ret;
342 343 while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
343 344
344 345 unless ($auth_source_id) {
345 346 my $method = $r->method;
346 347 my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest);
347 348 if ($hashed_password eq $salted_password && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
348 349 $ret = 1;
349 350 last;
350 351 }
351 352 } elsif ($CanUseLDAPAuth) {
352 353 my $sthldap = $dbh->prepare(
353 354 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
354 355 );
355 356 $sthldap->execute($auth_source_id);
356 357 while (my @rowldap = $sthldap->fetchrow_array) {
357 358 my $ldap = Authen::Simple::LDAP->new(
358 359 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
359 360 port => $rowldap[1],
360 361 basedn => $rowldap[5],
361 362 binddn => $rowldap[3] ? $rowldap[3] : "",
362 363 bindpw => $rowldap[4] ? $rowldap[4] : "",
363 364 filter => "(".$rowldap[6]."=%s)"
364 365 );
365 366 my $method = $r->method;
366 367 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
367 368
368 369 }
369 370 $sthldap->finish();
370 371 undef $sthldap;
371 372 }
372 373 }
373 374 $sth->finish();
374 375 undef $sth;
375 376 $dbh->disconnect();
376 377 undef $dbh;
377 378
378 379 if ($cfg->{RedmineCacheCredsMax} and $ret) {
379 380 if (defined $usrprojpass) {
380 381 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
381 382 } else {
382 383 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
383 384 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
384 385 $cfg->{RedmineCacheCredsCount}++;
385 386 } else {
386 387 $cfg->{RedmineCacheCreds}->clear();
387 388 $cfg->{RedmineCacheCredsCount} = 0;
388 389 }
389 390 }
390 391 }
391 392
392 393 $ret;
393 394 }
394 395
395 396 sub get_project_identifier {
396 397 my $r = shift;
397 398
398 399 my $location = $r->location;
399 400 my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
400 401 $identifier;
401 402 }
402 403
403 404 sub connect_database {
404 405 my $r = shift;
405 406
406 407 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
407 408 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
408 409 }
409 410
410 411 1;
General Comments 0
You need to be logged in to leave comments. Login now