Hilfsmittel zur Windows-Administration

Ein sehr leistungsfähiges System zur Administration von Windows-Netzwerken ist das seit Windows Server 2000 von Microsoft bereitgestellte Active Directory System (ADS). Es ermöglich, die Verwaltung von Benutzern, Computern und anderen Ressourcen über eine zentrale Datenbank vorzunehmen. Diese ist auf den sog. Domain-Controllern (Windows Servern mit zusätzlichen Funktionen) installiert. Microsoft stellt sowohl mit den Server-Betriebssystemen (Windows 2003 und 2008) als auch im Resource-Kit eine ganze Reihe von Werkzeugen zur Verwaltung des Active-Directory zur Verfügung.

Trotzdem bleiben manche Wünsche zur komfortablen Administration bestimmter Aufgaben unerfüllt. Obwohl die originäre Unterstützung von Microsoft sich auf C++ und Visual Basic beschränkt, bieten sich hier auch dem ambitionierten Delphi-Programmierer Möglichkeiten für eigene Softwareentwicklungen. Die benötigten Informationen für das ADS-Interface (ADSI) findet man im Windows-SDK. Um das ADSI mit Delphi zu verwenden benötigt man außerdem eine Interface-Unit zu den Windows-Schnittstellen. Diese und weitere interessante Informationen zu den Active Directory Service Interfaces findet man auf der Webseite von Agnisoft. Dort werden auch Beispielprogramme und die benötigten Delphi-Interface-Units bereitgestellt.
Bei der Verwendung dieser Units mit den aktuellen Windows-Serverbetriebssystemen (z.B. 2008 R2) hat sich allerdings gezeigt, dass diese aus dem Jahr 2000 stammende Units noch einige Fehler enthalten. Das führt manchmal zu Programmabstürzen, da Teile des Programmcodes durch Daten überschrieben werden. Es gibt inzwischen im Jedi-Projekt aber neue Units dafür, in denen an verschiedenen Stellen die Aufrufe der Bibliotheksroutinen für das Windows-ADSI korrigiert wurden.

Nachfolgend soll anhand von einigen Beispielprogrammen näher erläutert werden, welche Möglichkeiten sich hier dem Windows-Administrator bieten. Sie verwenden die oben beschriebenen neuen Jedi-Units und funktionieren auch unter Windows Server 2008R2 einwandfrei. Die Programme werden sowohl als ausführbare Dateien als auch im Quelltext bereitgestellt.

Hinweis: Wegen der Benutzerkontensteuerung auf den neueren Betriebssystemen ist es erforderlich, die nachfolgenden Programme explizit als Administrator zu starten. Hierfür ist in die jew. Ressourcen der Programme folgender Manifestabschnitt eingebaut:

  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
   <security>
     <requestedPrivileges>
      <requestedExecutionLevel level="requireAdministrator"/>
     </requestedPrivileges>
   </security>
  </trustInfo>

Programme

Benutzer an der Domäne authenfizieren Domänenbenutzer anlegen  Kursbenutzer anlegen  Domänenbenutzer löschen

Downloads (Vers. Jul. 2010)

Ausführbare Programme (deutsch und englisch, 1,77 MB) Quelltexte (459 kB)

Authenfizierung eines Benutzers am ADS

Häufig möchte man z.B. bei Web-Anwendungen den Zugang nur bestimmten Benutzern ermöglichen. Dazu kann man eine eigene Benutzerverwaltung verwenden. Wenn jedoch bereits ein Windows Active Directory vorhanden ist, ist es viel bequemer, dies für die Authentifizierung des Benutzers zu verwenden.

Eine solche Authentifizierung in den für Web-Anwendungen benutzten Programmierumgebungen, wie z.B.PHP oder Perl zu realisieren, erfordert einige Vorkenntnisse. Viel einfacher ist es, das hier vorgestellte Konsolenprogramm AuthAds zu integrieren. Es wird mit den erforderlichen Parametern aufgerufen und liefert als ExitCode einen Wert, der Aufschluss darüber gibt, ob der Benutzer berechtigt ist. Dabei kann zusätzlich zur Authentifizierung auch noch die Zugehörigkeit zu einer Gruppe geprüft werden.

Aufruf des Programms

AuthAds <domain> <username> <password> [<group>]

Domain: Name der Windows-Domäne
Username: Name des Benutzers
Password: Kennwort des Benutzers
Group: Name der Gruppe, der der Benutzer angehören soll (optional)

Werden nur die ersten drei Parameter angegeben, wird geprüft, ob es sich um einen in der Domäne registrierten Benutzer handelt. Ist zusätzlich eine Gruppe genannt, wird auch noch geprüft, ob der Benutzer dieser Gruppe angehört. Durch Anlegen einer Gruppe im ADS kann so sehr einfach gesteuert werden, wer die jeweilige Web-Anwendung benutzen darf.

Rückmeldewerte des Programms (ExitCode)

0 : OK - Benutzer wurde authenfiziert und gehört, falls angegeben der genannten Gruppe an.
1 : falsche oder fehlende Parameter (z.B. -? für Hilfe).
2 : Die angegebene Domäne wurde nicht gefunden.
3 : Authenfizierung des Benutzers ist fehlgeschlagen.
4 : Der Benutzer wurde authentifiziert, gehört aber nicht der angegebenen Gruppen an.

