To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / extra / soundsoftware / SoundSoftware.pm @ 442:753f1380d6bc

History | View | Annotate | Download (11.8 KB)

1
package Apache::Authn::SoundSoftware;
2

    
3
=head1 Apache::Authn::SoundSoftware
4

    
5
SoundSoftware - a mod_perl module for Apache authentication against a
6
Redmine database and optional LDAP implementing the access control
7
rules required for the SoundSoftware.ac.uk repository site.
8

    
9
=head1 SYNOPSIS
10

    
11
This module is closely based on the Redmine.pm authentication module
12
provided with Redmine.  It is intended to be used for authentication
13
in front of a repository service such as hgwebdir.
14

    
15
Requirements:
16

    
17
1. Clone/pull from repo for public project: Any user, no
18
authentication required
19

    
20
2. Clone/pull from repo for private project: Project members only
21

    
22
3. Push to repo for public project: "Permitted" users only (this
23
probably means project members who are also identified in the hgrc web
24
section for the repository and so will be approved by hgwebdir?)
25

    
26
4. Push to repo for private project: "Permitted" users only (as above)
27

    
28
=head1 INSTALLATION
29

    
30
Debian/ubuntu:
31

    
32
  apt-get install libapache-dbi-perl libapache2-mod-perl2 \
33
    libdbd-mysql-perl libauthen-simple-ldap-perl libio-socket-ssl-perl
34

    
35
Note that LDAP support is hardcoded "on" in this script (it is
36
optional in the original Redmine.pm).
37

    
38
=head1 CONFIGURATION
39

    
40
   ## This module has to be in your perl path
41
   ## eg:  /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm
42
   PerlLoadModule Apache::Authn::SoundSoftware
43

    
44
   # Example when using hgwebdir
45
   ScriptAlias / "/var/hg/hgwebdir.cgi/"
46

    
47
   <Location />
48
       AuthName "Mercurial"
49
       AuthType Basic
50
       Require valid-user
51
       PerlAccessHandler Apache::Authn::SoundSoftware::access_handler
52
       PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler
53
       SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost"
54
       SoundSoftwareDbUser "redmine"
55
       SoundSoftwareDbPass "password"
56
       Options +ExecCGI
57
       AddHandler cgi-script .cgi
58
       ## Optional where clause (fulltext search would be slow and
59
       ## database dependant).
60
       # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)"
61
       ## Optional prefix for local repository URLs
62
       # SoundSoftwareRepoPrefix "/var/hg/"
63
  </Location>
64

    
65
See the original Redmine.pm for further configuration notes.
66

    
67
=cut
68

    
69
use strict;
70
use warnings FATAL => 'all', NONFATAL => 'redefine';
71

    
72
use DBI;
73
use Digest::SHA1;
74
use Authen::Simple::LDAP;
75
use Apache2::Module;
76
use Apache2::Access;
77
use Apache2::ServerRec qw();
78
use Apache2::RequestRec qw();
79
use Apache2::RequestUtil qw();
80
use Apache2::Const qw(:common :override :cmd_how);
81
use APR::Pool ();
82
use APR::Table ();
83

    
84
my @directives = (
85
  {
86
    name => 'SoundSoftwareDSN',
87
    req_override => OR_AUTHCFG,
88
    args_how => TAKE1,
89
    errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
90
  },
91
  {
92
    name => 'SoundSoftwareDbUser',
93
    req_override => OR_AUTHCFG,
94
    args_how => TAKE1,
95
  },
96
  {
97
    name => 'SoundSoftwareDbPass',
98
    req_override => OR_AUTHCFG,
99
    args_how => TAKE1,
100
  },
101
  {
102
    name => 'SoundSoftwareDbWhereClause',
103
    req_override => OR_AUTHCFG,
104
    args_how => TAKE1,
105
  },
106
  {
107
    name => 'SoundSoftwareRepoPrefix',
108
    req_override => OR_AUTHCFG,
109
    args_how => TAKE1,
110
  },
111
);
112

    
113
sub SoundSoftwareDSN { 
114
    my ($self, $parms, $arg) = @_;
115
    $self->{SoundSoftwareDSN} = $arg;
116
    my $query = "SELECT 
117
                 hashed_password, auth_source_id, permissions
118
              FROM members, projects, users, roles, member_roles
119
              WHERE 
120
                projects.id=members.project_id
121
                AND member_roles.member_id=members.id
122
                AND users.id=members.user_id 
123
                AND roles.id=member_roles.role_id
124
                AND users.status=1 
125
                AND login=? 
126
                AND identifier=? ";
127
    $self->{SoundSoftwareQuery} = trim($query);
128
}
129

    
130
sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); }
131
sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); }
132
sub SoundSoftwareDbWhereClause { 
133
    my ($self, $parms, $arg) = @_;
134
    $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." ");
