Gebändigtes Leichtgewicht

LDAP breitet sich als Verzeichnisdienst zwar aus, seine Verwaltung ist jedoch Laien kaum zuzumuten. Mit Hilfe der passenden Perl-Schnittstellen lässt sich ein CGI-Programm bauen, das die üblichen Administrationsaufgaben vereinfacht.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 7 Min.
Von
  • Reinhard E. Voglmaier
Inhaltsverzeichnis

Typisch für den IT-Bereich in großen Unternehmen ist eine äußerst heterogene Systemlandschaft, die von Windows-Varianten über Unix bis zu Mainframes reicht. Die resultierenden Probleme wie Passwort-Proliferation oder redundante Datenhaltung und deren Lösung durch Einsatz von LDAP (Lightweight Directory Access Protocol) haben einige Autoren beschrieben [1, 2].

Neben viel kommerzieller LDAP-Software existiert die frei verfügbare Implementierung OpenLDAP, die auf einer Entwicklung der University of Michigan basiert. Die Konfiguration des Servers beschränkt sich auf das Editieren einiger ASCII-Dateien und bereitet keine großen Schwierigkeiten. Die Verwaltung der Daten geht jedoch über die Fähigkeiten eines Laien hinaus, sodass für den Einsatz beim Endanwender ein GUI notwendig ist, um die Pflege der Datenbank zu erleichtern. Dieser Artikel beschreibt, wie man mit OpenSource-Werkzeugen eine einfache Web-Anwendung schreiben kann, die die LDAP-Administration wesentlich vereinfacht.

Für eine große Pharmafirma war eine Applikation zu entwickeln, die Zugriffe auf Apache mittels einer LDAP-Datenbank kontrolliert. Die Benutzer sollten sich selbst eintragen und ihre Daten aktualisieren können. Augenblickliche Plattform ist Solaris, eine spätere Installation auf anderen Systemen nicht ausgeschlossen. Es bot sich deshalb an, die Aufgabe als CGI-Programm zu realisieren.

Wegen Zeitmangels und aus Rücksicht auf den Firmenstandard diente Perl als Implementierungssprache. Den Zugriff auf LDAP realisiert ein Modul, von denen zwei auf dem CPAN zur Auswahl stehen: Perl-ldap von Graham Barr und Clayton Donleys Net::LDAP.

Beide APIs bieten search()-, update()- und delete()-Methoden. Die Benutzung ähnelt sich ebenfalls: Man erzeugt ein LDAP-Objekt und verwendet dessen Methoden, um sich mit dem Server zu verbinden. Ein kleines Beispiel mit Perl-ldap enthält Listing 1. Nachdem die notwendigen Parameter $base und $filter definiert sind, erzeugt es ein LDAP-Objekt. Die bind-Methode stellt eine anonyme Verbindung zum Server her (mehr dazu später). Klappt das, ruft das Script die search()-Methode des LDAP-Objekts auf. Sie liefert ein Ergebnisobjekt zurück, das wiederum Methoden für die Kontrolle auf Fehler, Iteration über die Ergebnisse und deren Ausgabe enthält. Die Syntax der delete()-, update()- und insert()-Operationen ist ähnlich einfach. Eine äquivalente Formulierung mit Net::LDAPapi sieht ähnlich aus (s. Listing 2).

Mehr Infos

Listing 1

#!/usr/local/bin/perl -w

use Net::LDAP ;

$User = $ARGV[0] ;
print "Daten für: $User \n" ;

local $Base = "dc=heise, dc=de" ;
local $Filter = "uid=$User" ;
local $Host = "localhost" ;

$ldap = Net::LDAP->new($Host) or die "$@" ;
$ldap->bind || die "Could not bind to $Host" ;

$mesg = $ldap->search (
base => $Base,
filter => $Filter
);
$mesg->code && die $mesg->error ;

if ( $mesg->count == 0 ) {
print "NO ROW FOUND \n" ;
}
foreach $entry ( $mesg->all_entries ) { $entry->dump; }

$ldap->unbind ;
Mehr Infos

Listing 2

#!/usr/local/bin/perl -w

use Net::LDAPapi;

$User = $ARGV[0] ;
print "User data for: $User \n" ;

local $Base = "dc=heise,dc=de" ;
local $Filter = "uid=$User" ;

local $Scope = LDAP_SCOPE_SUBTREE;
local $AttributesOnly = 0;