Eintragen eines Domänen-Benutzers

Die in der ADS-Administration enthaltene Funktion zum Hinzufügen eines Domänenbenutzers bietet nur wenig Komfort bei der Eingabe. Alle Zusatzangaben müssen für jeden neuen Benutzer von Hand eingegeben. Für Eingabe der Passwörter, sowie die Zuordnung zu bestimmten Organisationseinheiten (OU) und Gruppen sind zusätzliche Schritte erforderlich.

Das nachfolgend beschriebene Delphi-Programmm erledigt dies alles in einem Schritt. Die innerhalb einer Domäne immer gleichbleibenden Angaben können voreingestellt werden.

Die Funktionen im Überblick:

Die wichtigsten Teile des Programms werden nachstehend näher erläutert
Verbinden mit der Domäne:
Diese und auch die nachfolgenden Abfragen basieren auf der LDAP-Syntax. Die Variable FDomain steht für den Namen der Domäne, mit der eine Verbindung hergestellt werden soll. Über die Funktion ADsGetObject wird ein Object root zurückgegeben, das es ermöglicht, weitere Informationen über die Domäne zu ermitteln, z.B. über Get('distinguishedName') den vollständigen Namen der Domäne (DC=aaa, DC=bbb, DC=ccc, DC=ddd,). Dieser kann dann mit der Funktion ToPrincipal in die übliche Nomenklatur (aaa.bbb.ccc.ddd) umgesetzt werden.
Anschließend wird ein weiteres Object DSearch vom Typ IDirectorySearch erzeugt, as für die im nächsten Schritt beschriebene Suche nach Organisationseinheiten und Gruppen benötigt wird. Zur Initialisierung der Suche müssen zunächst über SetSearchPreference die Einstellungen für die Suche vorgenommen werden.
function TfrmDomUser.InitDomainData : boolean;
  const
    szLen = 256;
  
  var
    bind         : widestring;
    opt          : ads_searchpref_info; 
    hr           : HResult;
    ptrResult    : THandle;
    col          : ads_search_column;
    dwErr        : DWord;
    szErr,szName : array[0..szLen-1] of WideCHar;
    sd,sn,sp     : string;
    root         : IADs;
    DSearch      : IDirectorySearch;
  
    function ToPrincipal(cs : string) : string;
    var
      i : integer;
      t : string;
    begin
      t:='';
      while length(cs)>0 do begin
        i:=pos('DC=',cs);
        if i>0 then begin
          delete(cs,1,3);
          if length(t)=0 then t:=ReadNxtStr(cs,',')
          else t:=t+'.'+ReadNxtStr(cs,',');
          end
        else cs:='';
        end;
      Result:=t;
      end;
  
  begin
    ...
    bind:='LDAP://'+FDomain;
    try
      ADsGetObject(PWideChar(Bind),IID_IADs,pointer(root));
      Context:=root.Get('distinguishedName');
      DNSName:=ToPrincipal(Context);
      edtDNSName.Text:='@'+DNSName;
      ...
      ADsGetObject(PWideChar(Bind),IDirectorySearch,DSearch);
      ...
      with opt do begin
        dwSearchPref:=ADS_SEARCHPREF_SEARCH_SCOPE;
        vValue.dwType:=ADSTYPE_INTEGER;
        vValue.Integer:=ADS_SCOPE_SUBTREE;
        end;
      with DSearch do begin
        if Failed(SetSearchPreference(@opt,1)) then begin
          ADsGetLastError(dwErr,@szErr,szLen,@szName[0],szLen);
          ShowMessage(WideCharToString(szErr)+sLineBreak+WideCharToString(szName));
          btnMakeUser.Enabled:=false;
          exit;
          end;
      ...
Abfragen der Organisationseinheiten:
ExecuteSearch ist eine Methodes des Objekts IDirectorySearch und wird hier mit dem Suchfilter organizationalUnit aufgerufen. In der While-Schleife werden nacheinander über die Variable col zunächst die Beschreibungen (Descriptions) der Organisationsheiten der Domäne abgefragt. Wenn eine gültige Beschreibung gefunden wurde, werden der zugehörige Name und der ADS-Pfad ermittelt. Alle Informationen werden dann in der Item-Liste der Combo-Box cbxOU als Objekte gespeichert.
Beim Anlegen eines neuen Benutzers kann aus dieser Liste die zugehörige Organisationseinheit ausgewählt werden.
  type
    TADsObject = class (TObject)
      FName,FDesc,FPath : string;
      constructor Create (AName,ADesc,Apath : string);
      end;
  const
    ColCount = 3;
    ColNames : array[0..ColCount-1] of WideString = ('Name','Description','ADsPath');
    ...
  function TfrmDomUser.InitDomainData : boolean;
    ...
      // Organisationseinheiten suchen
      cbxOU.Clear;
      ExecuteSearch('(objectClass=organizationalUnit)',@ColNames,ColCount,ptrResult);
      hr:=GetNextRow(ptrResult);
      while (hr<>S_ADS_NOMORE_ROWS) do begin
        sd:=''; sd:=''; sp:='';
        if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[1]),col)) then begin
          with col do if pADsValues<>nil then sd:=pAdsvalues^.CaseExactString;
          FreeColumn(@col);
          if (length(sd)>0) and (sd[1]='-') then sd:='';
          end;
        if length(sd)>0 then begin
          if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[0]),col)) then begin
            with col do if pADsValues<>nil then sn:=pAdsvalues^.CaseIgnoreString;
            FreeColumn(@col);
            end;
          if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[2]),col)) then begin
            with col do if pADsValues<>nil then sp:=pAdsvalues^.CaseIgnoreString;
            FreeColumn(@col);
            end;
          if length(sp)>0 then cbxOU.Items.AddObject(sd,TADsObject.Create(sn,sd,sp));
          end;
        hr:=GetNextRow(ptrResult);
        end;
      ...
