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