PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘Active Directory’ tag

Einen LDAP-Server/ActiveDirectory administrieren mit Zend_Ldap

with 2 comments

Ein Directory-Server zur zentralen Administration von Benutzeraccounts, Adressbüchern und anderen Resourcen aller Art ist eine feine Sache, sobald man mehrere Rechner, Dienste und/oder Benutzer zu administrieren hat. Nicht nur im Firmenumfeld, auch zu Hause haben IT-Spezies mehr und mehr einen LDAP (Leightweight Directory Access Protocol)-kompatiblen Server, der die genannten Informationen zentral zur Verfügung stellt.

Ohne hier detailliert auf die Vorteile eingehen zu wollen, kann man zum Beispiel viele Dienste mit den zentralen Benutzeraccounts auf dem LDAP-Server verbinden (evtl. mittels Samba, aber das soll hier nicht Thema sein), und so braucht sich ein Benutzer nur ein Passwort merken und dieses regelmäßig ändern anstatt für jeden Dienst einen eigenen Account zu haben. Ein Benutzer kann sich dann beispielsweise an jedem Rechner der Firma anmelden, sein Email-Kennwort ist das selbe, der FTP-Zugang ist auch automatisch verfügbar mit dem selben Kennwort, es können Rechte zentral verwaltet werden, und im Falle des Ausscheidens des Mitarbeiters muss man nur einen Account löschen und er hat nirgends mehr Zugriff. Natürlich können auch alle Intranet-Webseiten dann diese Login-Informationen nutzen, sodass nicht auf jeder Intranetseite ein separater Account angelegt und verwaltet werden muss (Stichwort Zend_Auth_Adapter_Ldap). Soweit die Theorie.

Auch ist es möglich, sein Adressbuch auf einem LDAP-Server zu speichern und dieses dann von überall nutzen zu können: via Webapplikation, Thunderbird, Outlook, Mail.app, wohlmöglich ein Software-IP-Telefon und und und. Das geht zum Beispiel nicht mit einer Datenbank.

Es gibt im Großen und Ganzen 2 bekannte Produkte auf dem Markt: OpenLDAP und ActiveDirectory von Microsoft. Beide Dienste bieten die oben genannten angerissenen Möglichkeiten und lassen sich via LDAP abfragen und manipulieren.

Nun zu PHP und Zend_Ldap: Wir können uns also zum LDAP-Dienst verbinden und dort neue Objekte anlegen, diese editieren, löschen, verschieben und vieles mehr. Im Folgenden zeige ich Euch am Beispiel von einem OpenLDAP-Server, wie man das sehr einfach machen kann.

Noch einige Grundlagen, damit der Code auch verständlicher ist:

  • Wurzel: Die Wurzel eines LDAP-Servers ist häufig eine Organisation (o) oder eine Domain (wird dann via dc benannt)
  • ou: Organisational Unit, wird zur Strukturierung (vgl. „Ordner“) genutzt, sodass sich ein Baum bildet
  • dn: Distinguished Name, das ist der vollständige Pfad zu einem Objekt
  • objectclass: Ein Objekt gehört mindestens einer (strukturellen) Klasse an, die die Attribute bestimmt und soetwas wie eine Schablone ist
  • … weitere Informationen finden sich im entsprechenden LDAP-Wikipedia Eintrag.

Die Baumstruktur sieht dann beispielsweise so aus:

PHPLdapAdmin

Ein Objekt in diesem Baum sieht dann zum Beispiel so aus:

Bild von Wikipedia

Und so greifen wir dann darauf zu:

$options = array(
    'host'              => '10.12.13.14',
    'username'          => 'cn=michael,ou=Users,dc=test,dc=de',
    'password'          => '123456',
    'bindRequiresDn'    => true,
    'accountDomainName' => 'test.de',
    'baseDn'            => 'dc=test,dc=de',
);

$ldap = new Zend_Ldap($options);
$ldap->bind();