Abfragen der Gruppen:
Die Abfrage der Domänen-Gruppen erfolgt ganz ähnlich. Bei der Suchanfrage werden alle vorgegebenen Gruppen (vom Typ Builtin) ausgeschlossen. Das Suchergebnis wird in einer Listbox lbxAg gespeichert.
Beim Anlegen eines neuen Benutzers können aus dieser Liste die Gruppen ausgewählt werden, denen der Benutzer angehören soll.
      // Gruppen suchen
      lbxAg.Clear;
      ExecuteSearch('(&(objectClass=group)(!CN=Builtin))',@ColNames,ColCount-1,ptrResult);
      hr:=GetNextRow(ptrResult);
      while (hr<>S_ADS_NOMORE_ROWS) do begin
        sd:=''; sd:=''; sp:='';
        if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[1]),col)) then begin
          with col do if pADsValues<>nil then sd:=pAdsvalues^.CaseExactString;
          FreeColumn(@col);
          end;
        if length(sd)>0 then begin
          if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[0]),col)) then begin
            with col do if pADsValues<>nil then sn:=pAdsvalues^.CaseIgnoreString;
            FreeColumn(@col);
            end;
          if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[2]),col)) then begin
            with col do if pADsValues<>nil then sp:=pAdsvalues^.CaseIgnoreString;
            FreeColumn(@col);
            end;
          if length(sp)>0 then lbxAg.Items.AddObject(sd,TADsObject.Create(sn,sd,sp));
          end;
        hr:=GetNextRow(ptrResult);
        end;
    ...
Prüfen, ob der Benutzer bereits eingetragen ist:
Bevor der neue Benutzer eingetragen werden kann, muss geprüft werden, ob es bereits einen mit diesem Namen gibt. Dazu wird eine Suchanfrage mit SearchUser (s.u.) gestartet. Gibt es den Benutzer schon, werden nur die zugehörigen Einträge im ADS angezeigt. Im anderen Fall kann ein neuer Benutzer mit den gemachten Vorgaben engelegt werden.
  procedure TfrmDomUser.btnMakeUserClick(Sender: TObject);
  var
    ADsCont  : IADsContainer;
    ADsOU    : IADsOU;
    User     : IDispatch;
    Grp      : IAdsGroup;
    bind     : widestring;
    ap,s     : string;
    i,n      : integer;

  const
    SpecChars : set of char = [',','/',';'];
  
    function ReplaceSpecChar(s : string) : string;
    var
      t : string;
      i : integer;
    begin
      t:='';
      for i:=1 to length(s) do if (s[i] in SpecChars) then t:=t+'\'+s[i] else t:=t+s[i];
      Result:=t;
      end;
  
  begin
    if CheckHomeDir and (length(edtKontoName.Text)>0) then begin
      if rbtGlobal.Checked then bind:='LDAP://'+FDomain+'/CN=Users,'+Context
      else with cbxOU do bind:=(Items.Objects[ItemIndex] as TAdsObject).FPath;
    // prüfen, ob der Benutzer bereits eingetragen ist
      ap:=SearchUser (edtKontoName.text);
      if length(ap)>0 then begin
        ADsGetObject(PWideChar(ap),IID_IADsUser,pointer(user));
        with User as IADsUser  do begin
          GetInfo;
          try
            edtFullName.Text:=FullName;
          except
            edtFullName.Text:='';
            end;
          ErrorDialog(Caption,'Der Benutzer '+edtFullname.Text+' ('+edtKontoName.Text+'@'+DNSName+')'+
                     sLineBreak+'ist bereits eingetragen!');
          ...
        end
      else if ConfirmDialog (Caption,'Neuen Benutzer: '+edtKontoName.Text+edtDNSName.Text+
                       sLineBreak+'unter: '+bind+' anlegen?') then begin // noch nicht vorhanden
        ADsGetObject(PWideChar(bind),IID_IADsContainer,pointer(ADsCont));
        User:=ADsCont.Create('user','CN='+ReplaceSpecChar(edtFullname.Text));
        ...
        end;
      end;  
    ADsCont:=nil;
    Pwd:=''; btnPwd.Font.Color:=clRed;
    end;
