Project

General

Profile

Redmine.pm with support for public projects and platform ... ยป Redmine.pm

Modified Redmine.pm - Thilo Paul-Stueve, 2011-02-11 17:09

 
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 allows anonymous users to browse public project 
11
repositories if platform authentication is disabled. Authenticated 
12
user with browse_repository permission (reporter, developer, manager,
13
non-members) are allowed to read the repository, authenticated users 
14
with commit_repository permissions (developer, manager) are allowed 
15
to write to the repository.
16

    
17
This method is far simpler than the one with pam_* and works with all
18
database without an hassle but you need to have apache/mod_perl on the
19
svn server.
20

    
21
=head1 INSTALLATION
22

    
23
For this to automagically work, you need to have a recent reposman.rb
24
(after r860) and if you already use reposman, read the last section to
25
migrate.
26

    
27
Sorry ruby users but you need some perl modules, at least mod_perl2,
28
DBI and DBD::mysql (or the DBD driver for you database as it should
29
work on allmost all databases).
30

    
31
On debian/ubuntu you must do :
32

    
33
  aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
34

    
35
If your Redmine users use LDAP authentication, you will also need
36
Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
37

    
38
  aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
39

    
40
=head1 CONFIGURATION
41

    
42
   ## This module has to be in your perl path
43
   ## eg:  /usr/lib/perl5/Apache/Authn/Redmine.pm
44
   PerlLoadModule Apache::Authn::Redmine
45
   <Location /svn>
46
     DAV svn
47
     SVNParentPath "/var/svn"
48

    
49
     AuthType Basic
50
     AuthName redmine
51
     Require valid-user
52

    
53
     PerlAccessHandler Apache::Authn::Redmine::access_handler
54
     PerlAuthenHandler Apache::Authn::Redmine::authen_handler
55
  
56
     ## for mysql
57
     RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
58
     ## for postgres
59
     # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
60

    
61
     RedmineDbUser "redmine"
62
     RedmineDbPass "password"
63
     
64
     ## Optional credentials cache size
65
     # RedmineCacheCredsMax 50
66
  </Location>
67

    
68
To be able to browse repository inside redmine, you must add something
69
like that :
70

    
71
   <Location /svn-private>
72
     DAV svn
73
     SVNParentPath "/var/svn"
74
     Order deny,allow
75
     Deny from all
76
     # only allow reading orders
77
     <Limit GET PROPFIND OPTIONS REPORT>
78
       Allow from redmine.server.ip
79
     </Limit>
80
   </Location>
81

    
82
and you will have to use this reposman.rb command line to create repository :
83

    
84
  reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