135
}
136

    
137
sub SoundSoftwareRepoPrefix { 
138
    my ($self, $parms, $arg) = @_;
139
    if ($arg) {
140
	$self->{SoundSoftwareRepoPrefix} = $arg;
141
    }
142
}
143

    
144
sub trim {
145
    my $string = shift;
146
    $string =~ s/\s{2,}/ /g;
147
    return $string;
148
}
149

    
150
sub set_val {
151
    my ($key, $self, $parms, $arg) = @_;
152
    $self->{$key} = $arg;
153
}
154

    
155
Apache2::Module::add(__PACKAGE__, \@directives);
156

    
157

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

    
160
sub access_handler {
161
    my $r = shift;
162

    
163
    print STDERR "SoundSoftware.pm: In access handler at " . scalar localtime() . "\n";
164

    
165
    unless ($r->some_auth_required) {
166
	$r->log_reason("No authentication has been configured");
167
	return FORBIDDEN;
168
    }
169

    
170
    my $method = $r->method;
171

    
172
    print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n";
173
    print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n";
174

    
175
    if (!defined $read_only_methods{$method}) {
176
	print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n";
177
	return OK;
178
    }
179

    
180
    my $dbh = connect_database($r);
181
    unless ($dbh) {
182
	print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n";
183
	return FORBIDDEN;
184
    }
185

    
186

    
187
print STDERR "Connected to db, dbh is " . $dbh . "\n";
188

    
189
    my $project_id = get_project_identifier($dbh, $r);
190
    my $status = get_project_status($dbh, $project_id, $r);
191

    
192
    $dbh->disconnect();
193
    undef $dbh;
194

    
195
    if ($status == 0) { # nonexistent
196
	print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n";
197
	return FORBIDDEN;
198
    } elsif ($status == 1) { # public
199
	print STDERR "SoundSoftware.pm: Project is public, no restriction here\n";
200
	$r->set_handlers(PerlAuthenHandler => [\&OK])
201
    } else { # private
202
	print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n";
203
    }
204

    
205
    return OK
206
}
207

    
208
sub authen_handler {
209
    my $r = shift;
210
    
211
    print STDERR "SoundSoftware.pm: In authentication handler at " . scalar localtime() . "\n";
212

    
213
    my $dbh = connect_database($r);
214
    unless ($dbh) {
215
        print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n";
216
        return AUTH_REQUIRED;
217
    }
218
    
219
    my $project_id = get_project_identifier($dbh, $r);
220
    my $realm = get_realm($dbh, $project_id, $r);
221
    $r->auth_name($realm);
222

    
223
    my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
224
    unless ($res == OK) {
225
	$dbh->disconnect();
226
	undef $dbh;
227
	return $res;
228
    }
229
    
230
    print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n";
231

    
232
    my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r);
233
    
234
    $dbh->disconnect();
235
    undef $dbh;
236

    
237
    if ($permitted) {
238
	return OK;
239
    } else {
240
	print STDERR "SoundSoftware.pm: Not permitted\n";
241
	$r->note_auth_failure();
242
	return AUTH_REQUIRED;
243
    }
244
}
245

    
246
sub get_project_status {
247
    my $dbh = shift;
248
    my $project_id = shift;
249
    my $r = shift;
250

    
251
    if (!defined $project_id or $project_id eq '') {
252
	return 0; # nonexistent
253
    }
254
    
255
    my $sth = $dbh->prepare(
256
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
257
    );
258

    
259
    $sth->execute($project_id);
260
    my $ret = 0; # nonexistent
261
    if (my @row = $sth->fetchrow_array) {
262
    	if ($row[0] eq "1" || $row[0] eq "t") {
263
	    $ret = 1; # public
264
    	} else {
265
	    $ret = 2; # private
266
	}
267
    }
268
    $sth->finish();
269
    undef $sth;
270

    
271
    $ret;
272
}
273

    
274
sub is_permitted {
275
    my $dbh = shift;
276
    my $project_id = shift;
277
    my $redmine_user = shift;
278
    my $redmine_pass = shift;
279
    my $r = shift;
280

    
281
    my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
282

    
283
    my $cfg = Apache2::Module::get_config
284
	(__PACKAGE__, $r->server, $r->per_dir_config);
285

    
286
    my $query = $cfg->{SoundSoftwareQuery};
287
    my $sth = $dbh->prepare($query);
288
    $sth->execute($redmine_user, $project_id);
289

    
290
    my $ret;
291
    while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
292

    
293
	# Test permissions for this user before we verify credentials
294
	# -- if the user is not permitted this action anyway, there's
295
	# not much point in e.g. contacting the LDAP
296

    
297
	my $method = $r->method;
298

    
299
	if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/)