Nach einem Benutzer suchen:
Die nachfolgende Routine sucht nach dem Anmeldenamen (sAMAccountName) eines Benutzers und liefert diesen als String zurück.
  function TfrmDomUser.SearchUser (CommonName : string) : string;
  var
    DSearch      : IDirectorySearch;
    opt          : ads_searchpref_info; 
    ptrResult    : THandle;
    col          : ads_search_column;
  begin
    Result:='';
    ADsGetObject(PWideChar('LDAP://'+FDomain),IID_IDirectorySearch,pointer(DSearch));
    with opt do begin
      dwSearchPref:=ADS_SEARCHPREF_SEARCH_SCOPE;
      vValue.dwType:=ADSTYPE_INTEGER;
      vValue.Integer:=ADS_SCOPE_SUBTREE;
      end;
    with DSearch do begin
      if Succeeded(SetSearchPreference(@opt,1)) then begin
        ExecuteSearch(PWideChar('(&(objectClass=user)(sAMAccountName='+CommonName+'))'),@ColNames,ColCount,ptrResult);
        if GetNextRow(ptrResult)<>S_ADS_NOMORE_ROWS then begin
          if Succeeded(GetColumn(ptrResult,PWideChar(ColNames[2]),col)) then begin
            with col do if pADsValues<>nil then Result:=pAdsvalues^.CaseIgnoreString;
            FreeColumn(@col);
            end;
          end
        end
      end;
    end;
Anzeige eines bereits eingetragenen Benutzers:
Die Benutzerangaben werden über die Methode GetInfo abgeholt. Bei der Abfrage der möglichen Eigenschaften (siehe dazu Windows-SDK) kommt es zu einem Fehler, wenn eine Eigenschaft nicht verfügbar ist. Daher muss jedesmal das Konstrukt try ... except .. end; verwendet werden.
      ...
      ADsGetObject(PWideChar(ap),IID_IADsUser,pointer(user));
      with User as IADsUser  do begin
        GetInfo;
        try
          edtFullName.Text:=FullName;
        except
          edtFullName.Text:='';
          end;
        ErrorDialog(Caption,'Der Benutzer '+edtFullname.Text+' ('+edtKontoName.Text+'@'+DNSName+')'+
                   sLineBreak+'ist bereits eingetragen!');
        try
          edtFirstName.Text:=FirstName;
        except
          edtFirstName.Text:='';
          end;
        try
          edtLastName.Text:=LastName;
        except
          edtLastName.Text:='';
          end;
        try
          edtDescription.Text:=Description;
        except
          edtDescription.Text:='';
          end;
        try
          edtKontoName.Text:=Get('sAMAccountName');
        except
          edtKontoName.Text:='';
          end;
        try
          edtRoom.Text:=OfficeLocations;
        except
          edtRoom.Text:='';
          end;
        try
          edtPhone.Text:=TelephoneNumber;
        except
          edtPhone.Text:='';
          end;
        try
          chbDisabled.Checked:=AccountDisabled;
        except
          chbDisabled.State:=cbGrayed;
          end;
        try
          s:=Profile;
          with hcbProfile do begin
            Text:=s;
            AddItem(ReplaceUsername(s));
            end;
        except
          end;
        try
          s:=HomeDirectory;
          with hcbHomeDir do begin
            Text:=s;
            AddItem(ReplaceUsername(s));
            end;
        except
          end;
        try
          s:=Get('homeDrive');
          with cbHomeDrive do begin
            n:=Items.IndexOf(s);
            if n<0 then n:=0;
            ItemIndex:=n;
            end;
        except
          end;
        try
          ADsGetObject(PWideChar(Parent),IID_IADsOU,pointer(ADsOU));
          rbtOU.Checked:=true;
          with cbxOU do ItemIndex:=Items.IndexOf(ADsOU.Description);
        except
          rbtGlobal.Checked:=true;
          cbxOU.ItemIndex:=-1;
          end;
        ADsOU:=nil;
        end;
      ...
