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 @ 440:6253d777aa12

History | View | Annotate | Download (12.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
5. Push to any repo that is tracking an external repo: Refused always
29

    
30
=head1 INSTALLATION
31

    
32
Debian/ubuntu:
33

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

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

    
40
=head1 CONFIGURATION
41

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

    
46
   # Example when using hgwebdir
47
   ScriptAlias / "/var/hg/hgwebdir.cgi/"
48

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

    
67
See the original Redmine.pm for further configuration notes.
68

    
69
=cut
70

    
71
use strict;
72
use warnings FATAL => 'all', NONFATAL => 'redefine';
73

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

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

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

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

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

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

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

    
157
Apache2::Module::add(__PACKAGE__, \@directives);
158

    
159

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

    
162
sub access_handler {
163
    my $r = shift;
164

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

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

    
172
    my $method = $r->method;
173

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

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

    
183
    print STDERR "Connected to db, dbh is " . $dbh . "\n";
184

    
185
    my $project_id = get_project_identifier($dbh, $r);
186

    
187
    if (!defined $read_only_methods{$method}) {
188
        print STDERR "SoundSoftware.pm: Method is not read-only\n";
189
        if (project_repo_is_readonly($dbh, $project_id, $r)) {
190
            print STDERR "SoundSoftware.pm: Project repo is read-only, refusing access\n";
191
	    return FORBIDDEN;
192
        } else {
193
	    print STDERR "SoundSoftware.pm: Project repo is read-write, authentication handler required\n";
194
            return OK;
195
        }
196
    }
197

    
198
    my $status = get_project_status($dbh, $project_id, $r);
199

    
200
    $dbh->disconnect();
201
    undef $dbh;
202

    
203
    if ($status == 0) { # nonexistent
204
	print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n";
205
	return FORBIDDEN;
206
    } elsif ($status == 1) { # public
207
	print STDERR "SoundSoftware.pm: Project is public, no restriction here\n";
208
	$r->set_handlers(PerlAuthenHandler => [\&OK])
209
    } else { # private
210
	print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n";
211
    }
212

    
213
    return OK
214
}
215

    
216
sub authen_handler {
217
    my $r = shift;
218
    
219
    print STDERR "SoundSoftware.pm: In authentication handler at " . scalar localtime() . "\n";
220

    
221
    my $dbh = connect_database($r);
222
    unless ($dbh) {
223
        print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n";
224
        return AUTH_REQUIRED;
225
    }
226
    
227
    my $project_id = get_project_identifier($dbh, $r);
228
    my $realm = get_realm($dbh, $project_id, $r);
229
    $r->auth_name($realm);
230

    
231
    my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
232
    unless ($res == OK) {
233
	$dbh->disconnect();
234
	undef $dbh;
235
	return $res;
236
    }
237
    
238
    print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n";
239

    
240
    my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r);
241
    
242
    $dbh->disconnect();
243
    undef $dbh;
244

    
245
    if ($permitted) {
246
	return OK;
247
    } else {
248
	print STDERR "SoundSoftware.pm: Not permitted\n";
249
	$r->note_auth_failure();
250
	return AUTH_REQUIRED;
251
    }
252
}
253

    
254
sub get_project_status {
255
    my $dbh = shift;
256
    my $project_id = shift;
257
    my $r = shift;
258

    
259
    if (!defined $project_id or $project_id eq '') {
260
	return 0; # nonexistent
261
    }
262
    
263
    my $sth = $dbh->prepare(
264
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
265
    );
266

    
267
    $sth->execute($project_id);
268
    my $ret = 0; # nonexistent
269
    if (my @row = $sth->fetchrow_array) {
270
    	if ($row[0] eq "1" || $row[0] eq "t") {
271
	    $ret = 1; # public
272
    	} else {
273
	    $ret = 2; # private
274
	}
275
    }
276
    $sth->finish();
277
    undef $sth;
278

    
279
    $ret;
280
}
281

    
282
sub project_repo_is_readonly {
283
    my $dbh = shift;
284
    my $project_id = shift;
285
    my $r = shift;
286

    
287
    if (!defined $project_id or $project_id eq '') {
288
        return 0; # nonexistent
289
    }
290

    
291
    my $sth = $dbh->prepare(
292
        "SELECT repositories.is_external FROM repositories, projects WHERE projects.identifier = ? AND repositories.project_id = projects.id;"
293
    );
294

    
295
    $sth->execute($project_id);
296
    my $ret = 0; # nonexistent
297
    if (my @row = $sth->fetchrow_array) {
298
        if (defined($row[0]) && ($row[0] eq "1" || $row[0] eq "t")) {
299
            $ret = 1; # read-only (i.e. external)
300
        } else {
301
            $ret = 0; # read-write
302
        }
303
    }
304
    $sth->finish();
305
    undef $sth;
306

    
307
    $ret;
308
}
309

    
310
sub is_permitted {
311
    my $dbh = shift;
312
    my $project_id = shift;
313
    my $redmine_user = shift;
314
    my $redmine_pass = shift;
315
    my $r = shift;
316

    
317
    my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
318

    
319
    my $cfg = Apache2::Module::get_config
320
	(__PACKAGE__, $r->server, $r->per_dir_config);
321

    
322
    my $query = $cfg->{SoundSoftwareQuery};
323
    my $sth = $dbh->prepare($query);
324
    $sth->execute($redmine_user, $project_id);
325

    
326
    my $ret;
327
    while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
328

    
329
	# Test permissions for this user before we verify credentials
330
	# -- if the user is not permitted this action anyway, there's
331
	# not much point in e.g. contacting the LDAP
332

    
333
	my $method = $r->method;
334

    
335
	if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/)
