comparison extra/svn/SoundSoftware.pm @ 7:3c16ed8faa07 yuya

* Add start of custom version of Apache mod_perl authentication module
author Chris Cannam
date Thu, 12 Aug 2010 13:01:14 +0100
parents
children 0c83d98252d9
comparison
equal deleted inserted replaced
6:ed5f37ea0d06 7:3c16ed8faa07
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 credentials cache size
62 # SoundSoftwareCacheCredsMax 50
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 # use Apache2::Directive qw();
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 => 'SoundSoftwareCacheCredsMax',
110 req_override => OR_AUTHCFG,
111 args_how => TAKE1,
112 errmsg => 'SoundSoftwareCacheCredsMax must be decimal number',
113 },
114 );
115
116 sub SoundSoftwareDSN {
117 my ($self, $parms, $arg) = @_;
118 $self->{SoundSoftwareDSN} = $arg;
119 my $query = "SELECT
120 hashed_password, auth_source_id, permissions
121 FROM members, projects, users, roles, member_roles
122 WHERE
123 projects.id=members.project_id
124 AND member_roles.member_id=members.id
125 AND users.id=members.user_id
126 AND roles.id=member_roles.role_id
127 AND users.status=1
128 AND login=?
129 AND identifier=? ";
130 $self->{SoundSoftwareQuery} = trim($query);
131 }
132
133 sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); }
134 sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); }
135 sub SoundSoftwareDbWhereClause {
136 my ($self, $parms, $arg) = @_;
137 $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." ");
138 }
139
140 sub SoundSoftwareCacheCredsMax {
141 my ($self, $parms, $arg) = @_;
142 if ($arg) {
143 $self->{SoundSoftwareCachePool} = APR::Pool->new;
144 $self->{SoundSoftwareCacheCreds} = APR::Table::make($self->{SoundSoftwareCachePool}, $arg);
145 $self->{SoundSoftwareCacheCredsCount} = 0;
146 $self->{SoundSoftwareCacheCredsMax} = $arg;
147 }
148 }
149
150 sub trim {
151 my $string = shift;
152 $string =~ s/\s{2,}/ /g;
153 return $string;
154 }
155
156 sub set_val {
157 my ($key, $self, $parms, $arg) = @_;
158 $self->{$key} = $arg;
159 }
160
161 Apache2::Module::add(__PACKAGE__, \@directives);
162
163
164 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
165
166 sub access_handler {
167 my $r = shift;
168
169 print STDERR "SoundSoftware.pm: In access handler\n";
170
171 unless ($r->some_auth_required) {
172 $r->log_reason("No authentication has been configured");
173 return FORBIDDEN;
174 }
175
176 my $method = $r->method;
177
178 print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n";
179
180 if (!defined $read_only_methods{$method}) {
181 print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n";
182 return OK;
183 }
184
185 my $project_id = get_project_identifier($r);
186
187 if (defined $project_id) {
188 print STDERR "SoundSoftware.pm: Project: $project_id\n";
189 } else {
190 print STDERR "SoundSoftware.pm: No project identifier available, refusing access\n";
191 return FORBIDDEN;
192 }
193
194 my $status = get_project_status($project_id, $r);
195
196 if ($status == 0) { # nonexistent
197 print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n";
198 return FORBIDDEN;
199 } elsif ($status == 1) { # public
200 print STDERR "SoundSoftware.pm: Project is public, no restriction here\n";
201 $r->set_handlers(PerlAuthenHandler => [\&OK])
202 } else { # private
203 print STDERR "SoundSoftware.pm: Project is not public, authentication handler required\n";
204 }
205
206 return OK
207 }
208
209 sub authen_handler {
210 my $r = shift;
211
212 print STDERR "SoundSoftware.pm: In authentication handler\n";
213
214 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
215 return $res unless $res == OK;
216
217 print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n";
218
219 if (is_member($r->user, $redmine_pass, $r)) {
220 return OK;
221 } else {
222 print STDERR "SoundSoftware.pm: Failed to validate project membership\n";
223 $r->note_auth_failure();
224 return AUTH_REQUIRED;
225 }
226 }
227
228 sub get_project_status {
229 my $project_id = shift;
230 my $r = shift;
231
232 my $dbh = connect_database($r);
233 my $sth = $dbh->prepare(
234 "SELECT is_public FROM projects WHERE projects.identifier = ?;"
235 );
236
237 $sth->execute($project_id);
238 my $ret = 0;
239 if (my @row = $sth->fetchrow_array) {
240 if ($row[0] eq "1" || $row[0] eq "t") {
241 $ret = 1; # public
242 } else {
243 $ret = 2; # private (0 means nonexistent)
244 }
245 }
246 $sth->finish();
247 undef $sth;
248 $dbh->disconnect();
249 undef $dbh;
250
251 $ret;
252 }
253
254 sub is_member {
255 my $redmine_user = shift;
256 my $redmine_pass = shift;
257 my $r = shift;
258
259 my $dbh = connect_database($r);
260 my $project_id = get_project_identifier($r);
261
262 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
263
264 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
265 my $usrprojpass;
266 if ($cfg->{SoundSoftwareCacheCredsMax}) {
267 $usrprojpass = $cfg->{SoundSoftwareCacheCreds}->get($redmine_user.":".$project_id);
268 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
269 }
270 my $query = $cfg->{SoundSoftwareQuery};
271 my $sth = $dbh->prepare($query);
272 $sth->execute($redmine_user, $project_id);
273
274 my $ret;
275 while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
276
277 unless ($auth_source_id) {
278 my $method = $r->method;
279 if ($hashed_password eq $pass_digest && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
280 $ret = 1;
281 last;
282 }
283 } else {
284 my $sthldap = $dbh->prepare(
285 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
286 );
287 $sthldap->execute($auth_source_id);
288 while (my @rowldap = $sthldap->fetchrow_array) {
289 my $ldap = Authen::Simple::LDAP->new(
290 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
291 port => $rowldap[1],
292 basedn => $rowldap[5],
293 binddn => $rowldap[3] ? $rowldap[3] : "",
294 bindpw => $rowldap[4] ? $rowldap[4] : "",
295 filter => "(".$rowldap[6]."=%s)"
296 );
297 my $method = $r->method;
298 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
299
300 }
301 $sthldap->finish();
302 undef $sthldap;
303 }
304 }
305 $sth->finish();
306 undef $sth;
307 $dbh->disconnect();
308 undef $dbh;
309
310 if ($cfg->{SoundSoftwareCacheCredsMax} and $ret) {
311 if (defined $usrprojpass) {
312 $cfg->{SoundSoftwareCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
313 } else {
314 if ($cfg->{SoundSoftwareCacheCredsCount} < $cfg->{SoundSoftwareCacheCredsMax}) {
315 $cfg->{SoundSoftwareCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
316 $cfg->{SoundSoftwareCacheCredsCount}++;
317 } else {
318 $cfg->{SoundSoftwareCacheCreds}->clear();
319 $cfg->{SoundSoftwareCacheCredsCount} = 0;
320 }
321 }
322 }
323
324 $ret;
325 }
326
327 sub get_project_identifier {
328 my $r = shift;
329
330 my $location = $r->location;
331 my ($repo) = $r->uri =~ m{$location/*([^/]+)};
332 $repo =~ s/[^a-zA-Z0-9\._-]//g;
333
334 my $dbh = connect_database($r);
335 my $sth = $dbh->prepare(
336 "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;"
337 );
338
339 my $identifier = '';
340
341 $sth->execute('%/' . $repo);
342 my $ret = 0;
343 if (my @row = $sth->fetchrow_array) {
344 $identifier = $row[0];
345 }
346 $sth->finish();
347 undef $sth;
348 $dbh->disconnect();
349 undef $dbh;
350
351 print STDERR "SoundSoftware.pm: Repository $repo belongs to project $identifier\n";
352
353 $identifier;
354 }
355
356 sub connect_database {
357 my $r = shift;
358
359 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
360 return DBI->connect($cfg->{SoundSoftwareDSN}, $cfg->{SoundSoftwareDbUser}, $cfg->{SoundSoftwareDbPass});
361 }
362
363 1;