##// END OF EJS Templates
Initial support for caching credentials in Memcached (debug is enabled)....
Liwiusz Ociepa -
r1630:f7ede727fd0e
parent child
Show More
@@ -1,340 +1,374
1 1 package Apache::Authn::Redmine;
2 2
3 3 =head1 Apache::Authn::Redmine
4 4
5 5 Redmine - a mod_perl module to authenticate webdav subversion users
6 6 against redmine database
7 7
8 8 =head1 SYNOPSIS
9 9
10 10 This module allow anonymous users to browse public project and
11 11 registred users to browse and commit their project. Authentication is
12 12 done against the redmine database or the LDAP configured in redmine.
13 13
14 14 This method is far simpler than the one with pam_* and works with all
15 15 database without an hassle but you need to have apache/mod_perl on the
16 16 svn server.
17 17
18 18 =head1 INSTALLATION
19 19
20 20 For this to automagically work, you need to have a recent reposman.rb
21 21 (after r860) and if you already use reposman, read the last section to
22 22 migrate.
23 23
24 24 Sorry ruby users but you need some perl modules, at least mod_perl2,
25 25 DBI and DBD::mysql (or the DBD driver for you database as it should
26 26 work on allmost all databases).
27 27
28 28 On debian/ubuntu you must do :
29 29
30 30 aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
31 31
32 32 If your Redmine users use LDAP authentication, you will also need
33 33 Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
34 34
35 35 aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
36 36
37 37 =head1 CONFIGURATION
38 38
39 39 ## This module has to be in your perl path
40 40 ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
41 41 PerlLoadModule Apache::Authn::Redmine
42 42 <Location /svn>
43 43 DAV svn
44 44 SVNParentPath "/var/svn"
45 45
46 46 AuthType Basic
47 47 AuthName redmine
48 48 Require valid-user
49 49
50 50 PerlAccessHandler Apache::Authn::Redmine::access_handler
51 51 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
52 52
53 53 ## for mysql
54 54 RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
55 55 ## for postgres
56 56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
57 57
58 58 RedmineDbUser "redmine"
59 59 RedmineDbPass "password"
60 60 ## Optional where clause (fulltext search would be slow and
61 61 ## database dependant).
62 62 # RedmineDbWhereClause "and members.role_id IN (1,2)"
63 ## Optional credentials cache size
64 # RedmineCacheCredsMax 50
63 ## Configuration for memcached
64 # RedmineMemcacheServers "127.0.0.1:112211"
65 # RedmineMemcacheExpirySec "12"
66 # # Defaults to "RedminePM:"
67 # RedmineMemcacheNamespace "RedmineCreds:"
65 68 </Location>
66 69
67 70 To be able to browse repository inside redmine, you must add something
68 71 like that :
69 72
70 73 <Location /svn-private>
71 74 DAV svn
72 75 SVNParentPath "/var/svn"
73 76 Order deny,allow
74 77 Deny from all
75 78 # only allow reading orders
76 79 <Limit GET PROPFIND OPTIONS REPORT>
77 80 Allow from redmine.server.ip
78 81 </Limit>
79 82 </Location>
80 83
81 84 and you will have to use this reposman.rb command line to create repository :
82 85
83 86 reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
84 87
85 88 =head1 MIGRATION FROM OLDER RELEASES
86 89
87 90 If you use an older reposman.rb (r860 or before), you need to change
88 91 rights on repositories to allow the apache user to read and write
89 92 S<them :>
90 93
91 94 sudo chown -R www-data /var/svn/*
92 95 sudo chmod -R u+w /var/svn/*
93 96
94 97 And you need to upgrade at least reposman.rb (after r860).
95 98
96 99 =cut
97 100
98 101 use strict;
99 102 use warnings FATAL => 'all', NONFATAL => 'redefine';
100 103
101 104 use DBI;
102 105 use Digest::SHA1;
103 106 # optional module for LDAP authentication
104 107 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
108 my $CanUseMemcached = eval("use Cache::Memcached; 1");
105 109
106 110 use Apache2::Module;
107 111 use Apache2::Access;
108 112 use Apache2::ServerRec qw();
109 113 use Apache2::RequestRec qw();
110 114 use Apache2::RequestUtil qw();
111 115 use Apache2::Const qw(:common :override :cmd_how);
112 use APR::Pool ();
113 use APR::Table ();
114 116
115 117 # use Apache2::Directive qw();
116 118
117 119 my @directives = (
118 120 {
119 121 name => 'RedmineDSN',
120 122 req_override => OR_AUTHCFG,
121 123 args_how => TAKE1,
122 124 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
123 125 },
124 126 {
125 127 name => 'RedmineDbUser',
126 128 req_override => OR_AUTHCFG,
127 129 args_how => TAKE1,
128 130 },
129 131 {
130 132 name => 'RedmineDbPass',
131 133 req_override => OR_AUTHCFG,
132 134 args_how => TAKE1,
133 135 },
134 136 {
135 137 name => 'RedmineDbWhereClause',
136 138 req_override => OR_AUTHCFG,
137 139 args_how => TAKE1,
138 140 },
139 141 {
140 name => 'RedmineCacheCredsMax',
142 name => 'RedmineMemcacheServers',
143 req_override => OR_AUTHCFG,
144 args_how => TAKE1,
145 },
146 {
147 name => 'RedmineMemcacheExpirySec',
148 req_override => OR_AUTHCFG,
149 args_how => TAKE1,
150 },
151 {
152 name => 'RedmineMemcacheNamespace',
141 153 req_override => OR_AUTHCFG,
142 154 args_how => TAKE1,
143 errmsg => 'RedmineCacheCredsMax must be decimal number',
144 155 },
145 156 );
146 157
147 158 sub RedmineDSN {
148 159 my ($self, $parms, $arg) = @_;
149 160 $self->{RedmineDSN} = $arg;
150 161 my $query = "SELECT
151 162 hashed_password, auth_source_id
152 163 FROM members, projects, users
153 164 WHERE
154 165 projects.id=members.project_id
155 166 AND users.id=members.user_id
156 167 AND users.status=1
157 168 AND login=?
158 169 AND identifier=? ";
159 170 $self->{RedmineQuery} = trim($query);
160 171 }
161 172 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
162 173 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
163 174 sub RedmineDbWhereClause {
164 175 my ($self, $parms, $arg) = @_;
165 176 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
166 177 }
167 178
168 sub RedmineCacheCredsMax {
179 sub RedmineMemcacheServers {
169 180 my ($self, $parms, $arg) = @_;
170 if ($arg) {
171 $self->{RedmineCachePool} = APR::Pool->new;
172 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
173 $self->{RedmineCacheCredsCount} = 0;
174 $self->{RedmineCacheCredsMax} = $arg;
181 if ($arg && $CanUseMemcached) {
182 $self->{RedmineMemcached} = new Cache::Memcached {
183 'servers' => [ $arg, ],
184 'debug' => 1,
185 };
186 $self->{RedmineMemcache} = 1;
187 # Undocumented feature of Cache::Memcached, please don't kill me
188 if (0 == length $self->{RedmineMemcached}->{namespace}) {
189 $self->{RedmineMemcached}->{namespace} = "RedminePM:";
190 $self->{RedmineMemcached}->{namespace_len} = length $self->{RedmineMemcached}->{namespace};
191 }
192 }
193 }
194
195 sub RedmineMemcacheExpirySec { set_val('RedmineMemcacheExpirySec', @_); }
196
197 sub RedmineMemcacheNamespace {
198 my ($self, $parms, $arg) = @_;
199 if ($CanUseMemcached) {
200 # Undocumented feature of Cache::Memcached, please don't kill me
201 $self->{RedmineMemcached}->{namespace} = $arg;
202 $self->{RedmineMemcached}->{namespace_len} = length $self->{RedmineMemcached}->{namespace};
175 203 }
176 204 }
177 205
178 206 sub trim {
179 207 my $string = shift;
180 208 $string =~ s/\s{2,}/ /g;
181 209 return $string;
182 210 }
183 211
184 212 sub set_val {
185 213 my ($key, $self, $parms, $arg) = @_;
186 214 $self->{$key} = $arg;
187 215 }
188 216
189 217 Apache2::Module::add(__PACKAGE__, \@directives);
190 218
191 219
192 220 my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
193 221
194 222 sub access_handler {
195 223 my $r = shift;
196 224
197 225 unless ($r->some_auth_required) {
198 226 $r->log_reason("No authentication has been configured");
199 227 return FORBIDDEN;
200 228 }
201 229
202 230 my $method = $r->method;
203 231 return OK if defined $read_only_methods{$method};
204 232
205 233 my $project_id = get_project_identifier($r);
206 234
207 235 $r->set_handlers(PerlAuthenHandler => [\&OK])
208 236 if is_public_project($project_id, $r);
209 237
210 238 return OK
211 239 }
212 240
213 241 sub authen_handler {
214 242 my $r = shift;
215 243
216 244 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
217 245 return $res unless $res == OK;
218 246
219 247 if (is_member($r->user, $redmine_pass, $r)) {
220 248 return OK;
221 249 } else {
222 250 $r->note_auth_failure();
223 251 return AUTH_REQUIRED;
224 252 }
225 253 }
226 254
227 255 sub is_public_project {
228 256 my $project_id = shift;
229 257 my $r = shift;
230 258
259 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
260 if ($cfg->{RedmineMemcache}) {
261 return 1 if ($cfg->{RedmineMemcached}->get($project_id));
262 }
231 263 my $dbh = connect_database($r);
232 264 my $sth = $dbh->prepare(
233 265 "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
234 266 );
235 267
236 268 $sth->execute($project_id);
237 269 my $ret = $sth->fetchrow_array ? 1 : 0;
238 270 $sth->finish();
239 271 $dbh->disconnect();
272 if ($cfg->{RedmineMemcache}) {
273 if ($cfg->{RedmineMemcacheExpirySec}) {
274 $cfg->{RedmineMemcached}->set($project_id, $ret, $cfg->{RedmineMemcacheExpirySec});
275 } else {
276 $cfg->{RedmineMemcached}->set($project_id, $ret);
277 }
278 }
279
240 280
241 281 $ret;
242 282 }
243 283
244 284 # perhaps we should use repository right (other read right) to check public access.
245 285 # it could be faster BUT it doesn't work for the moment.
246 286 # sub is_public_project_by_file {
247 287 # my $project_id = shift;
248 288 # my $r = shift;
249 289
250 290 # my $tree = Apache2::Directive::conftree();
251 291 # my $node = $tree->lookup('Location', $r->location);
252 292 # my $hash = $node->as_hash;
253 293
254 294 # my $svnparentpath = $hash->{SVNParentPath};
255 295 # my $repos_path = $svnparentpath . "/" . $project_id;
256 296 # return 1 if (stat($repos_path))[2] & 00007;
257 297 # }
258 298
259 299 sub is_member {
260 300 my $redmine_user = shift;
261 301 my $redmine_pass = shift;
262 302 my $r = shift;
263 303
264 304 my $dbh = connect_database($r);
265 305 my $project_id = get_project_identifier($r);
266 306
267 307 my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
268 308
269 309 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
270 310 my $usrprojpass;
271 if ($cfg->{RedmineCacheCredsMax}) {
272 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
311 if ($cfg->{RedmineMemcache}) {
312 $usrprojpass = $cfg->{RedmineMemcached}->get($redmine_user.":".$project_id);
273 313 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
274 314 }
275 315 my $query = $cfg->{RedmineQuery};
276 316 my $sth = $dbh->prepare($query);
277 317 $sth->execute($redmine_user, $project_id);
278 318
279 319 my $ret;
280 320 while (my @row = $sth->fetchrow_array) {
281 321 unless ($row[1]) {
282 322 if ($row[0] eq $pass_digest) {
283 323 $ret = 1;
284 324 last;
285 325 }
286 326 } elsif ($CanUseLDAPAuth) {
287 327 my $sthldap = $dbh->prepare(
288 328 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
289 329 );
290 330 $sthldap->execute($row[1]);
291 331 while (my @rowldap = $sthldap->fetchrow_array) {
292 332 my $ldap = Authen::Simple::LDAP->new(
293 333 host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
294 334 port => $rowldap[1],
295 335 basedn => $rowldap[5],
296 336 binddn => $rowldap[3] ? $rowldap[3] : "",
297 337 bindpw => $rowldap[4] ? $rowldap[4] : "",
298 338 filter => "(".$rowldap[6]."=%s)"
299 339 );
300 340 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass));
301 341 }
302 342 $sthldap->finish();
303 343 }
304 344 }
305 345 $sth->finish();
306 346 $dbh->disconnect();
307 347
308 if ($cfg->{RedmineCacheCredsMax} and $ret) {
309 if (defined $usrprojpass) {
310 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
348 if ($cfg->{RedmineMemcache} and $ret) {
349 if ($cfg->{RedmineMemcacheExpirySec}) {
350 $cfg->{RedmineMemcached}->set($redmine_user.":".$project_id, $pass_digest, $cfg->{RedmineMemcacheExpirySec});
311 351 } else {
312 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
313 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
314 $cfg->{RedmineCacheCredsCount}++;
315 } else {
316 $cfg->{RedmineCacheCreds}->clear();
317 $cfg->{RedmineCacheCredsCount} = 0;
318 }
352 $cfg->{RedmineMemcached}->set($redmine_user.":".$project_id, $pass_digest);
319 353 }
320 354 }
321 355
322 356 $ret;
323 357 }
324 358
325 359 sub get_project_identifier {
326 360 my $r = shift;
327 361
328 362 my $location = $r->location;
329 363 my ($identifier) = $r->uri =~ m{$location/*([^/]+)};
330 364 $identifier;
331 365 }
332 366
333 367 sub connect_database {
334 368 my $r = shift;
335 369
336 370 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
337 371 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
338 372 }
339 373
340 374 1;
General Comments 0
You need to be logged in to leave comments. Login now