Mercurial > hg > soundsoftware-site
comparison extra/svn/SoundSoftware.pm @ 8:0c83d98252d9 yuya
* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author | Chris Cannam |
---|---|
date | Thu, 12 Aug 2010 15:31:37 +0100 |
parents | 3c16ed8faa07 |
children | 2b5c13a9425f 2c10dc5f122d |
comparison
equal
deleted
inserted
replaced
7:3c16ed8faa07 | 8:0c83d98252d9 |
---|---|
56 Options +ExecCGI | 56 Options +ExecCGI |
57 AddHandler cgi-script .cgi | 57 AddHandler cgi-script .cgi |
58 ## Optional where clause (fulltext search would be slow and | 58 ## Optional where clause (fulltext search would be slow and |
59 ## database dependant). | 59 ## database dependant). |
60 # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)" | 60 # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)" |
61 ## Optional credentials cache size | 61 ## Optional prefix for local repository URLs |
62 # SoundSoftwareCacheCredsMax 50 | 62 # SoundSoftwareRepoPrefix "/var/hg/" |
63 </Location> | 63 </Location> |
64 | 64 |
65 See the original Redmine.pm for further configuration notes. | 65 See the original Redmine.pm for further configuration notes. |
66 | 66 |
67 =cut | 67 =cut |
78 use Apache2::RequestRec qw(); | 78 use Apache2::RequestRec qw(); |
79 use Apache2::RequestUtil qw(); | 79 use Apache2::RequestUtil qw(); |
80 use Apache2::Const qw(:common :override :cmd_how); | 80 use Apache2::Const qw(:common :override :cmd_how); |
81 use APR::Pool (); | 81 use APR::Pool (); |
82 use APR::Table (); | 82 use APR::Table (); |
83 | |
84 # use Apache2::Directive qw(); | |
85 | 83 |
86 my @directives = ( | 84 my @directives = ( |
87 { | 85 { |
88 name => 'SoundSoftwareDSN', | 86 name => 'SoundSoftwareDSN', |
89 req_override => OR_AUTHCFG, | 87 req_override => OR_AUTHCFG, |
104 name => 'SoundSoftwareDbWhereClause', | 102 name => 'SoundSoftwareDbWhereClause', |
105 req_override => OR_AUTHCFG, | 103 req_override => OR_AUTHCFG, |
106 args_how => TAKE1, | 104 args_how => TAKE1, |
107 }, | 105 }, |
108 { | 106 { |
109 name => 'SoundSoftwareCacheCredsMax', | 107 name => 'SoundSoftwareRepoPrefix', |
110 req_override => OR_AUTHCFG, | 108 req_override => OR_AUTHCFG, |
111 args_how => TAKE1, | 109 args_how => TAKE1, |
112 errmsg => 'SoundSoftwareCacheCredsMax must be decimal number', | |
113 }, | 110 }, |
114 ); | 111 ); |
115 | 112 |
116 sub SoundSoftwareDSN { | 113 sub SoundSoftwareDSN { |
117 my ($self, $parms, $arg) = @_; | 114 my ($self, $parms, $arg) = @_; |
118 $self->{SoundSoftwareDSN} = $arg; | 115 $self->{SoundSoftwareDSN} = $arg; |
119 my $query = "SELECT | 116 my $query = "SELECT |
120 hashed_password, auth_source_id, permissions | 117 hashed_password, auth_source_id, permissions |
121 FROM members, projects, users, roles, member_roles | 118 FROM members, projects, users, roles, member_roles |
122 WHERE | 119 WHERE |
123 projects.id=members.project_id | 120 projects.id=members.project_id |
124 AND member_roles.member_id=members.id | 121 AND member_roles.member_id=members.id |
125 AND users.id=members.user_id | 122 AND users.id=members.user_id |
126 AND roles.id=member_roles.role_id | 123 AND roles.id=member_roles.role_id |
127 AND users.status=1 | 124 AND users.status=1 |
128 AND login=? | 125 AND login=? |
129 AND identifier=? "; | 126 AND identifier=? "; |
130 $self->{SoundSoftwareQuery} = trim($query); | 127 $self->{SoundSoftwareQuery} = trim($query); |
131 } | 128 } |
132 | 129 |
133 sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); } | 130 sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); } |
134 sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); } | 131 sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); } |
135 sub SoundSoftwareDbWhereClause { | 132 sub SoundSoftwareDbWhereClause { |
136 my ($self, $parms, $arg) = @_; | 133 my ($self, $parms, $arg) = @_; |
137 $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." "); | 134 $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." "); |
138 } | 135 } |
139 | 136 |
140 sub SoundSoftwareCacheCredsMax { | 137 sub SoundSoftwareRepoPrefix { |
141 my ($self, $parms, $arg) = @_; | 138 my ($self, $parms, $arg) = @_; |
142 if ($arg) { | 139 if ($arg) { |
143 $self->{SoundSoftwareCachePool} = APR::Pool->new; | 140 $self->{SoundSoftwareRepoPrefix} = $arg; |
144 $self->{SoundSoftwareCacheCreds} = APR::Table::make($self->{SoundSoftwareCachePool}, $arg); | 141 } |
145 $self->{SoundSoftwareCacheCredsCount} = 0; | |
146 $self->{SoundSoftwareCacheCredsMax} = $arg; | |
147 } | |
148 } | 142 } |
149 | 143 |
150 sub trim { | 144 sub trim { |
151 my $string = shift; | 145 my $string = shift; |
152 $string =~ s/\s{2,}/ /g; | 146 $string =~ s/\s{2,}/ /g; |
153 return $string; | 147 return $string; |
154 } | 148 } |
155 | 149 |
156 sub set_val { | 150 sub set_val { |
157 my ($key, $self, $parms, $arg) = @_; | 151 my ($key, $self, $parms, $arg) = @_; |
158 $self->{$key} = $arg; | 152 $self->{$key} = $arg; |
159 } | 153 } |
160 | 154 |
161 Apache2::Module::add(__PACKAGE__, \@directives); | 155 Apache2::Module::add(__PACKAGE__, \@directives); |
162 | 156 |
163 | 157 |
164 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; | 158 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; |
165 | 159 |
166 sub access_handler { | 160 sub access_handler { |
167 my $r = shift; | 161 my $r = shift; |
168 | 162 |
169 print STDERR "SoundSoftware.pm: In access handler\n"; | 163 print STDERR "SoundSoftware.pm: In access handler\n"; |
170 | 164 |
171 unless ($r->some_auth_required) { | 165 unless ($r->some_auth_required) { |
172 $r->log_reason("No authentication has been configured"); | 166 $r->log_reason("No authentication has been configured"); |
173 return FORBIDDEN; | 167 return FORBIDDEN; |
174 } | 168 } |
175 | 169 |
176 my $method = $r->method; | 170 my $method = $r->method; |
177 | 171 |
178 print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n"; | 172 print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n"; |
179 | 173 print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n"; |
180 if (!defined $read_only_methods{$method}) { | 174 |
181 print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n"; | 175 if (!defined $read_only_methods{$method}) { |
182 return OK; | 176 print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n"; |
183 } | 177 return OK; |
184 | 178 } |
185 my $project_id = get_project_identifier($r); | 179 |
186 | 180 my $dbh = connect_database($r); |
187 if (defined $project_id) { | 181 |
188 print STDERR "SoundSoftware.pm: Project: $project_id\n"; | 182 my $project_id = get_project_identifier($dbh, $r); |
189 } else { | 183 my $status = get_project_status($dbh, $project_id, $r); |
190 print STDERR "SoundSoftware.pm: No project identifier available, refusing access\n"; | 184 |
191 return FORBIDDEN; | 185 $dbh->disconnect(); |
192 } | 186 undef $dbh; |
193 | 187 |
194 my $status = get_project_status($project_id, $r); | 188 if ($status == 0) { # nonexistent |
195 | 189 print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n"; |
196 if ($status == 0) { # nonexistent | 190 return FORBIDDEN; |
197 print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n"; | 191 } elsif ($status == 1) { # public |
198 return FORBIDDEN; | 192 print STDERR "SoundSoftware.pm: Project is public, no restriction here\n"; |
199 } elsif ($status == 1) { # public | 193 $r->set_handlers(PerlAuthenHandler => [\&OK]) |
200 print STDERR "SoundSoftware.pm: Project is public, no restriction here\n"; | 194 } else { # private |
201 $r->set_handlers(PerlAuthenHandler => [\&OK]) | 195 print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n"; |
202 } else { # private | 196 } |
203 print STDERR "SoundSoftware.pm: Project is not public, authentication handler required\n"; | 197 |
204 } | 198 return OK |
205 | |
206 return OK | |
207 } | 199 } |
208 | 200 |
209 sub authen_handler { | 201 sub authen_handler { |
210 my $r = shift; | 202 my $r = shift; |
211 | 203 |
212 print STDERR "SoundSoftware.pm: In authentication handler\n"; | 204 print STDERR "SoundSoftware.pm: In authentication handler\n"; |
213 | 205 |
214 my ($res, $redmine_pass) = $r->get_basic_auth_pw(); | 206 my $dbh = connect_database($r); |
215 return $res unless $res == OK; | 207 |
216 | 208 my $project_id = get_project_identifier($dbh, $r); |
217 print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n"; | 209 my $realm = get_realm($dbh, $project_id, $r); |
218 | 210 $r->auth_name($realm); |
219 if (is_member($r->user, $redmine_pass, $r)) { | 211 |
220 return OK; | 212 my ($res, $redmine_pass) = $r->get_basic_auth_pw(); |
221 } else { | 213 unless ($res == OK) { |
222 print STDERR "SoundSoftware.pm: Failed to validate project membership\n"; | 214 $dbh->disconnect(); |
223 $r->note_auth_failure(); | 215 undef $dbh; |
224 return AUTH_REQUIRED; | 216 return $res; |
225 } | 217 } |
218 | |
219 print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n"; | |
220 | |
221 my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r); | |
222 | |
223 $dbh->disconnect(); | |
224 undef $dbh; | |
225 | |
226 if ($permitted) { | |
227 return OK; | |
228 } else { | |
229 print STDERR "SoundSoftware.pm: Not permitted\n"; | |
230 $r->note_auth_failure(); | |
231 return AUTH_REQUIRED; | |
232 } | |
226 } | 233 } |
227 | 234 |
228 sub get_project_status { | 235 sub get_project_status { |
236 my $dbh = shift; | |
229 my $project_id = shift; | 237 my $project_id = shift; |
230 my $r = shift; | 238 my $r = shift; |
231 | 239 |
232 my $dbh = connect_database($r); | 240 if (!defined $project_id or $project_id eq '') { |
241 return 0; # nonexistent | |
242 } | |
243 | |
233 my $sth = $dbh->prepare( | 244 my $sth = $dbh->prepare( |
234 "SELECT is_public FROM projects WHERE projects.identifier = ?;" | 245 "SELECT is_public FROM projects WHERE projects.identifier = ?;" |
235 ); | 246 ); |
236 | 247 |
237 $sth->execute($project_id); | 248 $sth->execute($project_id); |
238 my $ret = 0; | 249 my $ret = 0; # nonexistent |
239 if (my @row = $sth->fetchrow_array) { | 250 if (my @row = $sth->fetchrow_array) { |
240 if ($row[0] eq "1" || $row[0] eq "t") { | 251 if ($row[0] eq "1" || $row[0] eq "t") { |
241 $ret = 1; # public | 252 $ret = 1; # public |
242 } else { | 253 } else { |
243 $ret = 2; # private (0 means nonexistent) | 254 $ret = 2; # private |
244 } | 255 } |
245 } | 256 } |
246 $sth->finish(); | 257 $sth->finish(); |
247 undef $sth; | 258 undef $sth; |
248 $dbh->disconnect(); | |
249 undef $dbh; | |
250 | 259 |
251 $ret; | 260 $ret; |
252 } | 261 } |
253 | 262 |
254 sub is_member { | 263 sub is_permitted { |
255 my $redmine_user = shift; | 264 my $dbh = shift; |
256 my $redmine_pass = shift; | 265 my $project_id = shift; |
257 my $r = shift; | 266 my $redmine_user = shift; |
258 | 267 my $redmine_pass = shift; |
259 my $dbh = connect_database($r); | 268 my $r = shift; |
260 my $project_id = get_project_identifier($r); | 269 |
261 | 270 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); |
262 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); | 271 |
263 | 272 my $cfg = Apache2::Module::get_config |
264 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); | 273 (__PACKAGE__, $r->server, $r->per_dir_config); |
265 my $usrprojpass; | 274 |
266 if ($cfg->{SoundSoftwareCacheCredsMax}) { | 275 my $query = $cfg->{SoundSoftwareQuery}; |
267 $usrprojpass = $cfg->{SoundSoftwareCacheCreds}->get($redmine_user.":".$project_id); | 276 my $sth = $dbh->prepare($query); |
268 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest)); | 277 $sth->execute($redmine_user, $project_id); |
269 } | 278 |
270 my $query = $cfg->{SoundSoftwareQuery}; | 279 my $ret; |
271 my $sth = $dbh->prepare($query); | 280 while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) { |
272 $sth->execute($redmine_user, $project_id); | 281 |
273 | 282 # Test permissions for this user before we verify credentials |
274 my $ret; | 283 # -- if the user is not permitted this action anyway, there's |
275 while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) { | 284 # not much point in e.g. contacting the LDAP |
276 | 285 |
277 unless ($auth_source_id) { | 286 my $method = $r->method; |
278 my $method = $r->method; | 287 |
279 if ($hashed_password eq $pass_digest && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) { | 288 if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) |
280 $ret = 1; | 289 || $permissions =~ /:commit_access/) { |
281 last; | 290 |
282 } | 291 # User would be permitted this action, if their |
283 } else { | 292 # credentials checked out -- test those now |
284 my $sthldap = $dbh->prepare( | 293 |
285 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;" | 294 print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n"; |
286 ); | 295 |
287 $sthldap->execute($auth_source_id); | 296 unless ($auth_source_id) { |
288 while (my @rowldap = $sthldap->fetchrow_array) { | 297 if ($hashed_password eq $pass_digest) { |
289 my $ldap = Authen::Simple::LDAP->new( | 298 print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n"; |
290 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], | 299 $ret = 1; |
291 port => $rowldap[1], | 300 last; |
292 basedn => $rowldap[5], | 301 } |
293 binddn => $rowldap[3] ? $rowldap[3] : "", | 302 } else { |
294 bindpw => $rowldap[4] ? $rowldap[4] : "", | 303 my $sthldap = $dbh->prepare( |
295 filter => "(".$rowldap[6]."=%s)" | 304 "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;" |
296 ); | 305 ); |
297 my $method = $r->method; | 306 $sthldap->execute($auth_source_id); |
298 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/)); | 307 while (my @rowldap = $sthldap->fetchrow_array) { |
299 | 308 my $ldap = Authen::Simple::LDAP->new( |
300 } | 309 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], |
301 $sthldap->finish(); | 310 port => $rowldap[1], |
302 undef $sthldap; | 311 basedn => $rowldap[5], |
303 } | 312 binddn => $rowldap[3] ? $rowldap[3] : "", |
304 } | 313 bindpw => $rowldap[4] ? $rowldap[4] : "", |
305 $sth->finish(); | 314 filter => "(".$rowldap[6]."=%s)" |
306 undef $sth; | 315 ); |
307 $dbh->disconnect(); | 316 if ($ldap->authenticate($redmine_user, $redmine_pass)) { |
308 undef $dbh; | 317 print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n"; |
309 | 318 $ret = 1; |
310 if ($cfg->{SoundSoftwareCacheCredsMax} and $ret) { | 319 } |
311 if (defined $usrprojpass) { | 320 } |
312 $cfg->{SoundSoftwareCacheCreds}->set($redmine_user.":".$project_id, $pass_digest); | 321 $sthldap->finish(); |
313 } else { | 322 undef $sthldap; |
314 if ($cfg->{SoundSoftwareCacheCredsCount} < $cfg->{SoundSoftwareCacheCredsMax}) { | 323 } |
315 $cfg->{SoundSoftwareCacheCreds}->set($redmine_user.":".$project_id, $pass_digest); | 324 } else { |
316 $cfg->{SoundSoftwareCacheCredsCount}++; | 325 print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n"; |
317 } else { | 326 } |
318 $cfg->{SoundSoftwareCacheCreds}->clear(); | 327 } |
319 $cfg->{SoundSoftwareCacheCredsCount} = 0; | 328 |
320 } | 329 $sth->finish(); |
321 } | 330 undef $sth; |
322 } | 331 |
323 | 332 $ret; |
324 $ret; | |
325 } | 333 } |
326 | 334 |
327 sub get_project_identifier { | 335 sub get_project_identifier { |
336 my $dbh = shift; | |
328 my $r = shift; | 337 my $r = shift; |
329 | 338 |
330 my $location = $r->location; | 339 my $location = $r->location; |
331 my ($repo) = $r->uri =~ m{$location/*([^/]+)}; | 340 my ($repo) = $r->uri =~ m{$location/*([^/]+)}; |
332 $repo =~ s/[^a-zA-Z0-9\._-]//g; | 341 $repo =~ s/[^a-zA-Z0-9\._-]//g; |
333 | 342 |
334 my $dbh = connect_database($r); | 343 # The original Redmine.pm returns the string just calculated as |
344 # the project identifier. That won't do for us -- we may have | |
345 # (and in fact already do have, in our test instance) projects | |
346 # whose repository names differ from the project identifiers. | |
347 | |
348 # This is a rather fundamental change because it means that almost | |
349 # every request needs more than one database query -- which | |
350 # prompts us to start passing around $dbh instead of connecting | |
351 # locally within each function as is done in Redmine.pm. | |
352 | |
335 my $sth = $dbh->prepare( | 353 my $sth = $dbh->prepare( |
336 "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;" | 354 "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;" |
337 ); | 355 ); |
338 | 356 |
357 my $cfg = Apache2::Module::get_config | |
358 (__PACKAGE__, $r->server, $r->per_dir_config); | |
359 | |
360 my $prefix = $cfg->{SoundSoftwareRepoPrefix}; | |
361 if (!defined $prefix) { $prefix = '%/'; } | |
362 | |
339 my $identifier = ''; | 363 my $identifier = ''; |
340 | 364 |
341 $sth->execute('%/' . $repo); | 365 $sth->execute($prefix . $repo); |
342 my $ret = 0; | 366 my $ret = 0; |
343 if (my @row = $sth->fetchrow_array) { | 367 if (my @row = $sth->fetchrow_array) { |
344 $identifier = $row[0]; | 368 $identifier = $row[0]; |
345 } | 369 } |
346 $sth->finish(); | 370 $sth->finish(); |
347 undef $sth; | 371 undef $sth; |
348 $dbh->disconnect(); | 372 |
349 undef $dbh; | 373 print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n"; |
350 | |
351 print STDERR "SoundSoftware.pm: Repository $repo belongs to project $identifier\n"; | |
352 | 374 |
353 $identifier; | 375 $identifier; |
354 } | 376 } |
355 | 377 |
378 sub get_realm { | |
379 my $dbh = shift; | |
380 my $project_id = shift; | |
381 my $r = shift; | |
382 | |
383 my $sth = $dbh->prepare( | |
384 "SELECT projects.name FROM projects WHERE projects.identifier = ?;" | |
385 ); | |
386 | |
387 my $name = $project_id; | |
388 | |
389 $sth->execute($project_id); | |
390 my $ret = 0; | |
391 if (my @row = $sth->fetchrow_array) { | |
392 $name = $row[0]; | |
393 } | |
394 $sth->finish(); | |
395 undef $sth; | |
396 | |
397 # be timid about characters not permitted in auth realm and revert | |
398 # to project identifier if any are found | |
399 if ($name =~ m/[^\w\d\s\._-]/) { | |
400 $name = $project_id; | |
401 } | |
402 | |
403 my $realm = '"Mercurial repository for ' . "'$name'" . '"'; | |
404 | |
405 $realm; | |
406 } | |
407 | |
356 sub connect_database { | 408 sub connect_database { |
357 my $r = shift; | 409 my $r = shift; |
358 | 410 |
359 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); | 411 my $cfg = Apache2::Module::get_config |
360 return DBI->connect($cfg->{SoundSoftwareDSN}, $cfg->{SoundSoftwareDbUser}, $cfg->{SoundSoftwareDbPass}); | 412 (__PACKAGE__, $r->server, $r->per_dir_config); |
413 | |
414 return DBI->connect($cfg->{SoundSoftwareDSN}, | |
415 $cfg->{SoundSoftwareDbUser}, | |
416 $cfg->{SoundSoftwareDbPass}); | |
361 } | 417 } |
362 | 418 |
363 1; | 419 1; |