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öglicht, 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 2008 und 2012) als auch im Ressource-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 geeignete Interface-Units zu den Windows-Schnittstellen. In den weiter unten beschriebenen Programmen werden dazu die Funktionen aus der JEDI Code Library verwendet. Einige weitere interessante Informationen zu den Active Directory Service Interfaces findet man auf der Webseite von Agnisoft.

Nachfolgend soll anhand von einigen Beispielprogrammen näher erläutert werden, welche Möglichkeiten sich hier dem Windows-Administrator bieten. Die Programme werden sowohl als ausführbare Dateien als auch im Quelltext bereitgestellt:

Programme

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

Downloads (Vers. Dez. 2016)

Ausführbare Programme (deutsch und englisch, 3,28 MB) Quelltexte (800 kB)

Authentifizierung 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>] [<coded password>]

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)
coded password: base64-(bzw. mime-)kodierte Kennwort

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.
Wenn <password> = base64 ist, wird der Parameter <coded password> benötigt. Er stellt das base64-(bzw. mime-)kodierte Kennwort dar. In diesem Fall muss der Parameter <group> immer angegeben werden. Falls keine Gruppe geprüft werden soll, muss hier eine Leerstring ("") stehen.

Rückmeldewerte des Programms (ExitCode)

0 : OK - Benutzer wurde authentifiziert 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 : Authentifizierung 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 werden. Für die Eingabe der Passwörter, sowie die Zuordnung zu bestimmten Organisationseinheiten (OU) und Gruppen sind zusätzliche Schritte erforderlich.

Das nachfolgend beschriebene Delphi-Programm 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.
  ...
  uses ..., JwaAdsHlp, JwaAdsTLB, JwaAdsErr;
  ...
  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),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 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 Organisationseinheiten 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.
  ...
  uses ..., JwaAdsHlp, JwaAdsTLB, JwaAdsErr;
  ...
  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;
    ...                      // see code snippet above
      // 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,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 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,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,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 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 angelegt 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('LDAP://'+FDomain,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 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,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 jedes Mal 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  // Kennwort
              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;
      // Arbeitsgruppen zuordnen
        with lbxAG do if SelCount>0 then for i:=0 to Items.Count-1 do if Selected[i] then begin
          if succeeded(ADsGetObject((Items.Objects[i] as TADsObject).FPath,IID_IADsGroup,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 Domänenbenutzer anzulegen. Durch Zuordnung zu einer geeigneten Organisationseinheit erhalten sie automatisch die passenden Gruppenrichtlinien 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.
Zu den weiteren Einstellungen siehe hier.
Außerdem ist es möglich vorhandene Konten mit neuen Passwörtern zu versehen.

Besonderheiten des Programms
Setzen der Eigenschaft: Benutzer kann Kennwort nicht ändern
Diese Eigenschaft kann nicht zusammen 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 Methode 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,FDesc,FPath : string;
      constructor Create (AName,ADesc,Apath : string);
      end;
  const
    UserCols = 'Name,sAMAccountName,ADsPath,userAccountControl,mail';
    ...
  procedure StringToPwArray (SListe : string; var PwArr : TPWideArray; var Count : integer);
  var
    s : string;
  begin
    Count:=0;
    while length(SListe)>0 do begin
      s:=ReadNxtStr(SListe,Comma);
      if length(s)>0 then begin
        SetLength(PwArr,Count+1);
        GetMem(PwArr[Count],256);
        StringToWideChar(s,PwArr[Count],256);
        inc(Count)
        end;
      end;
    end;
  ...
  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
          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
          AccDisabled:=AccountDisabled;
          with btToggleAccount do begin
            Enabled:=true;
            Glyph:=nil;
            if AccDisabled then begin
              Caption:=_('Enable account');
              ImageList.GetBitmap(0,Glyph);
              end
            else begin
              Caption:=_('Disable account');
              ImageList.GetBitmap(1,Glyph);
              end;
            end;
        except
          AccDisabled:=false;
          btToggleAccount.Enabled:=false;
          end;
        lmDisabled.Visible:=AccDisabled;
        laDisabled.Visible:=AccDisabled;
        try
          ADsGetObject(PWideChar(Parent),IID_IADsOU,pointer(ADsOU));
          OUPath:=ADsOU.AdsPath;
        except
          OuPath:='';
          end;
        ADsOU:=nil;
        end;
      end;
    end;
Suche nach den Benutzerdaten:
Es wird nach einem Unterverzeichnis mit dem Namen des Benutzers gesucht.
  function TfrmDelUser.SearchDir (Base,SubDir,UserName : string) : string;
  var
    DirInfo    : TSearchRec;
    Findresult : integer;
    sd         : string;
  begin
    Result:='';
    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 (length(Result)=0) and (FindResult=0) do with DirInfo do begin
      if NotSpecialDir(Name) then begin
        if SameFileName(DirInfo.Name,UserName) then Result:=IncludeTrailingPathDelimiter(sd)+DirInfo.Name
        else Result:=SearchDir(Base,IncludeTrailingPathDelimiter(SubDir)+DirInfo.Name,UserName);
        end;
      FindResult:=FindNext(DirInfo);
      end;
    FindClose(DirInfo);
    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).
  procedure TfrmDelUser.RemoveUser (const ABind : string);
  var
    s          : string;
    dc,fc,i,n  : integer;
    ADsCont    : IADsContainer;
  ...
  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;
          // Löschen Benutzerverzeichnis einschließlich aller Unterverzeichnisse und Dateien
          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 ;
  ...