$ldap = Net::LDAPapi->new('localhost')
|| die "Keine Verbindung: $@";

if ( $ldap->bind_s() != LDAP_SUCCESS) {
die $ldap->errstring;
}

@resultlist = ();

if ($ldap->search_s($Base, $Scope, $Filter,\@resultlist,$AttributesOnly) !=
LDAP_SUCCESS) {
die $ldap->errstring;
}

%record = %{$ldap->get_all_entries};

foreach (sort keys %record) {
print "dn: $_\n";
foreach $attr (keys %{$record{$_}}) {
for $item ( @{$record{$_}{$attr}}) {
print "$attr: $item\n";
}
}
}


$ldap->unbind();

Mit den Perl-Scripts für select, insert und modify ist schon über die Hälfte der Anwendung geschrieben. In Listing 3 finden sich kurze Beispiele. Alle Scripts erfordern schreibenden Zugriff auf die Datenbank. Dazu sind dem bind-Aufruf UserId und Password mitgegeben. Das aktuelle LDAP-Modul (1.42) liefert leider nicht in allen Fällen eine aussagekräftige Fehlermeldung zurück, sodass die Scripts gegebenenfalls lediglich das Scheitern mitteilen, jedoch keinen Grund dafür.

Mehr Infos

Listing 3

### Datensatz löschen

#!/usr/local/bin/perl -w

use Net::LDAP ;

my $Base = "dc=heise,dc=de" ;
my $Host = "localhost" ;
my $DN_ADM = "cn=root,$Base" ;
my $DN = "uid=EMeier, $Base";

$ldap = Net::LDAP->new($Host) or die "$@" ;
my $result = $ldap->bind (dn => $DN_ADM,
password => "secret") ;
$result->code &&
print "Could not bind to LDAP-Server: ",$result->error;

$result = $ldap->delete($DN);
$result->code &&
print "Could not delete $DN: ",$result->error;
$ldap->unbind();

### Datensatz einfügen

#!/usr/local/bin/perl -w

use Net::LDAP ;

local $Base = "dc=heise,dc=de" ;
local $Host = "localhost" ;
local $DN_ADM = "cn=root,$Base" ;

$ldap = Net::LDAP->new($Host) or die "$@" ;
$ldap->bind ( dn => $DN_ADM,
password => "secret" ) ||
die "Could not bind to $Host" ;
@objectclasses = ['top','person','organizationalPerson','inetOrgPerson'];
$Result = $ldap->add( dn => "uid=EMeier, $Base",
attr=> [ sn => "Meier",
givenname => "Erich",
uid => "EMeier",
cn => "Erich Meier",
password => "pass1" ,
objectclass => @objectclasses ] ) ;
$Result->code &&
print "Failed to add entry:", $Result->error ;
$ldap->unbind ();

### Datensatz ändern
### - Neues Feld hinzufügen

#!/usr/local/bin/perl -w

use Net::LDAP ;

local $Base = "dc=heise,dc=de" ;
local $Host = "localhost" ;
local $DN_ADM = "cn=root,$Base" ;

$ldap = Net::LDAP->new($Host) or die "$@" ;
$ldap->bind ( dn => $DN_ADM,
password => "secret" ) ||
die "Could not bind to $Host" ;
$Result = $ldap->modify(uid="EMeier, $Base");
add => { 'mail' => "EMeier\@my.provider.com" } );
$Result->code &&
print "Failed to add entry:", $Result->error ;
$ldap->unbind ();

### - Feld verändern

#!/usr/local/bin/perl -w

use Net::LDAP ;

local $Base = "dc=heise,dc=de" ;
local $Host = "localhost" ;
local $DN_ADM = "cn=root,$Base" ;

$ldap = Net::LDAP->new($Host) or die "$@" ;
$ldap->bind ( dn => $DN_ADM,
password => "secret" ) ||
die "Could not bind to $Host" ;

$DN = "uid=EMeier,$Base" ;
$Result = $ldap->modify("uid=EMeier,$Base");
replace => { 'password' => "pass2" } );
$Result->code &&
print "Failed to change entry:", $Result->error ;
$ldap->unbind ();

### - Feld löschen

#!/usr/local/bin/perl -w

use Net::LDAP ;

local $Base = "dc=heise,dc=de" ;
local $Host = "localhost" ;
local $DN_ADM = "cn=root,$Base" ;

