diff -Nur redmine-0.6.3/app/apis/sys_api.rb redmine-0.6.3-patched/app/apis/sys_api.rb --- redmine-0.6.3/app/apis/sys_api.rb 2007-12-18 19:17:39.000000000 +0100 +++ redmine-0.6.3-patched/app/apis/sys_api.rb 2008-04-11 22:10:28.000000000 +0200 @@ -22,4 +22,10 @@ api_method :repository_created, :expects => [:string, :string], :returns => [:int] + api_method :is_public_project, + :expects => [:string], + :returns => [:int] + api_method :can_access_repository, + :expects => [:string, :string, :string, :string], + :returns => [:int] end diff -Nur redmine-0.6.3/app/controllers/sys_controller.rb redmine-0.6.3-patched/app/controllers/sys_controller.rb --- redmine-0.6.3/app/controllers/sys_controller.rb 2007-12-18 19:17:39.000000000 +0100 +++ redmine-0.6.3-patched/app/controllers/sys_controller.rb 2008-04-11 22:28:10.000000000 +0200 @@ -15,6 +15,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'openssl' +require 'digest/sha2' + class SysController < ActionController::Base wsdl_service_name 'Sys' web_service_api SysApi @@ -39,6 +50,48 @@ repository.id || 0 end + def is_public_project(repository) + begin + project = Project.find_by_repository(Regexp.new('.*/'+Regexp.escape(repository)+'/?$')) + + return 0 if project.nil? + return 0 unless project.is_public + return 1 + rescue + return 0 + end + end + + def can_access_repository(repository,login,ciphered_pwd_hex,iv_hex) + begin + # Decipher the password + key = Digest::SHA256.digest(Setting.repositories_key) + ciphered_pwd = ciphered_pwd_hex.gsub(/(..)/){|h| h.hex.chr} + iv = iv_hex.gsub(/(..)/){|h| h.hex.chr} + + cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc') + cipher.key = key + cipher.iv = iv + cipher.decrypt + + pwd = cipher.update(ciphered_pwd) + pwd << cipher.final + + # Then, try to login + user = User.try_to_login(login, pwd) + return 0 unless user!=nil + + project = Project.find_by_repository(Regexp.new('.*/'+Regexp.escape(repository)+'/?$')) + logger.debug "Requesting access for project: #{project.name}" + return 0 unless user.member_of?(project) + return 0 unless user.allowed_to?(:browse_repository, project) + return 1 unless user.allowed_to?(:commit_repository, project) + return 2 + rescue + return 0 + end + end + protected def check_enabled(name, args) diff -Nur redmine-0.6.3/app/models/project.rb redmine-0.6.3-patched/app/models/project.rb --- redmine-0.6.3/app/models/project.rb 2007-12-18 19:17:39.000000000 +0100 +++ redmine-0.6.3-patched/app/models/project.rb 2008-04-09 15:05:50.000000000 +0200 @@ -182,6 +182,14 @@ end end + def self.find_by_repository(repository_wildcard) + project = Project.find_by_identifier(repository_wildcard) + return project unless project.nil? || project.repository.nil? || !project.repository.url=~repository_wildcard + Project.find(:all, :include => :repository).each do |project| + return project if !project.repository.nil? && project.repository.url=~repository_wildcard + end + end + protected def validate errors.add(parent_id, " must be a root project") if parent and parent.parent diff -Nur redmine-0.6.3/app/views/settings/edit.rhtml redmine-0.6.3-patched/app/views/settings/edit.rhtml --- redmine-0.6.3/app/views/settings/edit.rhtml 2007-12-18 19:17:41.000000000 +0100 +++ redmine-0.6.3-patched/app/views/settings/edit.rhtml 2008-04-11 22:24:01.000000000 +0200 @@ -56,6 +56,9 @@

<%= text_field_tag 'settings[repositories_encodings]', Setting.repositories_encodings, :size => 60 %>
<%= l(:text_comma_separated) %>

+ +

+<%= text_field_tag 'settings[repositories_key]', Setting.repositories_key, :size => 60 %>

