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 = $list\n";
}
print $_;
}
$ldap->unbind;
(Yes, Perl’s even nastier when the blog software strips out the indentation and blank lines…)
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.
5 responses so far ↓
anoop // September 17, 2008 at 8:52 pm |
The script works very well. i had to tweak it a bit but i can’t remember now what i did to it. Anyway, just wanted to correct an error on the security.conf file you have above. You need the @ in front of Administrators when you’re using providing permissions to a group for a particular repo.
Alex // September 18, 2008 at 8:15 am |
Well spotted.
I’ve updated the post accordingly and brought the script up to date too. It’s had some bugfixes and style improvements since I originally posted this.
Alex // November 4, 2008 at 11:20 am |
It seems that I forgot to exclude disabled user accounts. Altering the get_member_users subroutine as follows should do this:
sub get_member_users($)
{
my($group) = @_;
my @users = ();
my $members = $group->get_value(‘member’, asref=>1);
foreach (@{$members}) {
my($mesg, $member, @objectClass, $accountState);
$mesg = $ldap->search(
base => $_,
filter => “(objectClass=*)”,
attrs => [ 'member', 'dn', 'sAMAccountName', 'objectClass', 'userAccountControl' ]
);
$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 ) {
$accountState = $member->get_value(‘userAccountControl’);
# Exclude disabled user accounts.
if ( ( $accountState & 2 ) == 0 ) {
push @users, @{$member->get_value(‘sAMAccountName’, asref=>1)};
}
}
}
return @users;
}
Nikolaus // June 4, 2010 at 8:50 am |
Hi,
the download location seems to be unavailable now. Could you fix the download link, please?
Thanks for your work!
Will // June 4, 2010 at 9:01 am |
Thanks Nikolaus – should be working again now!