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