Anlegen eines neuen Benutzers:
Zunächst muss ein ADS-Context über ADsGetObject geholt werden, mit dem dann der neue Domänen-Benutzer über ADsCont.Create angelegt werden kann. Anschließend werden die Eigenschaften des benutzers gesetzt. Für diejenigen, für die keine explizite Eigenschaft vorgesehen (siehe Windows-SDK) ist, muss die Funktion Put verwendet werden (z.B. Put('sAMAccountName',...)). Zum Abschluss werden alle Information mit SetInfo im ADS gespeichert.
Anschließend wird dann noch die Mitgliedschaft in den Gruppen eingetragen (Grp.Add):
      ...
        ADsGetObject(PWideChar(bind),IID_IADsContainer,pointer(ADsCont));
        User:=ADsCont.Create('user','CN='+ReplaceSpecChar(edtFullname.Text));
        with User as IADsUser do begin
      // set Mandatory attributes
          Put('sAMAccountName',edtKontoName.Text);
      // set Optional attributes
          FullName:=edtFullName.Text;
          if length(edtFirstName.Text)>0 then FirstName:=edtFirstName.Text;
          LastName:=edtLastName.Text;
          Description:=edtDescription.Text;
          with hcbHomeDir do if length(Text)>0 then HomeDirectory:=ReplacePlaceholder(Text);
          with cbHomeDrive do if ItemIndex>0 then Put('homeDrive',Items[ItemIndex]);
          with hcbProfile do if length(Text)>0 then Profile:=ReplacePlaceholder(Text);
          Put('userPrincipalName',edtKontoname.Text+'@'+DNSName);
          try
            SetInfo;       // Speichern
          except
            on E:EOleException do ShowExceptionError(E);
            end;
      // additional attributes
          if length(Pwd)=0 then begin
            if not chbDisabled.Checked then begin  // Passwort
              Put('pwdLastSet',0);   // ändern bei nächster Anmeldung
              s:=StringReplace(DefPwd,'#',edtKontoname.Text,[]);
              try
                SetPassword (s);
              except
                on E:EOleException do ShowExceptionError(E);
                end;
              end;
            Put('userAccountControl',0);
            end
          else begin
            try
              SetPassword (Pwd);
            except
              on E:EOleException do ShowExceptionError(E);
              end;
            if NoExpire then
              Put('userAccountControl',ADS_UF_DONT_EXPIRE_PASSWD)
            else Put('userAccountControl',0);
            end;
          AccountDisabled:=chbDisabled.Checked;
          if length(edtRoom.Text)>0 then OfficeLocations:=edtRoom.Text;
          if length(edtPhone.Text)>0 then TelephoneNumber:=edtPhone.Text;
          EmailAddress:=edtKontoname.Text+'@'+DNSName;
          try
            SetInfo;       // Speichern
          except
            on E:EOleException do ShowExceptionError(E);
            end;
          end;
      // Gruppen zuordnen
        with lbxAG do if SelCount>0 then for i:=0 to Items.Count-1 do if Selected[i] then begin
          if succeeded(ADsGetObject(PWideChar((Items.Objects[i] as TADsObject).FPath),IID_IADsGroup,pointer(Grp))) then
            Grp.Add((user as IAdsUser).AdsPath);
          end;
        Grp:=nil;
        ...
        MessageDlg('In '+FDomain+' wurde ein neuer Benutzer angelegt:'+sLineBreak
                     +edtFullname.Text+'('+edtKontoname.Text+'@'+DNSName+')',
                     mtInformation,[mbOK],0);
        user:=nil;
        end;
      ADsCont:=nil;
      ...
      end
    ...

Eintragen von mehreren Kurs-Benutzern

An Schulen und Universitäten kommt es sehr häufig vor, dass für bestimmte Kurse mehrere Benutzer Zugriff zu den PCs eines Lernraums benötigen. Der administrative Aufwand hierfür gestaltet sich besonders gering, wenn diese PCs in eine Windows-Domäne integriert sind. Es sind dann lediglich einige neue Dömänenbenutzer anzulegen. Durch Zuordnung zu einer geeigneten Organisitionseinheit erhalten sie automatisch die passenden Gruppenrichlinien und durch Aufnahme in eine passende Gruppe die benötigten Zugriffsrechte.
Das nachfolgend beschriebene Programm automatisiert diesen Vorgang, indem es eine beliebig vorgebbare Anzahl von Benutzern (z.B. user01 .. user09) anlegt und diese einer auswählbaren Organisationseinheit und einer oder mehreren Gruppen zuordnet. Die Laufzeit des Kontos kann außerdem zeitlich begrenzt werden (z.B. auf ein Semester). Passwörter für die Konten werden automatisch erzeugt. Alle Benutzerinformation können außerdem in Etikettenform ausgedruckt werden, um die benötigten Anmeldeinformation an den jeweiligen Kursbenutzer weitergeben zu können.
Außerdem ist es möglich vorhandene Konten mit neuen Passwörtern zu versehen.