$ldap = Net::LDAP->new($Host) or die "$@" ;
$ldap->bind ( dn => $DN_ADM,
password => "secret" ) ||
die "Could not bind to $Host" ;

$Result = $ldap->modify("uid=EMeier, $Base",
delete => "password");
$Result->code &&
print "Failed to change entry:", $Result->error ;
$ldap->unbind ();

Es fehlen noch das GUI und die Zugriffskontrolle. Ersteres liefert der Browser gratis, wenn man ihm den richtigen HTML-Code schickt. Lincoln Steins CGI-Modul tut genau das Gewünschte, wie das Beispiel in Listing 4 zeigt.

Mehr Infos

Listing 4

#!/usr/bin/perl -w

# include der CGI, CGI-Fehlermodule und LDAP API:
use CGI;
use CGI::Carp qw(fatalsToBrowser);
use Net::LDAP;

# Parameter fuer LDAP Verbindung:
my $ldap_host = "localhost" ;
my $ldap_port = 389 ;
my $ldap_base = "o=GlaxoWellcome, c=IT";
my $dn = "";
my $pwd = "" ;

# Neues CGI Object
my $query = new CGI;

# Ausdrucken des Headers mit Document Typ
print $query->header('text/html'),
$query->start_html(-title=>'Search Form'),
$query->h1("PerLDAP Search Gateway");

# Query-Block. Falls der Parameter filter definiert ist,
# wird die Query ausgefuehrt:

if ( ($filter=$query->param('filter'))) {

@attrs = ['uid','cn'];
$ldap = new Net::LDAP('localhost');
$ldap->bind() || die "Could not bind to LDAP server" ;
$mesg = $ldap->search( base => $ldap_base,
filter => $filter,
attrs => @attrs
) ;
if ($mesg->code) {
warn $mesg->error;
} else {
&DisplayResult($mesg);
$ldap->unbind();
}
} else {
# Kein Parameter, also erster Aufruf des Scripts
my $action = $query->url;
print $query->startform(-method => 'POST',
-action => $action,
);
print "<TABLE border=0>\n";
print "<TR><TD>Enter LDAP Search Field</TD>";
print "<TD>",$query->textfield('filter',"",50),
"</TD></TR></TABLE>";
print $query->br();
print $query->submit();
print $query->endform();
}

print $query->end_html();

##########################################################
# Hilfsfunktionen
##########################################################

sub DisplayResult {
my ( $mesg ) = @_ ;

if ( ($Number=$mesg->count()) ) {
for ($i = 0 ; $i < $Number ; $i++ ) {
my $entry = $mesg->entry($i);
printf("Distinct Name: %s\n\n",$entry->dn());
foreach $attr ($entry->attributes) {
print "$attr\t", $entry->get($attr),"\a";
}
}
} else {

printf("Search did not return any result\n");
}
}

Was man noch hinzufügen könnte, wäre die Kontrolle der eingegebenen Daten, neue Fenster für kompliziertere Queries oder eine komfortablere Benutzerführung. Dazu genügt JavaScript in der Header-Anweisung oder eine Datei als Parameter im Aufruf von html->start_html():

print $HTML->start_html(
-title=>"DataBase Administration",
-onUnload=>"cleanup();",
-onLoad=>"InitArrays();",
-script=>{ -language => "JavaScript",
-src=>"./DBAdmin1.js" },
);

Bei der Formulierung der Suche ist die LDAP-Grammatik etwas eigenwillig. Jede Abfrage der Datenbank benötigt folgende Elemente:

  • die gewünschten Ergebnisdaten (Attribute), beispielsweise Name, Vorname, Adresse, TelefonNummer;
  • die Basis, sozusagen den Ausgangspunkt der Suche und
  • den Filter.

Letzterer ist das Suchkriterium oder die logische Verknüpfung mehrerer davon. Will man zum Beispiel nicht alle vorhandenen Datensätze erhalten, sondern nur den von Reinhard Voglmaier, sieht der Filter so aus: (sn=Reinhard Voglmaier). Verknüpfte Bedingungen, etwa alle Meiers, die in der IT- oder der ScientificData-Abteilung arbeiten, werden schnell unleserlich:

(&(sn=Meier)(|(ou=IT)(ou=ScientificData))).

Die Suchmaske benutzt Perl und JavaScript, um LDAP-Filter zu erstellen.

