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