Besonderheiten des Programms
Setzen der Eigenschaft: Benutzer kann Passwort nicht ändern
Diese Eigenschaft kann nicht mit den oben beschriebenen Benutzerinfos gesetzt werden. Der Mechanismus ist wesentlich komplizierter und erfordert einen Zugriff auf die Access Control Lists.
      // Set "User Cannot Change Password"
      // see sample in "Windows Platform SDK"
      IU:=(User as IADsUser).Get('ntSecurityDescriptor');
      IU.QueryInterface(IID_IADsSecurityDescriptor,SecDesc);
      ACL:=IADsAccessControlList(SecDesc.DiscretionaryAcl);
      IU:=ACL._NewEnum;
      if succeeded(IU.QueryInterface(IID_IEnumVARIANT,Enum)) then begin
        while (Succeeded(ADsEnumerateNext(Enum,1,VarArr,lNumEl)))and (lNumEl>0) do begin
          if Succeeded(IDispatch(varArr).QueryInterface(IID_IADsAccessControlEntry,ACE)) then with ACE do begin
            if UpperCase(ObjectType)=UpperCase(CHANGE_PASSWORD_GUID) then begin
            // nachfolgend sind die länderspez. Namen zu benutzen:
            // z.B. "Jeder" und "NT-AUTORITÄT\SELBST"
            // Die Ermittlung dieser Namen erfolgt über die Funktion
            // GetAccountName in WinApi (s.o.)
              if Trustee=TrEvrOne then begin  // 'Everyone'
                // Modify the ace type of the entry.
                if UserCannotChangePassword then AceType:=ADS_ACETYPE_ACCESS_DENIED_OBJECT
                else AceType:=ADS_ACETYPE_ACCESS_ALLOWED_OBJECT;
                end;
            // TrSelf (s.o.) enthält nicht den Teil "NT AUTHORITY\" (bzw. "NT-AUTORITÄT\")
            // Ich habe keine Infos gefunden, wie das anders zu machen geht
            // Da die Strings so nicht mit Trustee verglichen werden können,
            // wird der Teil vor "\" entfernt
              j:=AnsiPos('\',Trustee);
              if j>0 then begin
                s:=AnsiRightStr(Trustee,length(Trustee)-j);
                if s=TrSelf then begin // 'NT AUTHORITY\SELF'
                  // Modify the ace type of the entry.
                  if UserCannotChangePassword then AceType:=ADS_ACETYPE_ACCESS_DENIED_OBJECT
                  else AceType:=ADS_ACETYPE_ACCESS_ALLOWED_OBJECT;
                  end;
                end;
              end;
            end;
          end;
        // Update the ntSecurityDescriptor property.
        (User as IADsUser).Put ('ntSecurityDescriptor',SecDesc);
        //Commit the changes to the server.
        (User as IADsUser).SetInfo;
        end;
    ...

Domänenbenutzer und zugehörige persönliche Daten löschen

Wenn ein Domänenbenutzer aus dem Active Directory entfernt werden soll, ist es meist auch erforderlich die dazu gehörigen persönlichen Daten zu löschen. Sind diese in einem Verzeichnis abgelegt, das dem Anmeldenamen des Benutzers entspricht kann das vorliegenden Programm diese automatisch in einem einstellbaren Bereich (z.B. einer Freigabe auf einem Server) finden.
Das Programm liest beim Start eine Liste der aktuellen Domänenbenutzer ein. Der Administrator wählt den zu entfernenden Benutzer aus und stellt ein, ob auch die zugehörigen Daten gelöscht werden sollen. Auf Knopfdruck wird dies dann nach einer Bestätigungsabfrage ausgeführt.

Besonderheiten des Programms
Liste der Benutzer erstellen:
ExecuteSearch ist eine Methodes des Objekts IDirectorySearch und wird hier mit dem Suchfilter user, wobei computer und Builtin ausgeschlossen werden, aufgerufen. In der While-Schleife werden nacheinander über die Variable col die Namen der Benutzer (Name), die Kontonamen sAMAccountName und die ADS-Pfade ADsPath abgefragt.
  type
    TADsObject = class (TObject)
      FName,FAccount,FPath,FMail : string;
      FStatus : integer;
      constructor Create (AName,AAccount,APath,AMail: string; AStatus : integer);
      end;
  const
    OuCols = 'Name,Description,ADsPath';
    UserCols = 'Name,sAMAccountName,ADsPath,userAccountControl,mail';
    ...
  function TfrmDelUser.LoadUserList (const OU : string) : boolean;
  var
    hr           : HResult;
    ptrResult    : THandle;
    colnames     : TPWideArray;
    col          : ads_search_column;
    sd,sn,sp,sm  : string;
    ColCount,ns  : integer;
    ok           : boolean;
  begin
    try
      with DSearch do begin
        // Benutzer suchen
        UserList.Clear;
        StringToPwArray(UserCols,ColNames,ColCount);
        ExecuteSearch('(&(objectCategory=person)(objectClass=user))',@ColNames[0],ColCount,ptrResult);
        hr:=GetNextRow(ptrResult);
        while (hr<>S_ADS_NOMORE_ROWS) do begin
          sd:=''; sd:=''; sp:=''; sn:=''; sm:=''; ns:=4;
          if Succeeded(GetColumn(ptrResult,ColNames[1],col)) then begin
            with col do if pADsValues<>nil then sd:=pAdsvalues^.CaseExactString;
            FreeColumn(@col);
            if (length(sd)>0) and (sd[1]='-') then sd:='';
            end;
          if length(sd)>0 then begin
            if Succeeded(GetColumn(ptrResult,ColNames[0],col)) then begin
              with col do if pADsValues<>nil then sn:=pAdsvalues^.CaseIgnoreString;
              FreeColumn(@col);
              end;
            if Succeeded(GetColumn(ptrResult,ColNames[2],col)) then begin
              with col do if pADsValues<>nil then sp:=pAdsvalues^.CaseIgnoreString;
              FreeColumn(@col);
              end;
            if Succeeded(GetColumn(ptrResult,ColNames[3],col)) then begin
              with col do if pADsValues<>nil then begin
                if (pAdsvalues^.Integer and ADS_UF_ACCOUNTDISABLE)<>0 then ns:=1
                else ns:=0;
                end;
              FreeColumn(@col);
              end;
            if Succeeded(GetColumn(ptrResult,ColNames[4],col)) then begin
              with col do if pADsValues<>nil then sm:=pAdsvalues^.CaseIgnoreString;
              FreeColumn(@col);
              end;
            if length(OU)=0 then ok:=TextPos('ou',sp)=0
            else ok:=TextPos(OU,sp)>0;
            if (length(sn)>0) and ok then begin
              UserList.AddObject(sn,TADsObject.Create(sn,sd,sp,sm,ns));
              end;
            end;
          hr:=GetNextRow(ptrResult);
          end;
        lvUser.Items.Count:=UserList.Count;
        Result:=true;
        end;
    except
      Result:=false;
      end;
    end;
   ...
Eigenschaften des Benutzers anzeigen:
Beim Klick auf einen Benutzer in der Liste werden seine gespeicherten Eigenschaften angezeigt (siehe auch oben).
  procedure TfrmDelUser.ShowUserData (AIndex : integer);
  var
    ap    : string;
    ADsOU : IADsOU;
  begin
    if (AIndex>=0) and (AIndex<UserList.Count) then begin
      cbUserData.Checked:=false;
      lbUserDirs.Clear;
      ap:=(UserList.Objects[AIndex] as TADsObject).FPath;
      if Failed(ADsGetObject(PWideChar(ap),IID_IADsUser,pointer(user))) then Exit;
      with User as IADsUser  do begin
        GetInfo;
        try
          edtFullName.Text:=FullName;
        except
          edtFullName.Text:='';
          end;
      ...
      try
        ADsGetObject(PWideChar(Parent),IID_IADsOU,pointer(ADsOU));
        OUPath:=ADsOU.AdsPath;
      except
        OuPath:='';
        end;
      ADsOU:=nil;
      end;
    end;
Suche nach den Benutzerdaten:
Es wird nach einem Unterverzeichnis mit dem Namen des Benutzers gesucht. seLevel.Value legt die Anzahl der zu durchsuchenden Verzeichnisebenen fest.
  procedure TfrmDelUser.SearchDirs (Base,SubDir,UserName : string);
  var
    DirInfo    : TSearchRec;
    Findresult : integer;
    sd         : string;
  begin
    inc(DirLevel);
    if (length(SubDir)=0) then sd:=Base
    else if (SubDir[1]='\') then  sd:=Base+SubDir
    else sd:=IncludeTrailingPathDelimiter(Base)+SubDir;
    FindResult:=FindFirst(IncludeTrailingPathDelimiter(sd)+'*.*',faDirectory+faReadOnly+faHidden+faSysfile,DirInfo);
    while (FindResult=0) do with DirInfo do begin
      if NotSpecialDir(Name) then begin
        StatusWindow.Status:=IncludeTrailingPathDelimiter(SubDir)+DirInfo.Name;
        Application.ProcessMessages;
        if SameFileName(DirInfo.Name,UserName) then
          lbUserDirs.Items.Add(IncludeTrailingPathDelimiter(sd)+DirInfo.Name)
        else if DirLevel<seLevel.Value then
          SearchDirs(Base,IncludeTrailingPathDelimiter(SubDir)+DirInfo.Name,UserName);
        end;
      if StatusWindow.Stopped then begin
        FindClose(DirInfo);
        Exit;
        end;
      FindResult:=FindNext(DirInfo);
      end;
    FindClose(DirInfo);
    dec(DirLevel);
    end;
 ...
  procedure TfrmDelUser.btnDeleteClick(Sender: TObject);
  var
    s,bind    : string;
    n         : integer;
  begin
    with lbUserDirs do if cbUserData.Checked and (SelCount=0) then SelectAll;
    if cbUserData.Checked then
      s:=Format(_('Remove user %s from ADS'+sLineBreak+
                  'and delete selected user data in'+sLineBreak+'%s?'),
                  [edtFullName.Text,cbBaseDir.Text])
  //  'Benutzer %s aus ADS entfernen'+sLineBreak+'und ausgewählte Benutzerdaten auf'+sLineBreak+'%s löschen?'
    else s:=Format(_('Remove user %s from ADS'),[edtFullName.Text]); //'Benutzer %s aus ADS entfernen?';
    if ConfirmDialog(Caption,s,BottomLeftPos(btnSearch,Point(0,-150))) then begin
      if length(OUPath)>0 then bind:=OuPath
      else bind:='LDAP://'+FDomain+'/CN=Users,'+Context;
      n:=lvUser.ItemIndex;
      RemoveUser(bind);
      end;
    end;
    ...
Löschen des Benutzers und der Daten:
Zunächst wird der Benutzer gelöscht (ADsCont.Delete('user','CN='...), dann die zugehörigen Daten (DeleteDirectories).
  (* Lösche ein Verzeichnis einschließlich aller Unterverzeichnisse und Dateien *)
  procedure TfrmDelUser.DeleteDirectories (const Base,Dir    : string;
                                           var DCount,FCount : integer);
  var
    DirInfo    : TSearchRec;
    fc,dc,
    Findresult : integer;
    s,sd       : string;
  begin
    if length(Dir)>0 then sd:=IncludeTrailingPathDelimiter(Base)+Dir else sd:=Base;
    if DirectoryExists(sd) then begin
      StatusWindow.Status:=sd;
      FindResult:=FindFirst (Erweiter(sd,'*','*'),faAnyFile,DirInfo);
      while (FindResult=0) and not StatusWindow.Stopped do with DirInfo do begin
        if NotSpecialDir(Name) and ((Attr and faDirectory)<>0) then
          DeleteDirectories(Base,Erweiter(Dir,DirInfo.Name,''),DCount,FCount);
        FindResult:=FindNext (DirInfo);
        end;
      FindClose(DirInfo);
      if not StatusWindow.Stopped then begin
        fc:=0; dc:=0;
        FindResult:=FindFirst (Erweiter(sd,'*','*'),faArchive+faReadOnly+faHidden+faSysfile,DirInfo);
        while FindResult=0 do with DirInfo do begin
          if NotSpecialDir(Name) then begin
            inc(fc);
            (* Dateien löschen *)
            s:=SetDirName(sd)+Name;
            StatusWindow.Status:=s;
            Application.ProcessMessages;
            // immer löschen
            FileSetAttr(s,faArchive);
            if DeleteFile(s) then begin
              inc(FCount); inc(dc);
              end;
            if StatusWindow.Stopped then begin
              FindClose(DirInfo);
              Exit;
              end;
            end;
          FindResult:=FindNext (DirInfo);
          end;
        FindClose(DirInfo);
        if (fc=dc) then begin   // Verzeichnis leer ==> löschen
          FileSetAttr(sd,0);    // Attribute zum Löschen entfernen
          if RemoveDir(sd) then inc(DCount);
          end;
        end;
      end;
    end;
  ...
     
  procedure TfrmDelUser.btnDeleteClick(Sender: TObject);
  var
    s,bind    : string;
    n         : integer;
  begin
    with lbUserDirs do if cbUserData.Checked and (SelCount=0) then SelectAll;
    if cbUserData.Checked then
      s:=Format(_('Remove user %s from ADS'+sLineBreak+
                  'and delete selected user data in'+sLineBreak+'%s?'),
                  [edtFullName.Text,cbBaseDir.Text])
  //  'Benutzer %s aus ADS entfernen'+sLineBreak+'und ausgewählte Benutzerdaten auf'+sLineBreak+'%s löschen?'
    else s:=Format(_('Remove user %s from ADS'),[edtFullName.Text]); //'Benutzer %s aus ADS entfernen?';
    if ConfirmDialog(Caption,s,BottomLeftPos(btnSearch,Point(0,-150))) then begin
      if length(OUPath)>0 then bind:=OuPath
      else bind:='LDAP://'+FDomain+'/CN=Users,'+Context;
      n:=lvUser.ItemIndex;
      RemoveUser(bind);
      end;
    end;
  ...

  procedure TfrmDelUser.RemoveUser (const ABind : string);
  var
    s          : string;
    dc,fc,i,n  : integer;
    ADsCont    : IADsContainer;
  const
    SpecChars : set of char = [',','/',';'];
  
    function ReplaceSpecChar(s : string) : string;
    var
      t : string;
      i : integer;
    begin
      t:='';
      for i:=1 to length(s) do if (s[i] in SpecChars) then t:=t+'\'+s[i] else t:=t+s[i];
      Result:=t;
      end;
  
  begin
    ADsGetObject(PWideChar(ABind),IID_IADsContainer,pointer(ADsCont));
    try
      ADsCont.Delete('user','CN='+ReplaceSpecChar(edtFullName.Text));
      s:=Format(_('User: %s was removed from ADS!'),[edtFullName.Text]); //'Der Benutzer: %s wurde aus dem ADS entfernt!';
      if cbUserData.Checked then begin
        StatusWindow.ShowStatus(BottomLeftPos(btnSearch,Point(0,-150)),_('Deleting user data'),'',true,5);
        dc:=0; fc:=0;
        s:=s+sLineBreak+_('Following user data were deleted:'); //'Folgende Benutzerdaten wurden gelöscht:'
        with lbUserDirs do for i:=0 to Items.Count-1 do if Selected[i] then begin
          StatusWindow.Status:=Items[i];
          Application.ProcessMessages;
          DeleteDirectories(Items[i],'',dc,fc);
          s:=s+sLineBreak+_('  Directory: ')+Items[i]; //'  Verzeichnis: '
          end;
        StatusWindow.Close;
        dec(dc);
        if dc>0 then begin
          s:=s+sLineBreak+IntToStr(dc);
          if dc=1 then s:=s+_(' Subdirectory') //' Unterverzeichnis'
          else s:=s+_(' Subdirectories') //' Unterverzeichnisse';
          end
        else s:=s+sLineBreak+_('No subdirectories found'); //'keine Unterverzeichnisse gefunden'
        if fc>0 then begin
          s:=s+sLineBreak+IntToStr(fc);
          if fc=1 then s:=s+_(' file') //' Datei'
          else s:=s+_(' files'); //' Dateien';
          end;
        end ;
      InfoDialog (Caption,s);
      n:=lvUser.ItemIndex;
      with UserList do begin
        Delete(n);
        if n>=Count then n:=Count-1;
        end;
      with lvUser do begin
        Items.Count:=UserList.Count;
        ItemIndex:=n;
        ShowUserData(n);
        Invalidate;
        end;
    except
       ErrorDialog('',Format(_('User: %s '+sLineBreak+'could not be removed from ADS!'),
                             [edtFullName.Text]));
  //       'Der Benutzer: %s'+sLineBreak+'konnte nicht aus dem ADS entfernt werden!';
       end;
    end;...