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>
Benutzer an der Domäne authenfizieren
Domänenbenutzer anlegen
Kursbenutzer anlegen
Domänenbenutzer löschen
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.
AuthAds <domain> <username> <password> [<group>]
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.
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 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
...
|
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;
...
|
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;...
|