ubuntu 上 redmine 安装及 svn 权限绑定

本文并不适用于所有的需求。适用环境如下:

  • redmine 用户分为程序员和普通成员,普通成员不可访问 svn,程序员可以访问,并且可以访问全部 svn 代码。
  • 只有项目组成员才被允许向版本库中提交代码。

因为 apache 认证模块的限制,未能实现 svn 的浏览权限与项目绑定。因此程序员允许查阅全部代码。

一、下载并安装 redmine

sudo apt-get install apache2 libapache-ruby1.8 apache2-mpm-worker libapache2-mod-passenger
sudo apt-get install redmine redmine-sqlite
sudo chown www-data /usr/share/redmine/config/environment.rb

安装时选择 sqlite 数据库。

二、配置 apache

编辑文件:/etc/apache2/sites-available/default

<VirtualHost *:80>
  RailsBaseURI /
  SetEnv X_DEBIAN_SITEID default
  ServerName redmine
  DocumentRoot /usr/share/redmine/public

  <Directory "/usr/share/redmine/public">
    AllowOverride None
    Order allow,deny
    Allow from all
  </Directory>

</VirtualHost>

三、修改密码存储方式兼容 apache 认证

修改文件:app/models/user.rb

         return nil unless user.auth_source.authenticate(login, password)
       else
+        if user.hashed_password[0,5] != '{SHA}'
+          pw_s = ""
+          user.hashed_password.unpack(
+                  'a2'*(user.hashed_password.length / 2)).collect do |x|
+            pw_s << x.hex
+          end
+          user.hashed_password = '{SHA}' + Base64.encode64(pw_s).chomp
+        end
+
         return nil unless User.hash_password(password) == user.hashed_password
       end
     else
@@ -274,8 +287,10 @@

 private
   def self.hash_password(clear_password)
-    Digest::SHA1.hexdigest(clear_password || "")
+    '{SHA}' + Base64.encode64(Digest::SHA1.digest(clear_password || "")).chomp
   end
 end

四、安装 dbi 认证

cd /etc/apache2/mods-enabled
sudo ln -s ../mods-available/dbd.load
sudo ln -s ../mods-available/authn_dbd.load
sudo ln -s ../mods-available/auth_basic.load

五、修改认证方式,并关联 redmine 数据库

修改文件:/etc/apache2/mods-available/dav_svn.conf

DBDriver sqlite3
DBDParams "/var/redmine/default/redmine_default" 

<Location /svn>
  DAV svn
  SVNPath /var/svn
  AuthType Basic
  AuthName "Private area" 

  AuthBasicProvider dbd

  AuthDBDUserPWQuery "select hashed_password from users where login=%s and type='User' and status=1 and id in ( select user_id from groups_users,users where group_id=id and type='Group' and (lastname='svn' or lastname='svn_commit'))" 

  Require valid-user
</Location>

至此,svn 已被设置为只有属于 svn 或者 svn_commit 两个组的激活用户可以访问 svn,全部具备读写权限。

六、限制提交权限

在 svn\hooks 目录下创建 pre-commit.pl,内容如下。程序中第六行需修改为实际的 sqlite 数据库路径,第 11 行需修改为 svn 的本地实际路径。

#!/usr/bin/env perl

use strict;
use Carp;
use DBI;
my $dbh = DBI->connect("dbi:SQLite:/var/redmine/default/redmine_default") || die "Cannot connect: $DBI::errstr";

my $repos        = shift;
my $txn          = shift;
my $svnlook = "/usr/bin/svnlook";
my $svnbase = "file:///var/svn/";

my $minchars = 4;

my @svnlooklines = &read_from_process($svnlook, 'log', $repos, '-t', $txn);
my $comment = shift @svnlooklines;

unless (length $comment) {
  die "A comment is required!\n";
  }

if ( length($comment) < $minchars ) {
  die "Comment must be at least $minchars characters.\n";
  }

# Get the author from svnlook.
my @svnlooklines = &read_from_process($svnlook, 'author', $repos, '-t', $txn);
my $author = shift @svnlooklines;
unless (length $author)
  {
    die "$0: txn '$txn' has no author.\n";
  }

# Figure out what directories have changed using svnlook..
my @dirs_changed = &read_from_process($svnlook, 'dirs-changed', $repos,
                                      '-t', $txn);