Benutzern kann man die Eingabe solcher Ausdrücke nicht zumuten. Um ihnen trotzdem die Suche mit selbstdefinierten Kriterien zu ermöglichen, benutzen die CGI-Scripts ein Formular und ein wenig JavaScript, das die Suchbedingungen in LDAP-verständliche Form bringt. Ein Formular zeigt Abbildung 1, die dazugehörenden Dateien finden sich auf dem Listingserver der iX.

Für einfaches Suchen schließlich existiert ein Formular, das eine Reihe von Feldern enthält, im konkreten Fall sind dies Vorname, Nachname, UserId und Wohnort. Das CGI-Script prüft, in welchen dieser Felder der Anwender Suchbegriffe eingegeben hat und verknüpft diese durch ‘und’. Nach der Konstruktion des Filters

$filter = sprintf("( & ( cn = %s ) ( sn = %s ) )",$cn,$sn);

baut das Script die Verbindung zum LDAP-Server auf und erzeugt mit $ldap->search(...) ein Result-Objekt. Es enthält Informationen über die gefundenen Datensätze. Anschließend liefert das Script die gewünschten Felder aus den gefundenen Datensätzen. Die dazu notwendigen Objekte und Methoden enthält der Kasten ‘Suchmethoden’.

SuchMethoden
$ldap = new NET::LDAP($Host) definiert neues LDAP-Objekt
$ldap-[VERBATIM10]bind() Anmeldung beim LDAP-Server
$result = $ldap-[VERBATIM11]search() Suche nach Datensätzen
$number = $result-[VERBATIM12]count() Anzahl gefundener Datensätze
$entry-[VERBATIM13]get(’Field1’) liefert Field1 des Datensatzes
$entry-[VERBATIM14]dn() Distinct Name des Datensatzes
$entry-[VERBATIM15]dump() Datensatz ausgeben
$ldap-[VERBATIM16]unbind() Verbindung zum LDAP-Server lösen

Als letzter Baustein fehlt die Zugriffskontrolle. In der Wirklichkeit darf wohl kaum jeder sämtliche Einträge in der Datenbank sehen, geschweige denn ändern. Der LDAP-Server prüft selbstständig, ob der Benutzer zu einer Aktion berechtigt ist. Er benötigt dazu die im bind-Befehl gelieferten Benutzerdaten, das heißt UserId und Password. Ruft man bind ohne diese Parameter auf, bekommt man einen anonymen Zugang, bei dem nur bestimmte vom Administrator per Konfigurationsdatei festgelegte Operationen zulässig sind. Im Perl-LDAP-Stil sieht dies so aus:

$DN = "cn = $UserId, o=glaxowellcome, c=IT" ;
$ldap->bind (
dn => $DN,
password => $Password
);

Damit sind alle geforderten Funktionen implementiert. Zu den Aufgaben des LDAP-Verwalters gehört es außerdem, die Datenbank zu exportieren, zu importieren und die Indizes neu zu bilden. Auch die Serverüberwachung mit einem Monitor der Log-Dateien wäre wünschenswert. Im Moment bewerkstelligen dies in unserer Implementierung die bei OpenLDAP gelieferten Kommandozeilenwerkzeuge, für die es einen CGI-Wrapper gibt.

Im Moment führt das CGI-Script die Authentifizierung für die Verwaltung durch. Da der Apache dies mit Hilfe eines LDAP-Moduls ebenfalls erledigen kann, fehlt nur noch die Anbindung dieser Authentifizierung an das CGI-Script. Dies wird der nächste Schritt bei der Weiterentwicklung der Anwendung sein.

Reinhard E. Voglmaier
ist verantwortlich für Internet & Intranet Services bei GlaxoWellcome, Geschäftsbereich Italien.

[1] Klaus Basan, Martin Glas, Diemo Shergowski; Gemeinsamer Nenner; LDAP integriert Apache Webserver und Lotus Domino; iX 11/1999, S. 189

[2] Ingo Lütkebohle, Christian Kirsch; ... daß ich Rumpelstilzchen heiß; Einsatz eines freien LDAP-Servers; iX 5/1999, S. 155 ff.

Mehr Infos

iX-TRACT

  • Für den Zugriff auf LDAP-Server gibt es zwei Perl-Module, die ähnliche Funktionen enthalten.
  • In Kombination mit dem CGI-Modul lässt sich mit Net::LDAP ein Verwaltungs-GUI für LDAP-Server erstellen.
  • Mit Apache kann man die Authentifizierung ebenfalls via LDAP erledigen.

(ck)