85

    
86
=head1 MIGRATION FROM OLDER RELEASES
87

    
88
If you use an older reposman.rb (r860 or before), you need to change
89
rights on repositories to allow the apache user to read and write
90
S<them :>
91

    
92
  sudo chown -R www-data /var/svn/*
93
  sudo chmod -R u+w /var/svn/*
94

    
95
And you need to upgrade at least reposman.rb (after r860).
96

    
97
=cut
98

    
99
use strict;
100
use warnings FATAL => 'all', NONFATAL => 'redefine';
101
use DBI;
102
use Digest::SHA1;
103

    
104
# optional module for LDAP authentication
105
my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
106

    
107
use Apache2::Module;
108
use Apache2::Access;
109
use Apache2::ServerRec qw();
110
use Apache2::RequestRec qw();
111
use Apache2::RequestUtil qw();
112
use Apache2::Const qw(:common :override :cmd_how);
113
use APR::Pool  ();
114
use APR::Table ();
115

    
116
# use Apache2::Directive qw();
117

    
118
my @directives = (
119
	{   name         => 'RedmineDSN',
120
		req_override => OR_AUTHCFG,
121
		args_how     => TAKE1,
122
		errmsg =>
123
			'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
124
	},
125
	{   name         => 'RedmineDbUser',
126
		req_override => OR_AUTHCFG,
127
		args_how     => TAKE1,
128
	},
129
	{   name         => 'RedmineDbPass',
130
		req_override => OR_AUTHCFG,
131
		args_how     => TAKE1,
132
	},
133
	{   name         => 'RedmineCacheCredsMax',
134
		req_override => OR_AUTHCFG,
135
		args_how     => TAKE1,
136
		errmsg       => 'RedmineCacheCredsMax must be decimal number',
137
	},
138
);
139

    
140
sub RedmineDSN {
141
	my ( $self, $parms, $arg ) = @_;
142
	$self->{RedmineDSN} = $arg;
143
}
144

    
145
sub RedmineDbUser {
146
	set_val( 'RedmineDbUser', @_ );
147
}
148

    
149
sub RedmineDbPass {
150
	set_val( 'RedmineDbPass', @_ );
151
}
152

    
153
sub RedmineCacheCredsMax {
154
	my ( $self, $parms, $arg ) = @_;
155
	if ($arg) {
156
		$self->{RedmineCachePool} = APR::Pool->new;
157
		$self->{RedmineCacheCreds}
158
			= APR::Table::make( $self->{RedmineCachePool}, $arg );
159
		$self->{RedmineCacheCredsCount} = 0;
160
		$self->{RedmineCacheCredsMax}   = $arg;
161
	}
162
}
163

    
164
sub trim {
165
	my $string = shift;
166
	$string =~ s/\s{2,}/ /g;
167
	return $string;
168
}
169

    
170
sub set_val {
171
	my ( $key, $self, $parms, $arg ) = @_;
172
	$self->{$key} = $arg;
173
}
174

    
175
Apache2::Module::add( __PACKAGE__, \@directives );
176

    
177
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
178

    
179
sub access_handler {
180
	my $r = shift;
181

    
182
	# return FORBIDDEN if no authentication has been configured (means: if no auth is required here)
183
	# default security
184
	unless ( $r->some_auth_required ) {
185
		return FORBIDDEN;
186
	}
187

    
188
	# return OK if method requested is not one of the read only methods
189
	my $method = $r->method;
190
	unless ( defined $read_only_methods{$method} ) {
191
		return OK;
192
	}
193

    
194
	# Disable authentication for public projects if authentication is not forced
195
	# write method requests will not reach till here
196
	my $project_id = get_project_identifier($r);
197
	if ( is_public_project( $project_id, $r )
198
		&& !( is_authentication_forced($r) ) )
199
	{
200
		$r->set_handlers( PerlAuthenHandler => [ \&OK ] );
201
	}
202

    
203
	#Fall-through OK
204
	return OK;
205
}
206

    
207
sub authen_handler {
208
	my $r = shift;
209

    
210
	# Get password from request header, return error if failed
211
	my ( $res, $apache_password ) = $r->get_basic_auth_pw();
212
	unless ( $res == OK ) {
213
		return $res;
214
	}
215
	my $apache_user = $r->user;
216

    
217
	# is user authorised? check authentication & authorisation with respect to public projects
218
	if ( is_authorised( $apache_user, $apache_password, $r ) ) {
219
		return OK;
220
	}
221

    
222
	# Fall-through AUTH_REQUIRED
223
	$r->note_auth_failure();
224
	return AUTH_REQUIRED;
225
}
226

    
227
# check if platfrom authentication is forced
228
sub is_authentication_forced {
229
	my $r   = shift;
230
	my $dbh = connect_database($r);
231
	my $sth = $dbh->prepare(
232
		"SELECT value FROM settings where settings.name = 'login_required';");
233
	$sth->execute();
234
	my $ret = 0;
235
	if ( my @row = $sth->fetchrow_array ) {
236
		if ( $row[0] eq "1" || $row[0] eq "t" ) {
237
			$ret = 1;
238
		}
239
	}
240
	$sth->finish();
241
	undef $sth;
242
	$dbh->disconnect();
243
	undef $dbh;
244
	return $ret;
245
}
246

    
247
# obtain project identifier
248
sub get_project_identifier {
249
	my $r        = shift;
250
	my $location = $r->location;
251
	my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
252
	return $identifier;
253
}
254

    
255
# check if public project
256
sub is_public_project {
257
	my $project_id = shift;
258
	my $r          = shift;
259
	my $dbh        = connect_database($r);
260
	my $sth        = $dbh->prepare(
261
		"SELECT is_public FROM projects WHERE projects.identifier = ?;");
262
	$sth->execute($project_id);
263
	my $ret = 0;
264
	if ( my @row = $sth->fetchrow_array ) {
265
		if ( $row[0] eq "1" || $row[0] eq "t" ) {
266
			$ret = 1;
267
		}
268
	}
269
	$sth->finish();
270
	undef $sth;
271
	$dbh->disconnect();
272
	undef $dbh;
273
	return $ret;
274
}
275

    
276
# get the authentication source of the user
277
sub get_auth_source_id {
278
	my $r           = shift;
279
	my $apache_user = shift;
280
	my $ret         = 0;
281
	my $dbh         = connect_database($r);
282
	my $sth
283
		= $dbh->prepare("SELECT auth_source_id FROM users u WHERE login= ?;");
284
	$sth->execute($apache_user);
285
	if ( my @row = $sth->fetchrow_array ) {
286
		$ret = $row[0];
287
	}
288
	$sth->finish();
289
	undef $sth;
290
	$dbh->disconnect();
291
	undef $dbh;
292
	return $ret;
293
}
294

    
295
# get the project permissions of the user
296
sub get_permissions {
297
	my $r           = shift;
298
	my $apache_user = shift;
299
	my $project_id  = shift;
300
	my $ret         = "";
301
	my $dbh         = connect_database($r);
302
	my $query       = trim(
303
		"SELECT permissions
304
	    FROM members, projects, users, roles, member_roles 
305
	    WHERE projects.id=members.project_id 
306
	    AND member_roles.member_id=members.id 
307
	    AND users.id=members.user_id 
308
	    AND roles.id=member_roles.role_id 
309
	    AND users.status=1 
310
	    AND login=?
311
	    AND identifier=?"
312
	);
313
	my $sth = $dbh->prepare($query);
314
	$sth->execute( $apache_user, $project_id );
315

    
316
	if ( my @row = $sth->fetchrow_array ) {
317
		$ret = $row[0];
318
	}
319
	$sth->finish();
320
	undef $sth;
321
	$dbh->disconnect();
322
	undef $dbh;
323
	return $ret;
324
}
325

    
326
# check user credentials (ldap first, where most of our users live; you may want to change this)
327
sub is_authenticated {
328
	my $r               = shift;
329
	my $apache_user     = shift;
330
	my $apache_password = shift;
331
	my $ret = (is_authenticated_in_ldap( $r, $apache_user, $apache_password )
332
			|| is_authenticated_in_db( $r, $apache_user, $apache_password ) );
333
	return $ret;
334
}
335

    
336
# check database for authenticatin
337
sub is_authenticated_in_db {
338
	my $r                      = shift;
339
	my $apache_user            = shift;
340
	my $apache_password        = shift;
341
	my $apache_password_digest = Digest::SHA1::sha1_hex($apache_password);
342
	my $dbh                    = connect_database($r);
343
	my $sth                    = $dbh->prepare(
344
		"SELECT hashed_password FROM users u WHERE login = ?;");
345
	$sth->execute($apache_user);
346
	my $ret = 0;
347

    
348
	if ( my @row = $sth->fetchrow_array ) {
349
		if ( $row[0] eq $apache_password_digest ) {
350
			$ret = 1;
351
		}
352
	}
353
	$sth->finish();
354
	undef $sth;
355
	$dbh->disconnect();
356
	undef $dbh;
357
	return $ret;
358
}
359

    
360
# check ldap directory for authentication
361
sub is_authenticated_in_ldap {
362
	my $r               = shift;
363
	my $apache_user     = shift;
364
	my $apache_password = shift;
365
	my $auth_source_id  = get_auth_source_id( $r, $apache_user );
366
	my $ret             = 0;
367
	my $dbh             = connect_database($r);
368
	my $sthldap
369
		= $dbh->prepare(
370
		"SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
371
		);
372
	$sthldap->execute($auth_source_id);
373

    
374
	while ( my @rowldap = $sthldap->fetchrow_array ) {
375
		my $ldap = Authen::Simple::LDAP->new(
376
			host => ( $rowldap[2] eq "1" || $rowldap[2] eq "t" )
377
			? "ldaps://$rowldap[0]"
378
			: $rowldap[0],
379
			port   => $rowldap[1],
380
			basedn => $rowldap[5],
381
			binddn => $rowldap[3] ? $rowldap[3] : "",
382
			bindpw => $rowldap[4] ? $rowldap[4] : "",
383
			filter => "(" . $rowldap[6] . "=%s)"
384
		);
385
		if ( $ldap->authenticate( $apache_user, $apache_password ) ) {
386
			$ret = 1;
387
		}
388
	}
389
	$sthldap->finish();
390
	undef $sthldap;
391
	$dbh->disconnect();
392
	undef $dbh;
393
	return $ret;
394
}
395

    
396
# check authorisation (and authentication) with respect to public projects
397
sub is_authorised {
398
	my $apache_user     = shift;
399
	my $apache_password = shift;
400
	my $r               = shift;
401
	my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server,
402
		$r->per_dir_config );
403
	my $project_id             = get_project_identifier($r);
404
	my $method                 = $r->method;
405
	my $apache_password_digest = Digest::SHA1::sha1_hex($apache_password);
406
	my $cache_password_digest;
407
	my $authorised = 0;
408

    
409
	# look in cache first
410
	if ( $cfg->{RedmineCacheCredsMax} ) {
411
		$cache_password_digest = $cfg->{RedmineCacheCreds}
412
			->get( $apache_user . ":" . $project_id . ":" . $method );
413
		if ( defined $cache_password_digest
414
			and ( $cache_password_digest eq $apache_password_digest ) )
415
		{
416
			return 1;
417
		}
418
	}
419

    
420
	# get permissions
421
	my $permissions = get_permissions( $r, $apache_user, $project_id );
422

    
423
	# check if authenticated and has required permissions in project
424
	if (is_authenticated( $r, $apache_user, $apache_password )
425
		&& ((   defined $read_only_methods{$method}
426
				&& ( $permissions =~ /:browse_repository/
427
					|| is_public_project( $project_id, $r ) )
428
			)
429
			|| $permissions =~ /:commit_access/
430
		)
431
		)
432
	{
433
		$authorised = 1;
434
	}
435

    
436
	# update cache
437
	if ( $cfg->{RedmineCacheCredsMax} and $authorised ) {
438
		if ( defined $cache_password_digest ) {
439
			$cfg->{RedmineCacheCreds}
440
				->set( $apache_user . ":" . $project_id . ":" . $method,
441
				$apache_password_digest );
442
		}
443
		else {
444
			if ( $cfg->{RedmineCacheCredsCount}
445
				< $cfg->{RedmineCacheCredsMax} )
446
			{
447
				$cfg->{RedmineCacheCreds}
448
					->set( $apache_user . ":" . $project_id . ":" . $method,
449
					$apache_password_digest );
450
				$cfg->{RedmineCacheCredsCount}++;
451
			}
452
			else {
453
				$cfg->{RedmineCacheCreds}->clear();
454
				$cfg->{RedmineCacheCredsCount} = 0;
455
			}
456
		}
457
	}
458
	return $authorised;
459
}
460

    
461
# database connection
462
sub connect_database {
463
	my $r   = shift;
464
	my $cfg = Apache2::Module::get_config( __PACKAGE__, $r->server,
465
		$r->per_dir_config );
466
	return DBI->connect( $cfg->{RedmineDSN}, $cfg->{RedmineDbUser},
467
		$cfg->{RedmineDbPass} );
468
}
469

    
470
1;
    (1-1/1)