##// END OF EJS Templates
Merged r14892 (#21312)....
Jean-Philippe Lang -
r14536:f6c74e87990c
parent child
Show More
@@ -1,556 +1,557
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
55 ## for postgres
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
56 # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
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 REPOSITORIES NAMING
85 =head1 REPOSITORIES NAMING
86
86
87 A projet repository must be named with the projet identifier. In case
87 A projet repository must be named with the projet identifier. In case
88 of multiple repositories for the same project, use the project identifier
88 of multiple repositories for the same project, use the project identifier
89 and the repository identifier separated with a dot:
89 and the repository identifier separated with a dot:
90
90
91 /var/svn/foo
91 /var/svn/foo
92 /var/svn/foo.otherrepo
92 /var/svn/foo.otherrepo
93
93
94 =head1 MIGRATION FROM OLDER RELEASES
94 =head1 MIGRATION FROM OLDER RELEASES
95
95
96 If you use an older reposman.rb (r860 or before), you need to change
96 If you use an older reposman.rb (r860 or before), you need to change
97 rights on repositories to allow the apache user to read and write
97 rights on repositories to allow the apache user to read and write
98 S<them :>
98 S<them :>
99
99
100 sudo chown -R www-data /var/svn/*
100 sudo chown -R www-data /var/svn/*
101 sudo chmod -R u+w /var/svn/*
101 sudo chmod -R u+w /var/svn/*
102
102
103 And you need to upgrade at least reposman.rb (after r860).
103 And you need to upgrade at least reposman.rb (after r860).
104
104
105 =head1 GIT SMART HTTP SUPPORT
105 =head1 GIT SMART HTTP SUPPORT
106
106
107 Git's smart HTTP protocol (available since Git 1.7.0) will not work with the
107 Git's smart HTTP protocol (available since Git 1.7.0) will not work with the
108 above settings. Redmine.pm normally does access control depending on the HTTP
108 above settings. Redmine.pm normally does access control depending on the HTTP
109 method used: read-only methods are OK for everyone in public projects and
109 method used: read-only methods are OK for everyone in public projects and
110 members with read rights in private projects. The rest require membership with
110 members with read rights in private projects. The rest require membership with
111 commit rights in the project.
111 commit rights in the project.
112
112
113 However, this scheme doesn't work for Git's smart HTTP protocol, as it will use
113 However, this scheme doesn't work for Git's smart HTTP protocol, as it will use
114 POST even for a simple clone. Instead, read-only requests must be detected using
114 POST even for a simple clone. Instead, read-only requests must be detected using
115 the full URL (including the query string): anything that doesn't belong to the
115 the full URL (including the query string): anything that doesn't belong to the
116 git-receive-pack service is read-only.
116 git-receive-pack service is read-only.
117
117
118 To activate this mode of operation, add this line inside your <Location /git>
118 To activate this mode of operation, add this line inside your <Location /git>
119 block:
119 block:
120
120
121 RedmineGitSmartHttp yes
121 RedmineGitSmartHttp yes
122
122
123 Here's a sample Apache configuration which integrates git-http-backend with
123 Here's a sample Apache configuration which integrates git-http-backend with
124 a MySQL database and this new option:
124 a MySQL database and this new option:
125
125
126 SetEnv GIT_PROJECT_ROOT /var/www/git/
126 SetEnv GIT_PROJECT_ROOT /var/www/git/
127 SetEnv GIT_HTTP_EXPORT_ALL
127 SetEnv GIT_HTTP_EXPORT_ALL
128 ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
128 ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
129 <Location /git>
129 <Location /git>
130 Order allow,deny
130 Order allow,deny
131 Allow from all
131 Allow from all
132
132
133 AuthType Basic
133 AuthType Basic
134 AuthName Git
134 AuthName Git
135 Require valid-user
135 Require valid-user
136
136
137 PerlAccessHandler Apache::Authn::Redmine::access_handler
137 PerlAccessHandler Apache::Authn::Redmine::access_handler
138 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
138 PerlAuthenHandler Apache::Authn::Redmine::authen_handler
139 # for mysql
139 # for mysql
140 RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1"
140 RedmineDSN "DBI:mysql:database=redmine;host=127.0.0.1"
141 RedmineDbUser "redmine"
141 RedmineDbUser "redmine"
142 RedmineDbPass "xxx"
142 RedmineDbPass "xxx"
143 RedmineGitSmartHttp yes
143 RedmineGitSmartHttp yes
144 </Location>
144 </Location>
145
145
146 Make sure that all the names of the repositories under /var/www/git/ have a
146 Make sure that all the names of the repositories under /var/www/git/ have a
147 matching identifier for some project: /var/www/git/myproject and
147 matching identifier for some project: /var/www/git/myproject and
148 /var/www/git/myproject.git will work. You can put both bare and non-bare
148 /var/www/git/myproject.git will work. You can put both bare and non-bare
149 repositories in /var/www/git, though bare repositories are strongly
149 repositories in /var/www/git, though bare repositories are strongly
150 recommended. You should create them with the rights of the user running Redmine,
150 recommended. You should create them with the rights of the user running Redmine,
151 like this:
151 like this:
152
152
153 cd /var/www/git
153 cd /var/www/git
154 sudo -u user-running-redmine mkdir myproject
154 sudo -u user-running-redmine mkdir myproject
155 cd myproject
155 cd myproject
156 sudo -u user-running-redmine git init --bare
156 sudo -u user-running-redmine git init --bare
157
157
158 Once you have activated this option, you have three options when cloning a
158 Once you have activated this option, you have three options when cloning a
159 repository:
159 repository:
160
160
161 - Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password
161 - Cloning using "http://user@host/git/repo(.git)" works, but will ask for the password
162 all the time.
162 all the time.
163
163
164 - Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but
164 - Cloning with "http://user:pass@host/git/repo(.git)" does not have this problem, but
165 this could reveal accidentally your password to the console in some versions
165 this could reveal accidentally your password to the console in some versions
166 of Git, and you would have to ensure that .git/config is not readable except
166 of Git, and you would have to ensure that .git/config is not readable except
167 by the owner for each of your projects.
167 by the owner for each of your projects.
168
168
169 - Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc
169 - Use "http://host/git/repo(.git)", and store your credentials in the ~/.netrc
170 file. This is the recommended solution, as you only have one file to protect
170 file. This is the recommended solution, as you only have one file to protect
171 and passwords will not be leaked accidentally to the console.
171 and passwords will not be leaked accidentally to the console.
172
172
173 IMPORTANT NOTE: It is *very important* that the file cannot be read by other
173 IMPORTANT NOTE: It is *very important* that the file cannot be read by other
174 users, as it will contain your password in cleartext. To create the file, you
174 users, as it will contain your password in cleartext. To create the file, you
175 can use the following commands, replacing yourhost, youruser and yourpassword
175 can use the following commands, replacing yourhost, youruser and yourpassword
176 with the right values:
176 with the right values:
177
177
178 touch ~/.netrc
178 touch ~/.netrc
179 chmod 600 ~/.netrc
179 chmod 600 ~/.netrc
180 echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc
180 echo -e "machine yourhost\nlogin youruser\npassword yourpassword" > ~/.netrc
181
181
182 =cut
182 =cut
183
183
184 use strict;
184 use strict;
185 use warnings FATAL => 'all', NONFATAL => 'redefine';
185 use warnings FATAL => 'all', NONFATAL => 'redefine';
186
186
187 use DBI;
187 use DBI;
188 use Digest::SHA;
188 use Digest::SHA;
189 # optional module for LDAP authentication
189 # optional module for LDAP authentication
190 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
190 my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
191
191
192 use Apache2::Module;
192 use Apache2::Module;
193 use Apache2::Access;
193 use Apache2::Access;
194 use Apache2::ServerRec qw();
194 use Apache2::ServerRec qw();
195 use Apache2::RequestRec qw();
195 use Apache2::RequestRec qw();
196 use Apache2::RequestUtil qw();
196 use Apache2::RequestUtil qw();
197 use Apache2::Const qw(:common :override :cmd_how);
197 use Apache2::Const qw(:common :override :cmd_how);
198 use APR::Pool ();
198 use APR::Pool ();
199 use APR::Table ();
199 use APR::Table ();
200
200
201 # use Apache2::Directive qw();
201 # use Apache2::Directive qw();
202
202
203 my @directives = (
203 my @directives = (
204 {
204 {
205 name => 'RedmineDSN',
205 name => 'RedmineDSN',
206 req_override => OR_AUTHCFG,
206 req_override => OR_AUTHCFG,
207 args_how => TAKE1,
207 args_how => TAKE1,
208 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
208 errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
209 },
209 },
210 {
210 {
211 name => 'RedmineDbUser',
211 name => 'RedmineDbUser',
212 req_override => OR_AUTHCFG,
212 req_override => OR_AUTHCFG,
213 args_how => TAKE1,
213 args_how => TAKE1,
214 },
214 },
215 {
215 {
216 name => 'RedmineDbPass',
216 name => 'RedmineDbPass',
217 req_override => OR_AUTHCFG,
217 req_override => OR_AUTHCFG,
218 args_how => TAKE1,
218 args_how => TAKE1,
219 },
219 },
220 {
220 {
221 name => 'RedmineDbWhereClause',
221 name => 'RedmineDbWhereClause',
222 req_override => OR_AUTHCFG,
222 req_override => OR_AUTHCFG,
223 args_how => TAKE1,
223 args_how => TAKE1,
224 },
224 },
225 {
225 {
226 name => 'RedmineCacheCredsMax',
226 name => 'RedmineCacheCredsMax',
227 req_override => OR_AUTHCFG,
227 req_override => OR_AUTHCFG,
228 args_how => TAKE1,
228 args_how => TAKE1,
229 errmsg => 'RedmineCacheCredsMax must be decimal number',
229 errmsg => 'RedmineCacheCredsMax must be decimal number',
230 },
230 },
231 {
231 {
232 name => 'RedmineGitSmartHttp',
232 name => 'RedmineGitSmartHttp',
233 req_override => OR_AUTHCFG,
233 req_override => OR_AUTHCFG,
234 args_how => TAKE1,
234 args_how => TAKE1,
235 },
235 },
236 );
236 );
237
237
238 sub RedmineDSN {
238 sub RedmineDSN {
239 my ($self, $parms, $arg) = @_;
239 my ($self, $parms, $arg) = @_;
240 $self->{RedmineDSN} = $arg;
240 $self->{RedmineDSN} = $arg;
241 my $query = "SELECT
241 my $query = "SELECT
242 users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
242 users.hashed_password, users.salt, users.auth_source_id, roles.permissions, projects.status
243 FROM projects, users, roles
243 FROM projects, users, roles
244 WHERE
244 WHERE
245 users.login=?
245 users.login=?
246 AND projects.identifier=?
246 AND projects.identifier=?
247 AND users.type='User'
247 AND users.status=1
248 AND users.status=1
248 AND (
249 AND (
249 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
250 roles.id IN (SELECT member_roles.role_id FROM members, member_roles WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id)
250 OR
251 OR
251 (cast(projects.is_public as CHAR) IN ('t', '1')
252 (cast(projects.is_public as CHAR) IN ('t', '1')
252 AND (roles.builtin=1
253 AND (roles.builtin=1
253 OR roles.id IN (SELECT member_roles.role_id FROM members, member_roles, users g
254 OR roles.id IN (SELECT member_roles.role_id FROM members, member_roles, users g
254 WHERE members.user_id = g.id AND members.project_id = projects.id AND members.id = member_roles.member_id
255 WHERE members.user_id = g.id AND members.project_id = projects.id AND members.id = member_roles.member_id
255 AND g.type = 'GroupNonMember'))
256 AND g.type = 'GroupNonMember'))
256 )
257 )
257 )
258 )
258 AND roles.permissions IS NOT NULL";
259 AND roles.permissions IS NOT NULL";
259 $self->{RedmineQuery} = trim($query);
260 $self->{RedmineQuery} = trim($query);
260 }
261 }
261
262
262 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
263 sub RedmineDbUser { set_val('RedmineDbUser', @_); }
263 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
264 sub RedmineDbPass { set_val('RedmineDbPass', @_); }
264 sub RedmineDbWhereClause {
265 sub RedmineDbWhereClause {
265 my ($self, $parms, $arg) = @_;
266 my ($self, $parms, $arg) = @_;
266 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
267 $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
267 }
268 }
268
269
269 sub RedmineCacheCredsMax {
270 sub RedmineCacheCredsMax {
270 my ($self, $parms, $arg) = @_;
271 my ($self, $parms, $arg) = @_;
271 if ($arg) {
272 if ($arg) {
272 $self->{RedmineCachePool} = APR::Pool->new;
273 $self->{RedmineCachePool} = APR::Pool->new;
273 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
274 $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
274 $self->{RedmineCacheCredsCount} = 0;
275 $self->{RedmineCacheCredsCount} = 0;
275 $self->{RedmineCacheCredsMax} = $arg;
276 $self->{RedmineCacheCredsMax} = $arg;
276 }
277 }
277 }
278 }
278
279
279 sub RedmineGitSmartHttp {
280 sub RedmineGitSmartHttp {
280 my ($self, $parms, $arg) = @_;
281 my ($self, $parms, $arg) = @_;
281 $arg = lc $arg;
282 $arg = lc $arg;
282
283
283 if ($arg eq "yes" || $arg eq "true") {
284 if ($arg eq "yes" || $arg eq "true") {
284 $self->{RedmineGitSmartHttp} = 1;
285 $self->{RedmineGitSmartHttp} = 1;
285 } else {
286 } else {
286 $self->{RedmineGitSmartHttp} = 0;
287 $self->{RedmineGitSmartHttp} = 0;
287 }
288 }
288 }
289 }
289
290
290 sub trim {
291 sub trim {
291 my $string = shift;
292 my $string = shift;
292 $string =~ s/\s{2,}/ /g;
293 $string =~ s/\s{2,}/ /g;
293 return $string;
294 return $string;
294 }
295 }
295
296
296 sub set_val {
297 sub set_val {
297 my ($key, $self, $parms, $arg) = @_;
298 my ($key, $self, $parms, $arg) = @_;
298 $self->{$key} = $arg;
299 $self->{$key} = $arg;
299 }
300 }
300
301
301 Apache2::Module::add(__PACKAGE__, \@directives);
302 Apache2::Module::add(__PACKAGE__, \@directives);
302
303
303
304
304 my %read_only_methods = map { $_ => 1 } qw/GET HEAD PROPFIND REPORT OPTIONS/;
305 my %read_only_methods = map { $_ => 1 } qw/GET HEAD PROPFIND REPORT OPTIONS/;
305
306
306 sub request_is_read_only {
307 sub request_is_read_only {
307 my ($r) = @_;
308 my ($r) = @_;
308 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
309 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
309
310
310 # Do we use Git's smart HTTP protocol, or not?
311 # Do we use Git's smart HTTP protocol, or not?
311 if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
312 if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp}) {
312 my $uri = $r->unparsed_uri;
313 my $uri = $r->unparsed_uri;
313 my $location = $r->location;
314 my $location = $r->location;
314 my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
315 my $is_read_only = $uri !~ m{^$location/*[^/]+/+(info/refs\?service=)?git\-receive\-pack$}o;
315 return $is_read_only;
316 return $is_read_only;
316 } else {
317 } else {
317 # Standard behaviour: check the HTTP method
318 # Standard behaviour: check the HTTP method
318 my $method = $r->method;
319 my $method = $r->method;
319 return defined $read_only_methods{$method};
320 return defined $read_only_methods{$method};
320 }
321 }
321 }
322 }
322
323
323 sub access_handler {
324 sub access_handler {
324 my $r = shift;
325 my $r = shift;
325
326
326 unless ($r->some_auth_required) {
327 unless ($r->some_auth_required) {
327 $r->log_reason("No authentication has been configured");
328 $r->log_reason("No authentication has been configured");
328 return FORBIDDEN;
329 return FORBIDDEN;
329 }
330 }
330
331
331 return OK unless request_is_read_only($r);
332 return OK unless request_is_read_only($r);
332
333
333 my $project_id = get_project_identifier($r);
334 my $project_id = get_project_identifier($r);
334
335
335 if (is_public_project($project_id, $r) && anonymous_allowed_to_browse_repository($project_id, $r)) {
336 if (is_public_project($project_id, $r) && anonymous_allowed_to_browse_repository($project_id, $r)) {
336 $r->user("");
337 $r->user("");
337 $r->set_handlers(PerlAuthenHandler => [\&OK]);
338 $r->set_handlers(PerlAuthenHandler => [\&OK]);
338 }
339 }
339
340
340 return OK
341 return OK
341 }
342 }
342
343
343 sub authen_handler {
344 sub authen_handler {
344 my $r = shift;
345 my $r = shift;
345
346
346 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
347 my ($res, $redmine_pass) = $r->get_basic_auth_pw();
347 return $res unless $res == OK;
348 return $res unless $res == OK;
348
349
349 if (is_member($r->user, $redmine_pass, $r)) {
350 if (is_member($r->user, $redmine_pass, $r)) {
350 return OK;
351 return OK;
351 } else {
352 } else {
352 $r->note_auth_failure();
353 $r->note_auth_failure();
353 return DECLINED;
354 return DECLINED;
354 }
355 }
355 }
356 }
356
357
357 # check if authentication is forced
358 # check if authentication is forced
358 sub is_authentication_forced {
359 sub is_authentication_forced {
359 my $r = shift;
360 my $r = shift;
360
361
361 my $dbh = connect_database($r);
362 my $dbh = connect_database($r);
362 my $sth = $dbh->prepare(
363 my $sth = $dbh->prepare(
363 "SELECT value FROM settings where settings.name = 'login_required';"
364 "SELECT value FROM settings where settings.name = 'login_required';"
364 );
365 );
365
366
366 $sth->execute();
367 $sth->execute();
367 my $ret = 0;
368 my $ret = 0;
368 if (my @row = $sth->fetchrow_array) {
369 if (my @row = $sth->fetchrow_array) {
369 if ($row[0] eq "1" || $row[0] eq "t") {
370 if ($row[0] eq "1" || $row[0] eq "t") {
370 $ret = 1;
371 $ret = 1;
371 }
372 }
372 }
373 }
373 $sth->finish();
374 $sth->finish();
374 undef $sth;
375 undef $sth;
375
376
376 $dbh->disconnect();
377 $dbh->disconnect();
377 undef $dbh;
378 undef $dbh;
378
379
379 $ret;
380 $ret;
380 }
381 }
381
382
382 sub is_public_project {
383 sub is_public_project {
383 my $project_id = shift;
384 my $project_id = shift;
384 my $r = shift;
385 my $r = shift;
385
386
386 if (is_authentication_forced($r)) {
387 if (is_authentication_forced($r)) {
387 return 0;
388 return 0;
388 }
389 }
389
390
390 my $dbh = connect_database($r);
391 my $dbh = connect_database($r);
391 my $sth = $dbh->prepare(
392 my $sth = $dbh->prepare(
392 "SELECT is_public FROM projects WHERE projects.identifier = ? AND projects.status <> 9;"
393 "SELECT is_public FROM projects WHERE projects.identifier = ? AND projects.status <> 9;"
393 );
394 );
394
395
395 $sth->execute($project_id);
396 $sth->execute($project_id);
396 my $ret = 0;
397 my $ret = 0;
397 if (my @row = $sth->fetchrow_array) {
398 if (my @row = $sth->fetchrow_array) {
398 if ($row[0] eq "1" || $row[0] eq "t") {
399 if ($row[0] eq "1" || $row[0] eq "t") {
399 $ret = 1;
400 $ret = 1;
400 }
401 }
401 }
402 }
402 $sth->finish();
403 $sth->finish();
403 undef $sth;
404 undef $sth;
404 $dbh->disconnect();
405 $dbh->disconnect();
405 undef $dbh;
406 undef $dbh;
406
407
407 $ret;
408 $ret;
408 }
409 }
409
410
410 sub anonymous_allowed_to_browse_repository {
411 sub anonymous_allowed_to_browse_repository {
411 my $project_id = shift;
412 my $project_id = shift;
412 my $r = shift;
413 my $r = shift;
413
414
414 my $dbh = connect_database($r);
415 my $dbh = connect_database($r);
415 my $sth = $dbh->prepare(
416 my $sth = $dbh->prepare(
416 "SELECT permissions FROM roles WHERE permissions like '%browse_repository%'
417 "SELECT permissions FROM roles WHERE permissions like '%browse_repository%'
417 AND (roles.builtin = 2
418 AND (roles.builtin = 2
418 OR roles.id IN (SELECT member_roles.role_id FROM projects, members, member_roles, users
419 OR roles.id IN (SELECT member_roles.role_id FROM projects, members, member_roles, users
419 WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id
420 WHERE members.user_id = users.id AND members.project_id = projects.id AND members.id = member_roles.member_id
420 AND projects.identifier = ? AND users.type = 'GroupAnonymous'));"
421 AND projects.identifier = ? AND users.type = 'GroupAnonymous'));"
421 );
422 );
422
423
423 $sth->execute($project_id);
424 $sth->execute($project_id);
424 my $ret = 0;
425 my $ret = 0;
425 if (my @row = $sth->fetchrow_array) {
426 if (my @row = $sth->fetchrow_array) {
426 if ($row[0] =~ /:browse_repository/) {
427 if ($row[0] =~ /:browse_repository/) {
427 $ret = 1;
428 $ret = 1;
428 }
429 }
429 }
430 }
430 $sth->finish();
431 $sth->finish();
431 undef $sth;
432 undef $sth;
432 $dbh->disconnect();
433 $dbh->disconnect();
433 undef $dbh;
434 undef $dbh;
434
435
435 $ret;
436 $ret;
436 }
437 }
437
438
438 # perhaps we should use repository right (other read right) to check public access.
439 # perhaps we should use repository right (other read right) to check public access.
439 # it could be faster BUT it doesn't work for the moment.
440 # it could be faster BUT it doesn't work for the moment.
440 # sub is_public_project_by_file {
441 # sub is_public_project_by_file {
441 # my $project_id = shift;
442 # my $project_id = shift;
442 # my $r = shift;
443 # my $r = shift;
443
444
444 # my $tree = Apache2::Directive::conftree();
445 # my $tree = Apache2::Directive::conftree();
445 # my $node = $tree->lookup('Location', $r->location);
446 # my $node = $tree->lookup('Location', $r->location);
446 # my $hash = $node->as_hash;
447 # my $hash = $node->as_hash;
447
448
448 # my $svnparentpath = $hash->{SVNParentPath};
449 # my $svnparentpath = $hash->{SVNParentPath};
449 # my $repos_path = $svnparentpath . "/" . $project_id;
450 # my $repos_path = $svnparentpath . "/" . $project_id;
450 # return 1 if (stat($repos_path))[2] & 00007;
451 # return 1 if (stat($repos_path))[2] & 00007;
451 # }
452 # }
452
453
453 sub is_member {
454 sub is_member {
454 my $redmine_user = shift;
455 my $redmine_user = shift;
455 my $redmine_pass = shift;
456 my $redmine_pass = shift;
456 my $r = shift;
457 my $r = shift;
457
458
458 my $project_id = get_project_identifier($r);
459 my $project_id = get_project_identifier($r);
459
460
460 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
461 my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
461
462
462 my $access_mode = request_is_read_only($r) ? "R" : "W";
463 my $access_mode = request_is_read_only($r) ? "R" : "W";
463
464
464 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
465 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
465 my $usrprojpass;
466 my $usrprojpass;
466 if ($cfg->{RedmineCacheCredsMax}) {
467 if ($cfg->{RedmineCacheCredsMax}) {
467 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
468 $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id.":".$access_mode);
468 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
469 return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
469 }
470 }
470 my $dbh = connect_database($r);
471 my $dbh = connect_database($r);
471 my $query = $cfg->{RedmineQuery};
472 my $query = $cfg->{RedmineQuery};
472 my $sth = $dbh->prepare($query);
473 my $sth = $dbh->prepare($query);
473 $sth->execute($redmine_user, $project_id);
474 $sth->execute($redmine_user, $project_id);
474
475
475 my $ret;
476 my $ret;
476 while (my ($hashed_password, $salt, $auth_source_id, $permissions, $project_status) = $sth->fetchrow_array) {
477 while (my ($hashed_password, $salt, $auth_source_id, $permissions, $project_status) = $sth->fetchrow_array) {
477 if ($project_status eq "9" || ($project_status ne "1" && $access_mode eq "W")) {
478 if ($project_status eq "9" || ($project_status ne "1" && $access_mode eq "W")) {
478 last;
479 last;
479 }
480 }
480
481
481 unless ($auth_source_id) {
482 unless ($auth_source_id) {
482 my $method = $r->method;
483 my $method = $r->method;
483 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
484 my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
484 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
485 if ($hashed_password eq $salted_password && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/) ) {
485 $ret = 1;
486 $ret = 1;
486 last;
487 last;
487 }
488 }
488 } elsif ($CanUseLDAPAuth) {
489 } elsif ($CanUseLDAPAuth) {
489 my $sthldap = $dbh->prepare(
490 my $sthldap = $dbh->prepare(
490 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
491 "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
491 );
492 );
492 $sthldap->execute($auth_source_id);
493 $sthldap->execute($auth_source_id);
493 while (my @rowldap = $sthldap->fetchrow_array) {
494 while (my @rowldap = $sthldap->fetchrow_array) {
494 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
495 my $bind_as = $rowldap[3] ? $rowldap[3] : "";
495 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
496 my $bind_pw = $rowldap[4] ? $rowldap[4] : "";
496 if ($bind_as =~ m/\$login/) {
497 if ($bind_as =~ m/\$login/) {
497 # replace $login with $redmine_user and use $redmine_pass
498 # replace $login with $redmine_user and use $redmine_pass
498 $bind_as =~ s/\$login/$redmine_user/g;
499 $bind_as =~ s/\$login/$redmine_user/g;
499 $bind_pw = $redmine_pass
500 $bind_pw = $redmine_pass
500 }
501 }
501 my $ldap = Authen::Simple::LDAP->new(
502 my $ldap = Authen::Simple::LDAP->new(
502 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
503 host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]:$rowldap[1]" : $rowldap[0],
503 port => $rowldap[1],
504 port => $rowldap[1],
504 basedn => $rowldap[5],
505 basedn => $rowldap[5],
505 binddn => $bind_as,
506 binddn => $bind_as,
506 bindpw => $bind_pw,
507 bindpw => $bind_pw,
507 filter => "(".$rowldap[6]."=%s)"
508 filter => "(".$rowldap[6]."=%s)"
508 );
509 );
509 my $method = $r->method;
510 my $method = $r->method;
510 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
511 $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass) && (($access_mode eq "R" && $permissions =~ /:browse_repository/) || $permissions =~ /:commit_access/));
511
512
512 }
513 }
513 $sthldap->finish();
514 $sthldap->finish();
514 undef $sthldap;
515 undef $sthldap;
515 }
516 }
516 }
517 }
517 $sth->finish();
518 $sth->finish();
518 undef $sth;
519 undef $sth;
519 $dbh->disconnect();
520 $dbh->disconnect();
520 undef $dbh;
521 undef $dbh;
521
522
522 if ($cfg->{RedmineCacheCredsMax} and $ret) {
523 if ($cfg->{RedmineCacheCredsMax} and $ret) {
523 if (defined $usrprojpass) {
524 if (defined $usrprojpass) {
524 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
525 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
525 } else {
526 } else {
526 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
527 if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
527 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
528 $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id.":".$access_mode, $pass_digest);
528 $cfg->{RedmineCacheCredsCount}++;
529 $cfg->{RedmineCacheCredsCount}++;
529 } else {
530 } else {
530 $cfg->{RedmineCacheCreds}->clear();
531 $cfg->{RedmineCacheCreds}->clear();
531 $cfg->{RedmineCacheCredsCount} = 0;
532 $cfg->{RedmineCacheCredsCount} = 0;
532 }
533 }
533 }
534 }
534 }
535 }
535
536
536 $ret;
537 $ret;
537 }
538 }
538
539
539 sub get_project_identifier {
540 sub get_project_identifier {
540 my $r = shift;
541 my $r = shift;
541
542
542 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
543 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
543 my $location = $r->location;
544 my $location = $r->location;
544 $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
545 $location =~ s/\.git$// if (defined $cfg->{RedmineGitSmartHttp} and $cfg->{RedmineGitSmartHttp});
545 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
546 my ($identifier) = $r->uri =~ m{$location/*([^/.]+)};
546 $identifier;
547 $identifier;
547 }
548 }
548
549
549 sub connect_database {
550 sub connect_database {
550 my $r = shift;
551 my $r = shift;
551
552
552 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
553 my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
553 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
554 return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
554 }
555 }
555
556
556 1;
557 1;
General Comments 0
You need to be logged in to leave comments. Login now