# Lose the trailing slash in the directory names if one exists, except
# in the case of '/'.
my $rootchanged = 0;
for (my $i=0; $i<@dirs_changed; ++$i)
  {
    if ($dirs_changed[$i] eq '/')
      {
        $rootchanged = 1;
      }
    else
      {
        $dirs_changed[$i] =~ s#^(.+)[/\\]$#$1#;
      }
  }

# Figure out what files have changed using svnlook.
my @files_changed;
foreach my $line (&read_from_process($svnlook, 'changed', $repos, '-t', $txn))
  {
    # Split the line up into the modification code and path, ignoring
    # property modifications.
    if ($line =~ /^..  (.*)$/)
      {
        push(@files_changed, $1);
      }
  }

# Create the list of all modified paths.
my @changed = (@dirs_changed, @files_changed);

# There should always be at least one changed path.  If there are
# none, then there maybe something fishy going on, so just exit now
# indicating that the commit should not proceed.
unless (@changed)
  {
    die "$0: no changed paths found in txn '$txn'.\n";
  }

# check admin
my $dbconn = $dbh->prepare("select login from users where login='$author' and admin='t'");
$dbconn->execute();

if ( $dbconn->fetchrow_array )
{
  exit 0;
}

# check group svn_commit
my $dbconn = $dbh->prepare("select login from users where login='$author' and type='User' and status=1 and id in ( select user_id from groups_users,users where group_id=id and type='Group' and lastname='svn_commit')");
$dbconn->execute();

unless ( $dbconn->fetchrow_array )
{
  die "user '$author' does not have permission to commit.\n";
}

my %permissions;
foreach my $path (@changed)
  {
    $permissions{$path} = 0;
  }

my $dbconn = $dbh->prepare("select substr(url, length('$svnbase')+1) from users,repositories,members where repositories.project_id=members.project_id and users.id=members.user_id and users.login='$author' and url like '$svnbase%'");
$dbconn->execute();

while( my $row = $dbconn->fetchrow_array)
{
  my ($url) = $row;

  if (substr($url, -1) eq "/")
  {
    $url = substr($url, 0, length($url) - 1);
  }

  foreach my $path (@changed)
  {
    if( $path eq $url || ( substr( $path, 0, length($url)) eq $url && substr( $path, length($url), 1) eq "/" ))
    {
      $permissions{$path} = 1;
    }
  }
}

my $exit_code = 0;

foreach my $path (@changed)
{
  if($permissions{$path} ne 1)
  {
    print STDERR "user '$author' does not have permission to commit to '$path'\n";
    $exit_code = 1;
  }
}

exit $exit_code;

# All checks passed, so allow the commit.

sub safe_read_from_pipe
{
  unless (@_)
    {
      croak "$0: safe_read_from_pipe passed no arguments.\n";
    }
  print "Running @_\n";
  my $pid = open(SAFE_READ, '-|');
  unless (defined $pid)
    {
      die "$0: cannot fork: $!\n";
    }
  unless ($pid)
    {
      open(STDERR, ">&STDOUT")
        or die "$0: cannot dup STDOUT: $!\n";
      exec(@_)
        or die "$0: cannot exec '@_': $!\n";
    }
  my @output;
  while (<SAFE_READ>)
    {
      chomp;
      push(@output, $_);
    }
  close(SAFE_READ);
  my $result = $?;
  my $exit   = $result >> 8;
  my $signal = $result & 127;
  my $cd     = $result & 128 ? "with core dump" : "";
  if ($signal or $cd)
    {
      warn "$0: pipe from '@_' failed $cd: exit=$exit signal=$signal\n";
    }
  if (wantarray)
    {
      return ($result, @output);
    }
  else
    {
      return $result;
    }
}

sub read_from_process
  {
  unless (@_)
    {
      croak "$0: read_from_process passed no arguments.\n";
    }
  my ($status, @output) = &safe_read_from_pipe(@_);
  if ($status)
    {
      if (@output)
        {
          die "$0: '@_' failed with this output:\n", join("\n", @output), "\n";
        }
      else
        {
          die "$0: '@_' failed with no output.\n";
        }
    }
  else
    {
      return @output;
    }
}

修改完成后,svn 权限被设置成为必须属于项目组成员才可以提交。

七、配置示例

下面举例说明如何具体使用:

假设 svn 路径为 /var/svn,项目为 prj1

在 redmine 的版本库管理里,需要选择 subversion 并设定地址为:file:///var/svn/prj1

svn 将自动按照 file:///var/svn/ 匹配所属项目,并检验用户是否为合法项目成员,验证成功后才会予以提交。




Related posts

coded by nessus

发表评论