300
	    || $permissions =~ /:commit_access/) {
301

    
302
	    # User would be permitted this action, if their
303
	    # credentials checked out -- test those now
304

    
305
	    print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n";
306

    
307
	    unless ($auth_source_id) {
308
		if ($hashed_password eq $pass_digest) {
309
		    print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n";
310
		    $ret = 1;
311
		    last;
312
		}
313
	    } else {
314
		my $sthldap = $dbh->prepare(
315
		    "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;"
316
		    );
317
		$sthldap->execute($auth_source_id);
318
		while (my @rowldap = $sthldap->fetchrow_array) {
319
		    my $ldap = Authen::Simple::LDAP->new(
320
			host    => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
321
			port    => $rowldap[1],
322
			basedn  => $rowldap[5],
323
			binddn  => $rowldap[3] ? $rowldap[3] : "",
324
			bindpw  => $rowldap[4] ? $rowldap[4] : "",
325
			filter  => "(".$rowldap[6]."=%s)"
326
			);
327
		    if ($ldap->authenticate($redmine_user, $redmine_pass)) {
328
			print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n";
329
			$ret = 1;
330
		    }
331
		}
332
		$sthldap->finish();
333
		undef $sthldap;
334
	    }
335
	} else {
336
	    print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n";
337
	}
338
    }
339

    
340
    $sth->finish();
341
    undef $sth;
342

    
343
    $ret;
344
}
345

    
346
sub get_project_identifier {
347
    my $dbh = shift;
348
    my $r = shift;
349

    
350
    my $location = $r->location;
351
    my ($repo) = $r->uri =~ m{$location/*([^/]+)};
352

    
353
    return $repo if (!$repo);
354

    
355
    $repo =~ s/[^a-zA-Z0-9\._-]//g;
356

    
357
    # The original Redmine.pm returns the string just calculated as
358
    # the project identifier.  That won't do for us -- we may have
359
    # (and in fact already do have, in our test instance) projects
360
    # whose repository names differ from the project identifiers.
361

    
362
    # This is a rather fundamental change because it means that almost
363
    # every request needs more than one database query -- which
364
    # prompts us to start passing around $dbh instead of connecting
365
    # locally within each function as is done in Redmine.pm.
366

    
367
    my $sth = $dbh->prepare(
368
        "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;"
369
    );
370

    
371
    my $cfg = Apache2::Module::get_config
372
	(__PACKAGE__, $r->server, $r->per_dir_config);
373

    
374
    my $prefix = $cfg->{SoundSoftwareRepoPrefix};
375
    if (!defined $prefix) { $prefix = '%/'; }
376

    
377
    my $identifier = '';
378

    
379
    $sth->execute($prefix . $repo);
380
    my $ret = 0;
381
    if (my @row = $sth->fetchrow_array) {
382
	$identifier = $row[0];
383
    }
384
    $sth->finish();
385
    undef $sth;
386

    
387
    print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n";
388

    
389
    $identifier;
390
}
391

    
392
sub get_realm {
393
    my $dbh = shift;
394
    my $project_id = shift;
395
    my $r = shift;
396

    
397
    my $sth = $dbh->prepare(
398
        "SELECT projects.name FROM projects WHERE projects.identifier = ?;"
399
    );
400

    
401
    my $name = $project_id;
402

    
403
    $sth->execute($project_id);
404
    my $ret = 0;
405
    if (my @row = $sth->fetchrow_array) {
406
	$name = $row[0];
407
    }
408
    $sth->finish();
409
    undef $sth;
410

    
411
    # be timid about characters not permitted in auth realm and revert
412
    # to project identifier if any are found
413
    if ($name =~ m/[^\w\d\s\._-]/) {
414
	$name = $project_id;
415
    }
416

    
417
    my $realm = '"Mercurial repository for ' . "'$name'" . '"';
418

    
419
    $realm;
420
}
421

    
422
sub connect_database {
423
    my $r = shift;
424
    
425
    my $cfg = Apache2::Module::get_config
426
	(__PACKAGE__, $r->server, $r->per_dir_config);
427

    
428
    return DBI->connect($cfg->{SoundSoftwareDSN},
429
	                $cfg->{SoundSoftwareDbUser},
430
		        $cfg->{SoundSoftwareDbPass});
431
}
432

    
433
1;