[Rt-devel] Updating RT from an LDAP database

Petter Reinholdtsen pere at hungry.com
Wed Aug 10 07:28:11 EDT 2005


One of the needs we have here at the University of Oslo, is to keep
the information in RT up to date with the information in LDAP.  To
make this happen, I wrote a small script to update the info for all
"local" RT users with information from LDAP.

It could be useful for others, so I post it here as well.  The LDAP
connect and disconnect functions are available in an extention to
RT::USER on <URL:http://www.usit.uio.no/it/rt/modifications.html>.

#!/site/perl-5.8.6/bin/perl
#
# Author: Petter Reinholdtsen
# Date:   2005-08-08
# License: GPL
#
# Update RT user information with information from an LDAP database.

use lib ("/site/rt3/local/lib", "/site/rt3/lib");
use strict;
use warnings;

use Getopt::Std;

use Net::LDAP qw(LDAP_SUCCESS LDAP_SERVER_DOWN LDAP_OPERATIONS_ERROR);
use Net::LDAP::Util qw (ldap_error_name);
use RT::Interface::CLI qw(CleanEnv);
use RT::User;

my %opts;

getopts("dn", \%opts);

CleanEnv();
RT::LoadConfig();
RT::Init();

my %system_users = ('root'    => "Superuser",
                    'rt-user' => "Internal user",
                    'Nobody'  => "No user at all",
                    'RT_System' => "Internal user");

# Only the entries present here will be updated, no matter what the
# LDAP database contain.
#
# Not including Name to make sure users don't suddenly change user
# name because their mail address happen to point to a different user.
my @updatevalues =
    qw(
       EmailAddress
       RealName
       WorkPhone
       Address1
       );

# How to map from LDAP to RT values for the threes being queried for
# information.
my @LdapUserMap =
    (
     {
         base   => 'cn=targets,cn=mail,dc=uio,dc=no',
         search => 'target',
         filter => '(&(objectClass=mailAddr)(targetType=user))',
         scope  => 'one',
         map    => {'target'             => 'Name',
                    'defaultMailAddress' => 'EmailAddress'}
     },
     {
         base   => 'cn=people,dc=uio,dc=no',
         search => 'uid',
         filter => '(objectClass=person)',
         scope  => '',
         map    => {'cn'              => 'RealName',
                    'telephoneNumber' => 'WorkPhone',
                    'street'          => 'Address1'}
     },
     {
         base   => 'cn=users,cn=system,dc=uio,dc=no',
         search => 'uid',
         filter => '(objectclass=posixAccount)',
         scope  => '',
         map    => {'cn'              => 'RealName',
                    'telephoneNumber' => 'WorkPhone'},
     }
     );

my $ldaphandle = RT::User::LdapConnect();

# Loop over all RT users
my $users = new RT::Users($RT::SystemUser);
while (my $user = $users->Next) {
    my $username = $user->Name;
    next if ($system_users{$username});
    next if ($username =~ m/@/); # Ignore external users

    my %current;
    for my $key (@updatevalues) {
        $current{$key} = $user->$key;
    }

    my ($found, %userinfo) =
        LookupExternalUserInfoByName($user->Name);

    # XXX LDAP lookup of RealName is not implemented in
    # LookupExternalUserInfo() [pere 2005-08-08]
    if (!$found) {
        my $msg = "User '$username' is missing in LDAP";
        $RT::Logger->info("$msg"); print "$msg\n" if $opts{'d'};
        # XXX Deleted user?  Should it be converted to an external
        # user, by replacing the username with the mail address?
        # Not sure.  Ignore it for now. [pere 2005-08-08]
        next;
    }
    for my $key (sort keys %userinfo) {
        #print "  $key - '$userinfo{$key}' cmp '$current{$key}'\n" if $opts{'d'};
        if (exists $current{$key} && $current{$key} ne $userinfo{$key}) {
            my $dryrun = $opts{'n'} ? " (dryrun)" : "";

            my $msg = "Updating $key for user '$username' from LDAP: ".
                "'$current{$key}' to '$userinfo{$key}'$dryrun";
            $RT::Logger->info("$msg"); print "$msg\n" if $opts{'d'};

            my $method = "Set$key";
            my ($retval, $retmsg) = $user->$method($userinfo{$key})
                unless $opts{'n'};
        }
    }
}

RT::User::LdapDisconnect($ldaphandle);

exit 0;

=head2  LookupExternalUserInfoByName

Look up a username in several subtrees of LDAP, and pass the
information found back to the caller.

=cut

sub LookupExternalUserInfoByName {
    my ($name) = @_;

    # Wash the name to avoid surprises when searching for it.
    if ($name =~ m/^([a-z0-9_.-]+)$/) {
        $name = $1;
    } else {
        my $msg = "LookupExternalUserInfoByName: ".
            "Illegal username '$name' rejected.";
        $RT::Logger->debug($msg); print "$msg\n" if $opts{'d'};
        return (0, undef);
    }

    my %userinfo;
    my $found = 0;
    for my $ldaptree (@LdapUserMap) {
      retry:
        my @attrs = keys %{$ldaptree->{'map'}};
        my $filter = "(&($ldaptree->{search}=$name)$ldaptree->{filter})";
        my $mesg = $ldaphandle->search(base   => $ldaptree->{base},
                                       filter => $filter,
                                       attrs  => [@attrs]);

        # Handle timeouts
        if (($mesg->code == LDAP_SERVER_DOWN) or
            ($mesg->code == LDAP_OPERATIONS_ERROR)) {
            my $msg = "LookupExternalUserInfoByName: ".
                "Connection time out.  Reconnecting";
            $RT::Logger->debug($msg); print "$msg\n" if $opts{'d'};
            $ldaphandle = RT::User::LdapConnect();
            goto retry if ($ldaphandle);
            $msg = "LookupExternalUserInfoByName: ".
                "Reconnect failed.  Giving up!";
            $RT::Logger->debug($msg); print "$msg\n" if $opts{'d'};
            last;
        }

        if ($mesg->code == LDAP_SUCCESS) {
            if (1 == $mesg->count) {
                # Got working result.  Use it.
                while( my $entry = $mesg->shift_entry) {
                    foreach my $attr (keys %{$ldaptree->{'map'}}) {
                        foreach my $value ($entry->get_value($attr)) {
                            # Let Perl know this is UTF-8
                            $value = Encode::decode_utf8( $value );
                            $userinfo{$ldaptree->{'map'}{$attr}} = $value;
                        }
                    }
                }
                $found = 1;
            } elsif (1 < $mesg->count) {
                my $msg = "LookupExternalUserInfoByName: ".
                    "Searching for $filter returned $mesg->count entries.  ".
                    "It should return only one.";
                $RT::Logger->debug($msg); print "$msg\n" if $opts{'d'};
            } # else count == 0 -> no hit, nothing to report
        } else {
            my $msg = "LookupExternalUserInfoByName: ".
                "Could not search for $filter: " .
                "retval=" . $mesg->code . " " . ldap_error_name($mesg->code);
            $RT::Logger->debug($msg); print "$msg\n" if $opts{'d'};
        }
    }
    return ($found, %userinfo)
}


More information about the Rt-devel mailing list