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-salted.pm @ 389:0bc92382a86b

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

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

    
380
    $ret;
381
}
382

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

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

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

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

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

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

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

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

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

    
414
    my $identifier = '';
415

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

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

    
426
    $identifier;
427
}
428

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

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

    
438
    my $name = $project_id;
439

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

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

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

    
456
    $realm;
457
}
458

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

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

    
470
1;