|
1 |
package Apache::Authn::RedmineSOAP;
|
|
2 |
|
|
3 |
=head1 Apache::Authn::RedmineSOAP
|
|
4 |
|
|
5 |
RedmineSOAP - a mod_perl module to authenticate webdav subversion users
|
|
6 |
against redmine authentication system (using WebService)
|
|
7 |
|
|
8 |
=head1 SYNOPSIS
|
|
9 |
|
|
10 |
This module allow anonymous users to browse public project. Registered
|
|
11 |
users may have read-only or read-write access depending their 'role'
|
|
12 |
in their project.
|
|
13 |
|
|
14 |
This method use the same login method as the redmine interface, so it
|
|
15 |
work no matter the auth sources setup in redmine. Password is cyphered
|
|
16 |
to ensure non divulgation in log or during the communication. It is
|
|
17 |
strongly recommended to use HTTPS for the WebService.
|
|
18 |
To improve performance a simple but configurable cache system has been
|
|
19 |
implemented (a 'svn co' requires up to 10 authentication requests)
|
|
20 |
|
|
21 |
=head1 INSTALLATION
|
|
22 |
|
|
23 |
For this to automagically work, you need to have a recent reposman.rb
|
|
24 |
(after r860) and if you already use reposman, read the last section to
|
|
25 |
migrate.
|
|
26 |
|
|
27 |
Sorry ruby users but you need some perl modules, at least mod_perl2,
|
|
28 |
SOAP::Lite, File::Cache, Crypt::CBC, Crypt::Rijndael.
|
|
29 |
|
|
30 |
On debian/ubuntu you must do :
|
|
31 |
|
|
32 |
aptitude install libapache2-mod-perl2 libsoap-lite-perl libcrypt-cbc-perl \
|
|
33 |
livcrypt-rijndael-perl libfile-cache-perl
|
|
34 |
|
|
35 |
=head1 CONFIGURATION
|
|
36 |
|
|
37 |
## if the module isn't in your perl path
|
|
38 |
PerlRequire /usr/local/apache/RedmineSOAP.pm
|
|
39 |
## else
|
|
40 |
# PerlModule Apache::Authn::RedmineSOAP
|
|
41 |
<Location /svn>
|
|
42 |
DAV svn
|
|
43 |
SVNParentPath "/var/svn"
|
|
44 |
|
|
45 |
AuthType Basic
|
|
46 |
AuthName redmine
|
|
47 |
Require valid-user
|
|
48 |
|
|
49 |
PerlAccessHandler Apache::Authn::RedmineSOAP::access_handler
|
|
50 |
PerlAuthenHandler Apache::Authn::RedmineSOAP::authen_handler
|
|
51 |
|
|
52 |
# Verbosity level (0: disabled)
|
|
53 |
PerlSetVar verbosity 0
|
|
54 |
|
|
55 |
# secret key for password encryption (the same as redmine
|
|
56 |
# configuration)
|
|
57 |
PerlSetVar encryption_key "2p7PQ6FCFMWdBNK5SErQFQICUrZtSibJ"
|
|
58 |
|
|
59 |
# Enable/disable cache
|
|
60 |
# Caching is strongly recommanded, at least with few seconds cache!
|
|
61 |
PerlSetVar enable_cache 1
|
|
62 |
|
|
63 |
# Caching system configuration
|
|
64 |
PerlSetVar private_project_expire "60 seconds"
|
|
65 |
PerlSetVar public_project_expire "60 seconds"
|
|
66 |
PerlSetVar auth_failed_expire "60 seconds"
|
|
67 |
PerlSetVar auth_ro_expire "60 seconds"
|
|
68 |
PerlSetVar auth_rw_neg "60 seconds"
|
|
69 |
|
|
70 |
# Redming WebService URL
|
|
71 |
PerlSetVar url https://dev.ginkgo-networks.com
|
|
72 |
|
|
73 |
</Location>
|
|
74 |
|
|
75 |
To be able to browse repository inside redmine, you must add something
|
|
76 |
like that :
|
|
77 |
|
|
78 |
<Location /svn-private>
|
|
79 |
DAV svn
|
|
80 |
SVNParentPath "/var/svn"
|
|
81 |
Order deny,allow
|
|
82 |
Deny from all
|
|
83 |
# only allow reading orders
|
|
84 |
<Limit GET PROPFIND OPTIONS REPORT>
|
|
85 |
Allow from redmine.server.ip
|
|
86 |
</Limit>
|
|
87 |
</Location>
|
|
88 |
|
|
89 |
and you will have to use this reposman.rb command line to create repository :
|
|
90 |
|
|
91 |
reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/
|
|
92 |
|
|
93 |
=head1 MIGRATION FROM OLDER RELEASES
|
|
94 |
|
|
95 |
If you use an older reposman.rb (r860 or before), you need to change
|
|
96 |
rights on repositories to allow the apache user to read and write
|
|
97 |
S<them :>
|
|
98 |
|
|
99 |
sudo chown -R www-data /var/svn/*
|
|
100 |
sudo chmod -R u+w /var/svn/*
|
|
101 |
|
|
102 |
And you need to upgrade at least reposman.rb (after r860).
|
|
103 |
|
|
104 |
=cut
|
|
105 |
|
|
106 |
use strict;
|
|
107 |
|
|
108 |
use SOAP::Lite;
|
|
109 |
use File::Cache;
|
|
110 |
|
|
111 |
use Digest::SHA qw(sha256);
|
|
112 |
use Crypt::CBC;
|
|
113 |
|
|
114 |
use Apache2::Module;
|
|
115 |
use Apache2::Access;
|
|
116 |
use Apache2::ServerRec qw();
|
|
117 |
use Apache2::RequestRec qw();
|
|
118 |
use Apache2::RequestUtil qw();
|
|
119 |
use Apache2::Const qw(:common);
|
|
120 |
# use Apache2::Directive qw();
|
|
121 |
|
|
122 |
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
|
|
123 |
|
|
124 |
my $verbosity = 0;
|
|
125 |
my $file = "/tmp/redmine.auth.log";
|
|
126 |
|
|
127 |
my $url = "http://localhost";
|
|
128 |
|
|
129 |
## Password encryption
|
|
130 |
my $key = "redminedefaultkey";
|
|
131 |
|
|
132 |
## Cache system
|
|
133 |
my $cache = 1;
|
|
134 |
my $public_cache = new File::Cache( { namespace => 'svn_redmine_public_project' } );
|
|
135 |
my $auth_cache = new File::Cache( { namespace => 'svn_redmine_auth' } );
|
|
136 |
|
|
137 |
# Expiration
|
|
138 |
# 0 --> negative
|
|
139 |
# 1 --> positive
|
|
140 |
my @public_expire = (5, 5);
|
|
141 |
my @auth_expire = (5, 5);
|
|
142 |
|
|
143 |
|
|
144 |
sub is_set {
|
|
145 |
my $r = shift;
|
|
146 |
my $key = shift;
|
|
147 |
return defined $r->dir_config($key);
|
|
148 |
}
|
|
149 |
|
|
150 |
sub config {
|
|
151 |
my $r = shift;
|
|
152 |
|
|
153 |
$verbosity = $r->dir_config("verbosity") if is_set($r,"verbosity");
|
|
154 |
$cache = $r->dir_config("enable_cache") if is_set($r,"enable_cache");
|
|
155 |
$key = $r->dir_config("encryption_key") if is_set($r,"encryption_key");
|
|
156 |
$url = $r->dir_config("url") if is_set($r,"url");
|
|
157 |
|
|
158 |
@public_expire[0] = $r->dir_config("private_project_expire") if is_set($r,"private_project_expire");
|
|
159 |
@public_expire[1] = $r->dir_config("public_project_expire") if is_set($r,"public_project_expire");
|
|
160 |
|
|
161 |
@auth_expire[0] = $r->dir_config("auth_failed_expire") if is_set($r,"auth_failed_expire");
|
|
162 |
@auth_expire[1] = $r->dir_config("auth_ro_expire") if is_set($r,"auth_ro_expire");
|
|
163 |
@auth_expire[2] = $r->dir_config("auth_rw_expire") if is_set($r,"auth_rw_expire");
|
|
164 |
}
|
|
165 |
|
|
166 |
sub access_handler {
|
|
167 |
my $r = shift;
|
|
168 |
config($r);
|
|
169 |
|
|
170 |
unless ($r->some_auth_required) {
|
|
171 |
$r->log_reason("No authentication has been configured");
|
|
172 |
return FORBIDDEN;
|
|
173 |
}
|
|
174 |
|
|
175 |
my $repository_name = get_repository_name($r);
|
|
176 |
|
|
177 |
$r->set_handlers(PerlAuthenHandler => [\&OK])
|
|
178 |
if is_public_project($repository_name, $r);
|
|
179 |
|
|
180 |
return OK
|
|
181 |
}
|
|
182 |
|
|
183 |
sub authen_handler {
|
|
184 |
my $r = shift;
|
|
185 |
config($r);
|
|
186 |
|
|
187 |
my ($res, $redmine_pass) = $r->get_basic_auth_pw();
|
|
188 |
return $res unless $res == OK;
|
|
189 |
|
|
190 |
Log(level => 1, text => "Checking auth");
|
|
191 |
my $ret = get_credits($r->user, $redmine_pass, $r);
|
|
192 |
my $method = $r->method;
|
|
193 |
|
|
194 |
if ($ret>=2) {
|
|
195 |
return OK;
|
|
196 |
} elsif($ret==1 && $read_only_methods{$method}==1) {
|
|
197 |
return OK;
|
|
198 |
} else {
|
|
199 |
$r->note_auth_failure();
|
|
200 |
return AUTH_REQUIRED;
|
|
201 |
}
|
|
202 |
}
|
|
203 |
|
|
204 |
sub is_public_project {
|
|
205 |
my $repository_name = shift;
|
|
206 |
my $r = shift;
|
|
207 |
|
|
208 |
my $ret;
|
|
209 |
$ret = $public_cache->get($repository_name) if $cache;
|
|
210 |
if ( not defined $ret ) {
|
|
211 |
# We have to request to Redmine SAOP WebService if the project is public or not
|
|
212 |
|
|
213 |
my $service = connect_redmine($r);
|
|
214 |
Log(level => 3, text => "requesting visibility for project $repository_name");
|
|
215 |
$ret = $service->IsPublicProject($repository_name);
|
|
216 |
|
|
217 |
# Add the response to the cache
|
|
218 |
if($cache) {
|
|
219 |
Log(level => 3, text => "project $repository_name is_public saving: $ret");
|
|
220 |
$public_cache->set($repository_name, $ret, $public_expire[$ret]);
|
|
221 |
Log(level => 3, text => "project $repository_name is_public saved: $ret");
|
|
222 |
}
|
|
223 |
}
|
|
224 |
|
|
225 |
Log(level => 1, text => "Returned project $repository_name is_public: $ret");
|
|
226 |
return $ret;
|
|
227 |
}
|
|
228 |
|
|
229 |
sub get_credits {
|
|
230 |
my $redmine_user = shift;
|
|
231 |
my $redmine_pass = shift;
|
|
232 |
my $r = shift;
|
|
233 |
my $repository_name = get_repository_name($r);
|
|
234 |
|
|
235 |
my $ret;
|
|
236 |
$ret = $auth_cache->get( "$repository_name/$redmine_user/"+sha256($redmine_pass) ) if $cache;
|
|
237 |
if ( not defined $ret ) {
|
|
238 |
# We have to request credentials to Redmine
|
|
239 |
my $service = connect_redmine($r);
|
|
240 |
Log(level => 2, text => "Access requested for $redmine_user for $repository_name: $ret");
|
|
241 |
|
|
242 |
# Encrypt the serial symmetricaly
|
|
243 |
# We cannot use a digest for password matching in case there are external auth system used in Redmine
|
|
244 |
my $iv = Crypt::CBC->random_bytes(16);
|
|
245 |
my $cipher = Crypt::CBC->new(
|
|
246 |
-cipher => 'Rijndael',
|
|
247 |
-key => sha256($key),
|
|
248 |
-iv => $iv,
|
|
249 |
-literal_key => 1,
|
|
250 |
-header => 'none');
|
|
251 |
$cipher->start('encrypt');
|
|
252 |
|
|
253 |
my $ciphered_pass_hex = $cipher->encrypt_hex($redmine_pass);
|
|
254 |
my $iv_hex = unpack("H*", $iv);
|
|
255 |
Log(level => 4, text => "Ciphered password: $ciphered_pass_hex");
|
|
256 |
|
|
257 |
# Then, request:
|
|
258 |
$ret = $service->CanAccessRepository($repository_name,$redmine_user, $ciphered_pass_hex,$iv_hex);
|
|
259 |
|
|
260 |
# Finally, cache the result
|
|
261 |
if($cache) {
|
|
262 |
Log(level => 3, text => "Saving value for ($redmine_user,$repository_name): $ret");
|
|
263 |
$auth_cache->set( "$repository_name/$redmine_user/"+sha256($redmine_pass), $ret, $auth_expire[$ret]);
|
|
264 |
Log(level => 3, text => "Saved value for ($redmine_user,$repository_name): $ret");
|
|
265 |
}
|
|
266 |
}
|
|
267 |
|
|
268 |
Log(level => 1, text => "Returned value for ($redmine_user,$repository_name): $ret");
|
|
269 |
return $ret;
|
|
270 |
}
|
|
271 |
|
|
272 |
sub get_repository_name {
|
|
273 |
my $r = shift;
|
|
274 |
|
|
275 |
my $location = $r->location;
|
|
276 |
my ($repository_name) = $r->uri =~ m{$location/*([^/]+)};
|
|
277 |
$repository_name;
|
|
278 |
}
|
|
279 |
|
|
280 |
sub connect_redmine {
|
|
281 |
my $r = shift;
|
|
282 |
|
|
283 |
my $wsdl = "$url/sys/service.wsdl";
|
|
284 |
|
|
285 |
return SOAP::Lite->service($wsdl);
|
|
286 |
}
|
|
287 |
|
|
288 |
sub Log {
|
|
289 |
my %args = (level => 0, text => '', @_);
|
|
290 |
|
|
291 |
my $level = delete $args{level};
|
|
292 |
my $text = delete $args{text};
|
|
293 |
return unless $level <= $verbosity;
|
|
294 |
|
|
295 |
open FILE, ">>$file" or die "unable to open $file $!";
|
|
296 |
print FILE "$text\n";
|
|
297 |
|
|
298 |
exit $args{exit}
|
|
299 |
if defined $args{exit};
|
|
300 |
}
|
|
301 |
|
|
302 |
1;
|