Wir übergeben dem Konstruktor also die nötigen Daten und authentifizieren uns (bind()). Danach können wir, wenn die Authentifizierung geklappt hat und wir die entsprechenden Rechte in den OUs haben, Daten abfragen, zum Beispiel ein bestimmtes Objekt holen:

$user1 = $ldap->getEntry('cn=user1,ou=michael,ou=Addressbook,dc=test,dc=de');
/* array(
 *    'dn'          => 'cn=user1,ou=michael,ou=Addressbook,dc=test,dc=de',
 *    'cn'          => array('user1'),
 *    'givenname'   => array('Klaus'),
 *   ....
 */

Ein Objekt verändern ist auch sehr einfach:

$user1 = $ldap->getEntry('cn=user1,ou=michael,ou=Addressbook,dc=test,dc=de');
Zend_Ldap_Attribute::setAttribute($user1, 'displayName', 'User Eins');
$ldap->update('cn=user1,ou=michael,ou=Addressbook,dc=test,dc=de', $user1);

Ein neues Objekt fügen wir folgendermaßen hinzu:

$user2 = array();
Zend_Ldap_Attribute::setAttribute($user2, 'cn', 'UserZwei');
Zend_Ldap_Attribute::setAttribute($user2, 'objectClass', 'inetorgPerson');
$ldap->add('cn=UserZwei,ou=michael,ou=Addressbook,dc=test,dc=de', $user2);

Ein Objekt löschen könnte einfacher nicht sein:

$ldap->delete('cn=UserZwei,ou=michael,ou=Addressbook,dc=test,dc=de');

Suchen können wir natürlich auch nach verschiedenen Kriterien, beispielsweise so:

$filter1  = Zend_Ldap_Filter::begins('cn', 			$searchString);
$filter2  = Zend_Ldap_Filter::begins('sn', 			$searchString);
$filter3  = Zend_Ldap_Filter::begins('gn', 			$searchString);
$filter4  = Zend_Ldap_Filter::contains('displayname', 	$searchString);
$filter5  = Zend_Ldap_Filter::contains('mail', 			$searchString);

$filter = Zend_Ldap_Filter::orFilter($filter1, $filter2, $filter3, $filter4, $filter5);
$results1 = $ldap->searchEntries($filter);
$results2 = $ldap->search($filter);

search() und searchEntries() unterscheiden sich nur im Rückgabewert. Einmal erhalten wir ein Zend_Ldap_Collection Objekt und einmal ein einfaches Array mit den Ergebnissen.

Damit sollten die oft gebrauchten Anwendungsfälle abgedeckt sein. Weitere Informationen gibt es natürlich im Manual des Zend Frameworks zu Zend_Ldap.

Written by Michael Kliewe

Januar 18th, 2010 at 5:02 pm

Posted in PHP

Tagged with , , ,

LDAP Authentifizierung und andere Abfragen

with 6 comments

Zuhause beim Programmieren oder bei der Erstellung von einfachen Webseiten wird man mit LDAP wahrscheinlich nicht viel zu tun haben, doch wenn man häufiger Intranet-Projekte erstellt oder in irgendeiner Weise den Login an ein OpenLDAP-Verzeichnis oder ActiveDirectory knüpfen will, kommt man an LDAP kaum vorbei.

Grundsätzliche Informationen über LDAP und ActiveDirectory findet man natürlich bei Wikipedia. Ich möchte hier nicht 5 Seiten darüber schreiben, wofür soetwas gut ist und warum man sowas haben sollte, da könnt ihr euch am besten selbst die Informationen sammeln. Stichworte sind wie gesagt LDAP, ActiveDirectory, OpenLDAP und für die hart gesottenen die RFC 2307.

Im Firmenumfeld werden Administrationsseiten (von denen es in größeren Firmen schnell Unmengen gibt) natürlich auch geschützt, sodass nur die berechtigten Personen darauf zugreifen können. Da man nicht für jede Seite ein eigenes Passwort etc. verwenden möchte, nutzt man das vorhandene ActiveDirectory, um Zugriffe auf die Seiten zu vergeben. Außerdem ist es damit einfacher, bei Ausscheiden eines Mitarbeiters zentral an einer Stelle den Account zu löschen.