<%= l(:setting_issue_list_default_columns) %> diff -Nur redmine-0.6.3/config/settings.yml redmine-0.6.3-patched/config/settings.yml --- redmine-0.6.3/config/settings.yml 2007-12-18 19:17:46.000000000 +0100 +++ redmine-0.6.3-patched/config/settings.yml 2008-04-14 11:23:36.000000000 +0200 @@ -97,10 +97,11 @@ # multiple values accepted, comma separated repositories_encodings: default: '' +repositories_key: + default: 'redminedefaultkey' ui_theme: default: '' emails_footer: default: |- You have received this notification because you have either subscribed to it, or are involved in. To change your notification preferences, please click here: http://hostname/my/account - \ No newline at end of file diff -Nur redmine-0.6.3/lang/en.yml redmine-0.6.3-patched/lang/en.yml --- redmine-0.6.3/lang/en.yml 2007-12-18 19:17:44.000000000 +0100 +++ redmine-0.6.3-patched/lang/en.yml 2008-04-11 22:20:48.000000000 +0200 @@ -196,6 +197,7 @@ setting_cross_project_issue_relations: Allow cross-project issue relations setting_issue_list_default_columns: Default columns displayed on the issue list setting_repositories_encodings: Repositories encodings +setting_repositories_key: Repositories communication key setting_emails_footer: Emails footer setting_protocol: Protocol diff -Nur redmine-0.6.3/lang/fr.yml redmine-0.6.3-patched/lang/fr.yml --- redmine-0.6.3/lang/fr.yml 2007-12-18 19:17:44.000000000 +0100 +++ redmine-0.6.3-patched/lang/fr.yml 2008-04-11 22:20:48.000000000 +0200 @@ -196,6 +197,7 @@ setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes setting_repositories_encodings: Encodages des dépôts +setting_repositories_key: Clé de chiffrement avec les dépôts setting_emails_footer: Pied-de-page des emails setting_protocol: Protocole diff -Nur redmine-0.6.3/lib/redmine.rb redmine-0.6.3-patched/lib/redmine.rb --- redmine-0.6.3/lib/redmine.rb 2007-12-18 19:17:48.000000000 +0100 +++ redmine-0.6.3-patched/lib/redmine.rb 2008-04-10 17:30:00.000000000 +0200 @@ -77,6 +77,7 @@ map.project_module :repository do |map| map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member + map.permission :commit_repository, {:repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]}, :require => :member map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph] map.permission :view_changesets, :repositories => [:show, :revisions, :revision] end diff -Nur redmine-0.6.3/extra/svn/RedmineSOAP.pm redmine-0.6.3-patched/extra/svn/RedmineSOAP.pm --- redmine-0.6.3/extra/svn/RedmineSOAP.pm 1970-01-01 01:00:00.000000000 +0100 +++ redmine-0.6.3-patched/extra/svn/RedmineSOAP.pm 2008-04-14 11:18:15.000000000 +0200 @@ -0,0 +1,302 @@ +package Apache::Authn::RedmineSOAP; + +=head1 Apache::Authn::RedmineSOAP + +RedmineSOAP - a mod_perl module to authenticate webdav subversion users +against redmine authentication system (using WebService) + +=head1 SYNOPSIS + +This module allow anonymous users to browse public project. Registered +users may have read-only or read-write access depending their 'role' +in their project. + +This method use the same login method as the redmine interface, so it +work no matter the auth sources setup in redmine. Password is cyphered +to ensure non divulgation in log or during the communication. It is +strongly recommended to use HTTPS for the WebService. +To improve performance a simple but configurable cache system has been +implemented (a 'svn co' requires up to 10 authentication requests) + +=head1 INSTALLATION + +For this to automagically work, you need to have a recent reposman.rb +(after r860) and if you already use reposman, read the last section to +migrate. + +Sorry ruby users but you need some perl modules, at least mod_perl2, +SOAP::Lite, File::Cache, Crypt::CBC, Crypt::Rijndael. + +On debian/ubuntu you must do : + + aptitude install libapache2-mod-perl2 libsoap-lite-perl libcrypt-cbc-perl \ + livcrypt-rijndael-perl libfile-cache-perl + +=head1 CONFIGURATION + + ## if the module isn't in your perl path + PerlRequire /usr/local/apache/RedmineSOAP.pm + ## else + # PerlModule Apache::Authn::RedmineSOAP + + DAV svn + SVNParentPath "/var/svn" + + AuthType Basic + AuthName redmine + Require valid-user + + PerlAccessHandler Apache::Authn::RedmineSOAP::access_handler + PerlAuthenHandler Apache::Authn::RedmineSOAP::authen_handler + + # Verbosity level (0: disabled) + PerlSetVar verbosity 0 + + # secret key for password encryption (the same as redmine + # configuration) + PerlSetVar encryption_key "2p7PQ6FCFMWdBNK5SErQFQICUrZtSibJ" + + # Enable/disable cache + # Caching is strongly recommanded, at least with few seconds cache! + PerlSetVar enable_cache 1 + + # Caching system configuration + PerlSetVar private_project_expire "60 seconds" + PerlSetVar public_project_expire "60 seconds" + PerlSetVar auth_failed_expire "60 seconds" + PerlSetVar auth_ro_expire "60 seconds" + PerlSetVar auth_rw_neg "60 seconds" + + # Redming WebService URL + PerlSetVar url https://dev.ginkgo-networks.com + + + +To be able to browse repository inside redmine, you must add something +like that : + + + DAV svn + SVNParentPath "/var/svn" + Order deny,allow + Deny from all + # only allow reading orders + + Allow from redmine.server.ip + + + +and you will have to use this reposman.rb command line to create repository : + + reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/ + +=head1 MIGRATION FROM OLDER RELEASES + +If you use an older reposman.rb (r860 or before), you need to change +rights on repositories to allow the apache user to read and write +S + + sudo chown -R www-data /var/svn/* + sudo chmod -R u+w /var/svn/* + +And you need to upgrade at least reposman.rb (after r860). + +=cut + +use strict; + +use SOAP::Lite; +use File::Cache; + +use Digest::SHA qw(sha256); +use Crypt::CBC; + +use Apache2::Module; +use Apache2::Access; +use Apache2::ServerRec qw(); +use Apache2::RequestRec qw(); +use Apache2::RequestUtil qw(); +use Apache2::Const qw(:common); +# use Apache2::Directive qw(); + +my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; + +my $verbosity = 0; +my $file = "/tmp/redmine.auth.log"; + +my $url = "http://localhost"; + +## Password encryption +my $key = "redminedefaultkey"; + +## Cache system +my $cache = 1; +my $public_cache = new File::Cache( { namespace => 'svn_redmine_public_project' } ); +my $auth_cache = new File::Cache( { namespace => 'svn_redmine_auth' } ); + +# Expiration +# 0 --> negative +# 1 --> positive +my @public_expire = (5, 5); +my @auth_expire = (5, 5); + + +sub is_set { + my $r = shift; + my $key = shift; + return defined $r->dir_config($key); +} + +sub config { + my $r = shift; + + $verbosity = $r->dir_config("verbosity") if is_set($r,"verbosity"); + $cache = $r->dir_config("enable_cache") if is_set($r,"enable_cache"); + $key = $r->dir_config("encryption_key") if is_set($r,"encryption_key"); + $url = $r->dir_config("url") if is_set($r,"url"); + + @public_expire[0] = $r->dir_config("private_project_expire") if is_set($r,"private_project_expire"); + @public_expire[1] = $r->dir_config("public_project_expire") if is_set($r,"public_project_expire"); + + @auth_expire[0] = $r->dir_config("auth_failed_expire") if is_set($r,"auth_failed_expire"); + @auth_expire[1] = $r->dir_config("auth_ro_expire") if is_set($r,"auth_ro_expire"); + @auth_expire[2] = $r->dir_config("auth_rw_expire") if is_set($r,"auth_rw_expire"); +} + +sub access_handler { + my $r = shift; + config($r); + + unless ($r->some_auth_required) { + $r->log_reason("No authentication has been configured"); + return FORBIDDEN; + } + + my $repository_name = get_repository_name($r); + + $r->set_handlers(PerlAuthenHandler => [\&OK]) + if is_public_project($repository_name, $r); + + return OK +} + +sub authen_handler { + my $r = shift; + config($r); + + my ($res, $redmine_pass) = $r->get_basic_auth_pw(); + return $res unless $res == OK; + + Log(level => 1, text => "Checking auth"); + my $ret = get_credits($r->user, $redmine_pass, $r); + my $method = $r->method; + + if ($ret>=2) { + return OK; + } elsif($ret==1 && $read_only_methods{$method}==1) { + return OK; + } else { + $r->note_auth_failure(); + return AUTH_REQUIRED; + } +} + +sub is_public_project { + my $repository_name = shift; + my $r = shift; + + my $ret; + $ret = $public_cache->get($repository_name) if $cache; + if ( not defined $ret ) { + # We have to request to Redmine SAOP WebService if the project is public or not + + my $service = connect_redmine($r); + Log(level => 3, text => "requesting visibility for project $repository_name"); + $ret = $service->IsPublicProject($repository_name); + + # Add the response to the cache + if($cache) { + Log(level => 3, text => "project $repository_name is_public saving: $ret"); + $public_cache->set($repository_name, $ret, $public_expire[$ret]); + Log(level => 3, text => "project $repository_name is_public saved: $ret"); + } + } + + Log(level => 1, text => "Returned project $repository_name is_public: $ret"); + return $ret; +} + +sub get_credits { + my $redmine_user = shift; + my $redmine_pass = shift; + my $r = shift; + my $repository_name = get_repository_name($r); + + my $ret; + $ret = $auth_cache->get( "$repository_name/$redmine_user/"+sha256($redmine_pass) ) if $cache; + if ( not defined $ret ) { + # We have to request credentials to Redmine + my $service = connect_redmine($r); + Log(level => 2, text => "Access requested for $redmine_user for $repository_name: $ret"); + + # Encrypt the serial symmetricaly + # We cannot use a digest for password matching in case there are external auth system used in Redmine + my $iv = Crypt::CBC->random_bytes(16); + my $cipher = Crypt::CBC->new( + -cipher => 'Rijndael', + -key => sha256($key), + -iv => $iv, + -literal_key => 1, + -header => 'none'); + $cipher->start('encrypt'); + + my $ciphered_pass_hex = $cipher->encrypt_hex($redmine_pass); + my $iv_hex = unpack("H*", $iv); + Log(level => 4, text => "Ciphered password: $ciphered_pass_hex"); + + # Then, request: + $ret = $service->CanAccessRepository($repository_name,$redmine_user, $ciphered_pass_hex,$iv_hex); + + # Finally, cache the result + if($cache) { + Log(level => 3, text => "Saving value for ($redmine_user,$repository_name): $ret"); + $auth_cache->set( "$repository_name/$redmine_user/"+sha256($redmine_pass), $ret, $auth_expire[$ret]); + Log(level => 3, text => "Saved value for ($redmine_user,$repository_name): $ret"); + } + } + + Log(level => 1, text => "Returned value for ($redmine_user,$repository_name): $ret"); + return $ret; +} + +sub get_repository_name { + my $r = shift; + + my $location = $r->location; + my ($repository_name) = $r->uri =~ m{$location/*([^/]+)}; + $repository_name; +} + +sub connect_redmine { + my $r = shift; + + my $wsdl = "$url/sys/service.wsdl"; + + return SOAP::Lite->service($wsdl); +} + +sub Log { + my %args = (level => 0, text => '', @_); + + my $level = delete $args{level}; + my $text = delete $args{text}; + return unless $level <= $verbosity; + + open FILE, ">>$file" or die "unable to open $file $!"; + print FILE "$text\n"; + + exit $args{exit} + if defined $args{exit}; +} + +1;