SVN and Active Directory

Our old SVN/Trac system is getting a shakeup. We’re constructing a new wiki project for the next version of our product and this seemed like the ideal time to rearrange the furniture. Trac’s getting moved to an actual database (backing up an SQLite DB centrally was getting annoying) and SVN’s finally getting its own server. A properly-configured reverse proxy acting as gateway to all the services should eliminate differences between internal and external URLs for a given service. The whole shebang’s being moved to Ubuntu Linux 7.04 too, since NFSv4 is much nicer for running shared repos and Trac project dirs than Windows Networking, and although we’re not running redundant/load-balanced Trac servers yet it’s probably a good idea to think ahead.

Of course, the reason we used Windows Server originally was that we needed Active Directory integration for Trac and SVN, and the Apache SSPI module is not available on Linux. Group-based, per-directory access control is awkward even with that setup, but there’s a solution: using mod_authz_svn for access control and running a script to synchronise the module’s user file with Active Directory via LDAP. I wasn’t able to find such a script, but hacking one together didn’t take long:


#!/usr/bin/perl
use warnings;
use strict;
use Net::LDAP;
sub resolve_group($);
sub get_member_users($);
sub get_users_in_group($);
# Domain name.
my $ldap_domain = "<your domain name here>";
# DN of the LDAP directory root.
my $ldap_root = "dc=".$ldap_domain;
$ldap_root =~ s/./,dc=/g;
# Domain Controllers.
my @dcs = ( 'dc1', 'dc2' );
# DN of the LDAP directory base for our queries.
my $ldap_base = "<your base DN here>,$ldap_root";
# Authorisation of guest account used for queries.
my $ldap_username = "<guest AD user DN here>";
my $ldap_password = "<guest AD user's password here>";
#====================================
# Connect and bind to LDAP server.
my $ldap;
foreach (@dcs) {
$ldap = Net::LDAP->new("$_.$ldap_domain") and last;
}
$ldap or die "$@";
$ldap->bind($ldap_username, password => $ldap_password) or die "$@";
# Gets a group by cn.
sub resolve_group($)
{
my($name) = @_;
my $mesg = $ldap->search(
base => $ldap_base,
filter => "(&(cn=$name)(objectClass=group))",
sizelimit => 1
) or die "$@";
return $mesg->entry(0);
}
# Gets sAMAccountName attribute from all member users of a group, descending
# recursively into member groups.
sub get_member_users($)
{
my($group) = @_;
my @users = ();
my $members = $group->get_value('member', asref=>1);
foreach (@{$members}) {
my($mesg, $member, @objectClass);
$mesg = $ldap->search(
base => $_,
filter => "(objectClass=*)",
attrs => [ 'member', 'dn', 'sAMAccountName', 'objectClass' ]
);
$member = $mesg->entry(0);
@objectClass = @{$member->get_value('objectClass', asref=>1)};
if ( grep $_ eq "group", @objectClass ) {
push @users, get_member_users($member);
}
if ( grep $_ eq "user", @objectClass ) {
push @users, @{$member->get_value('sAMAccountName', asref=>1)};
}
}
return @users;
}
sub get_users_in_group($)
{
my($groupName) = @_;
my $group = resolve_group($groupName);
return keys %{{ map { lc $_ => 1 } get_member_users($group) }};
}
my($section, @usernames);
while (readline STDIN) {
if($_ =~ /[(.*?)]/) {
$section = $1;
} elsif($section eq "groups" and $_ =~ /^(.*?)s*=/) {
@usernames = get_users_in_group($1);
my $list = join ', ', @usernames;
$_ = "$1 = $listn";
}
print $_;
}
$ldap->unbind;

(Yes, Perl’s even nastier when the blog software strips out the indentation and blank lines…)

Download update-groups.pl.

Active Directory will not allow an LDAP client to operate against it anonymously, therefore you will have to provide a user DN and a password in plaintext in the script. Fortunately, the permissions required for this script to operate are minimal. Simply create a new user in the Directory, add it to Domain Guests, set Domain Guests as primary group, and remove from Domain Users. Assuming you haven’t granted extra privileges to Domain Guests, the script will get no more than it needs to operate.

Fill in the fields appropriately for your Active Directory and write a mod_authz_svn user file, eg.:

[groups]
Administrators =
[/]
* = r
@Administrators = rw

When run against this, the Perl script will look up every group name specified in [groups], extract all the member user names from the Active Directory, and write out the new group definition. The script takes input on STDIN and generates output on STDOUT, so run something like the following as a cron job (adjust paths as necessary):

#!/bin/bash
cd /svn
cat security.conf | perl update-groups.pl > security.conf.new && mv security.conf.new security.conf

This will only replace the user file if the new one is generated without errors.

Of course, to authenticate HTTP access to the repo you will have to configure Apache to use LDAP appropriately too. Something like the following works:

<Location />
AuthLDAPURL "ldap://<your domain controllers here>/<full DN for LDAP base here>?sAMAccountName?sub?(objectClass=user)"
AuthLDAPBindDN "<guest AD user's DN here>"
AuthLDAPBindPassword <guest AD user's password here>"
AuthBasicProvider ldap
AuthUserFile /dev/null
AuthType Basic
AuthzLDAPAuthoritative off
AuthName "Repository"
Require valid-user
DAV svn
SVNParentPath /svn
AuthzSVNAccessFile /svn/security.conf
</Location>

As before, fix up paths and values as necessary. Something similar can be used for controlling access to Trac. Don’t forget to add the mod_authz_svn, mod_authnz_ldap and mod_ldap modules to your Apache configuration, depending on Linux distro and version.