Nur wie macht man das? Wie gestatte ich einem ActiveDirectory-User den Zugriff, wohingegen ich anderen den Zugriff verweigere?

Es gibt mehrere Lösungen. Zum einen bietet der Webserver da Möglichkeiten. Beim IIS zum Beispiel kann man bei der Konfiguration der Website entweder anonymen Zugriff erlauben (d.h. jeder Besucher kann sie betreten), oder man aktiviert eine Passwortabfrage, wie zB:

ldap1

Falls man dann die entsprechende Webseite betreten möchte, kommt eine Abfrage:

ldap2

Damit ist schonmal sichergestellt, dass man nur mit einen gültigen Account Zutritt erlangt. Doch wie bestimme ich nun, dass nur eine Untermenge aller Accounts zugelassen werden soll? Im Falle des IIS macht man das mit Hilfe der NTFS-Berechtigungen. Wenn man auf das DocumentRoot nur denjenigen Leserechte gibt, die auch die Seite betreten können sollen, haben wir genau das, was wir wollen.

Auch der Apache kann natürlich etwas vergleichbares. Dazu benötigt man das Modul mod_auth_ldap. Hier eine einfache Beispielkonfiguration:

<Location /example-repository>
    # LDAP soll für die Authentifizierung zuständig sein.
    AuthLDAPAuthoritative on

    AuthType Basic
    AuthName "Mein geschütztes Verzeichnis"

    # Wenn anonyme Zugriffe auf nicht erlaubt sind müssen sie hier
    # den DN für einen Benutzer angeben, der für den Lesezugriff
    # verwendet werden kann.
    AuthLDAPBindDN "CN=browse_user,OU=FunktionaleUser,DC=example,DC=com"
    # Das Passwort für den „Browse User“
    AuthLDAPBindPassword sicheres_passwort

    # Die LDAP URL für die Verbindung.
    # Alle Verzeichnisse unterhalb der angegebenen „Bind URL“ werden
    # durchsucht. Das Feld „login“ wird für die Suche nach dem Benutzernamen verwendet.
    # Format: scheme://host:port/basedn?attribute?scope?filter
    AuthLDAPURL "ldap://ldap.example.com:389/DC=example,DC=com?login?sub?(objectClass=*)"

    # Natürlich ist auch eine gesicherte Verbindung möglich. Beispiel:
    # "ldaps://ldap.example.com:636/DC=example,DC=com?login?sub?(objectClass=*)"

    Require valid-user
</Location>

Weitere Informationen dazu gibt es natürlich auf der entsprechenden Webseite zu mod_auth_ldap.

Dies sind also Methoden, die die Webserver allein regeln, PHP bekommt davon garnichts mit. Falls die Authentifizierung erfolgreich war (Der Login ist korrekt und dieser User hat Leserechte), kann man in PHP mittels der Variablen $_SERVER[‚AUTH_USER‘] den Benutzernamen herausfinden.

Um noch flexibler zu sein, kann man diese Authentifizierung natürlich auch direkt in PHP erledigen. Dazu bietet php einige ldap_* Funktionen, die es uns ermöglichen, via LDAP das Verzeichnis zu durchsuchen und Informationen daraus auszulesen. Außerdem kann man sich dann ein formschönes Login-Formular basteln, und hat nicht so einen grauen Kasten.

Um sich mit einem LDAP-Server verbinden zu können, benötigt man die Serverdaten, die Protokollversion und natürlich einen Login für den LDAP-Server, das ist der Bind-User. Das sieht dann ungefähr so aus:

function setupLdapConnection() {
	// get ldap connection to domainX
	$ldapOptions = array (
				'binddn'    => 'cn=ldapsearch,ou=serviceuser,DC=domainX,DC=net',
				'bindpw'    => 'ldapsearchpwd',
				'basedn'    => 'DC=domainX,DC=net',
				'host'      => 'ldap.domainX.net'    );
	
	$ldap = ldap_connect($ldapOptions['host']);
	if ($ldap == false) {
		throw new Exception('LDAP connnect failed');
	}
	
	ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
	ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
	
	$res = ldap_bind($ldap, $ldapOptions['binddn'], $ldapOptions['bindpw']);
	if ($res === false) {
		throw new Exception('Unable to bind to LDAP server');
	}
	
	return $ldap;
}

Damit haben wir nun eine LDAP-Connection aufgebaut, und dann kann man anfangen zu Suchen, beispielsweise so:

$ldap = setupLdapConnection();

$baseDn = 'DC=domainX,DC=net';
$attributes = array('cn', 'mail', 'objectClass', 'sAMAccountName', 'extensionAttribute13');
$filter = 'cn=User123';

$res = ldap_search($ldap, $baseDn, $filter, $attributes);

if ($res === false) {
	return "LDAP search failed\r\n";
}

if (ldap_count_entries($ldap, $res) === 0) {
	return "LDAP search failed, no entries found\r\n";
}

$entry = ldap_get_entries($ldap, $res);
if ($entry === false) {
	return "LDAP get entry failed\r\n";
}

return $res;

In $res haben wir dann das Ergebnis-Array mit den gewünschten Informationen. Man kann natürlich auch mit Hilfe der $baseDn nur in einem bestimmten Zweig suchen, oder mit Hilfe des $filter nach anderen Attributen suchen.

Mit diesen Funktionen kann man dann auch zB prüfen, ob ein User Mitglied einer Gruppe ist (Attribut memberOf), oder seine Email-Adresse herausfinden oder oder…

Zur Authentifizierung selbst gibt es natürlich auch schon fertige Klassen. Zu nennen sind da wohl PEAR_Auth und Zend_Auth_Adapter_Ldap. Hier ein schönes kurzes Beispiel mit dem ZF:

application.ini:

ldap.server1.host = ldap.domainX.net
ldap.server1.useSsl = false

ldap.server1.accountDomainName = domainX.net
ldap.server1.accountDomainNameShort = domainX
ldap.server1.accountCanonicalForm = 3
ldap.server1.accountFilterFormat = "(&(objectClass=user)(sAMAccountName=%s))"

ldap.server1.username = "cn=ldapsearch,ou=serviceuser,DC=domainX,DC=net"
ldap.server1.password = ldapsearchpwd
ldap.server1.baseDn = "DC=domainX,DC=net"
ldap.server1.bindRequiresDn = true

IndexController:

public function loginAction() {
	$form = new forms_LoginForm();

    if ($this->getRequest()->isPost()) {
    	$formData = $this->getRequest()->getPost();
        if ($form->isValid($formData)) {
			$values = $form->getValues();
				
			$loginSuccessful = false;
			$auth = Zend_Auth::getInstance();
			
			$options = Zend_Registry::get('configIni')->ldap->toArray();
			
			$adapter = new Zend_Auth_Adapter_Ldap($options, 
												$values['loginusername'], 
												$values['loginpassword']);
			$result = $auth->authenticate($adapter);

			if ($result->isValid()) {					
				$namespace = new Zend_Session_Namespace();
				$namespace->username = $values['loginusername'];				
			} else {
				$this->_flashMessenger->addMessage('error:login_failed_ldap');					
			}
			$this->_redirect("");				
		} else {
			$form->populate($formData);
		}
               
    }
    $this->view->form = $form;
}

Ist natürlich alles auf das Wesentliche gekürzt, man hat normalerweise natürlich noch diverse Sicherheitsabfragen oder zB einen try-catch-Block um das authenticate() etc.

Nutzt ihr auch LDAP, und wenn ja wie?

Written by Michael Kliewe

August 7th, 2009 at 4:06 pm