336
	    || $permissions =~ /:commit_access/) {
337

    
338
	    # User would be permitted this action, if their
339
	    # credentials checked out -- test those now
340

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

    
343
	    unless ($auth_source_id) {
344
		if ($hashed_password eq $pass_digest) {
345
		    print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n";
346
		    $ret = 1;
347
		    last;
348
		}
349
	    } else {
350
		my $sthldap = $dbh->prepare(
351
		    "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;"
352
		    );
353
		$sthldap->execute($auth_source_id);
354
		while (my @rowldap = $sthldap->fetchrow_array) {
355
		    my $ldap = Authen::Simple::LDAP->new(
356
			host    => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
357
			port    => $rowldap[1],
358
			basedn  => $rowldap[5],
359
			binddn  => $rowldap[3] ? $rowldap[3] : "",
360
			bindpw  => $rowldap[4] ? $rowldap[4] : "",
361
			filter  => "(".$rowldap[6]."=%s)"
362
			);
363
		    if ($ldap->authenticate($redmine_user, $redmine_pass)) {
364
			print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n";
365
			$ret = 1;
366
		    }
367
		}
368
		$sthldap->finish();
369
		undef $sthldap;
370
	    }
371
	} else {
372
	    print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n";
373
	}
374
    }
375

    
376
    $sth->finish();
377
    undef $sth;
378

    
379
    $ret;
380
}
381

    
382
sub get_project_identifier {
383
    my $dbh = shift;
384
    my $r = shift;
385

    
386
    my $location = $r->location;
387
    my ($repo) = $r->uri =~ m{$location/*([^/]+)};
388

    
389
    return $repo if (!$repo);
390

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

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

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

    
403
    my $sth = $dbh->prepare(
404
        "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;"
405
    );
406

    
407
    my $cfg = Apache2::Module::get_config
408
	(__PACKAGE__, $r->server, $r->per_dir_config);
409

    
410
    my $prefix = $cfg->{SoundSoftwareRepoPrefix};
411
    if (!defined $prefix) { $prefix = '%/'; }
412

    
413
    my $identifier = '';
414

    
415
    $sth->execute($prefix . $repo);
416
    my $ret = 0;
417
    if (my @row = $sth->fetchrow_array) {
418
	$identifier = $row[0];
419
    }
420
    $sth->finish();
421
    undef $sth;
422

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

    
425
    $identifier;
426
}
427

    
428
sub get_realm {
429
    my $dbh = shift;
430
    my $project_id = shift;
431
    my $r = shift;
432

    
433
    my $sth = $dbh->prepare(
434
        "SELECT projects.name FROM projects WHERE projects.identifier = ?;"
435
    );
436

    
437
    my $name = $project_id;
438

    
439
    $sth->execute($project_id);
440
    my $ret = 0;
441
    if (my @row = $sth->fetchrow_array) {
442
	$name = $row[0];
443
    }
444
    $sth->finish();
445
    undef $sth;
446

    
447
    # be timid about characters not permitted in auth realm and revert
448
    # to project identifier if any are found
449
    if ($name =~ m/[^\w\d\s\._-]/) {
450
	$name = $project_id;
451
    }
452

    
453
    my $realm = '"Mercurial repository for ' . "'$name'" . '"';
454

    
455
    $realm;
456
}
457

    
458
sub connect_database {
459
    my $r = shift;
460
    
461
    my $cfg = Apache2::Module::get_config
462
	(__PACKAGE__, $r->server, $r->per_dir_config);
463

    
464
    return DBI->connect($cfg->{SoundSoftwareDSN},
465
	                $cfg->{SoundSoftwareDbUser},
466
		        $cfg->{SoundSoftwareDbPass});
467
}
468

    
469
1;