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