We help IT Professionals succeed at work.

We've partnered with Certified Experts, Carl Webster and Richard Faulkner, to bring you two Citrix podcasts. Learn about 2020 trends and get answers to your biggest Citrix questions!Listen Now

x

Active Directory problems.

QC20N
QC20N asked
on
Medium Priority
839 Views
Last Modified: 2013-11-23
I have this code. When I run the code and in, the While not, I don't get any errors 1st time I pass line 15, but 2nd time, it jumps to line 28.

Should I release the object some how everytime I have done something with the object?

procedure TFrmMain.Button4Click(Sender: TObject);
var i, iDays : integer; adUsr : IADsUser; sDays : string;
begin
  memo1.Lines.Clear;
  ADOQuery1.Close;
  try
    ADOQuery1.SQL.Clear;
    ADOQuery1.SQL.Text := 'SELECT samAccountName, CN FROM '+ Quotedstr('LDAP://OU=USERS,' + edit2.Text) + ' WHERE objectClass='+ Quotedstr('user') + ' ORDER by CN';
    ADOQuery1.Open;
    while not ADOQuery1.Eof do
    begin
      ADsGetObject('LDAP://CN='+ADOQuery1.FieldByName('CN').AsString+',OU=USERS,' + Edit2.Text, IADsUser, adusr);
      label8.Caption := datetostr(adusr.LastLogin);
      if adusr.AccountDisabled then
      begin
        sdays := InttoStr(MonthsBetween(Date, ADUsr.LastLogin));
        idays := StrToInt(sDays);
        if IDays > 0 then
        begin
          if not renamefile('\\seluws0187\home_dkso\'+ADOQuery1.FieldByName('samAccountName').AsString,'\\seluws0187\home_dkso\_'+ADOQuery1.FieldByName('samAccountName').AsString) then
          begin
            Memo1.Lines.Add(ADOQuery1.FieldByName('samAccountName').AsString);
          end;
        end;
      end;
      ADOQuery1.Next;
    end;
  finally
    ADOQuery1.Close;
     label7.Caption := inttostr(memo1.Lines.Count);
  end;
end;

Open in new window

Comment
Watch Question

MerijnBSr. Software Engineer
CERTIFIED EXPERT

Commented:
The line numbers you mention, are those the line numbers in the code snippet in your post (line 15 = begin, line 28 = finally)
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
add this:
if adusr <> nil then
begin



ADsGetObject('LDAP://CN='+ADOQuery1.FieldByName('CN').AsString+',OU=USERS,' + Edit2.Text, IADsUser, adusr);
if adusr <> nil then
begin
      label8.Caption := datetostr(adusr.LastLogin);
      if adusr.AccountDisabled then

Author

Commented:
No, the line numbers is 13 and 26.

Author

Commented:
Geert Gruwez. It does the same thing.

My error is OLE Error 8000500D

Author

Commented:
Ahh, crap. No, it is #13 and #29.

My mistake. Sorry.

Author

Commented:
You have any input?

Author

Commented:
.As long I don't use:
adusr.Lastlogin

I don't get any errors.

Very annoying. :)
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
only use lastlogin once

var dt: TDateTime;

      label8.Caption := '';
      if adusr.AccountDisabled then
      begin
        dt := ADUsr.LastLogin;
        label8.Caption := DateToStr(dt);
        sdays := InttoStr(MonthsBetween(Date, dt));
        idays := StrToInt(sDays);
        if IDays > 0 then
        begin

Author

Commented:
But I want to check it for every user in our environment, that is disabled.

        label8.Caption := '';
        if adusr.AccountDisabled then
        begin
          dt := ADUsr.LastLogin;
          label8.Caption := DateToStr(dt);
          sdays := InttoStr(MonthsBetween(Date, dt));
          idays := StrToInt(sDays);
          if IDays > 0 then
          begin

Open in new window

Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
you only put it in the caption for label8
you aren't doing any checking at all for disabled accounts ...

it's not logical why you want to display a date in a caption and not do anything with it
it gets written over for the next non disabled user any way

i know it's only a test scenario you gave, but that's all we see to work with
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
next to that, you don't have any
Application.ProcessMessages calls nor any label8.update
so while doing the loop windows will not have any idle time to draw the caption anyway
so you won't see the label8.caption change either

it may look nice in code, but you just don't see anything on screen changing when running
unless you have 2 monitors and are debugging

Author

Commented:
All you see is only test scenario.
The reason for displaying date in Caption is just when I debug. I'm not going to use in the finished project.

Believe me, I have tried differents solution to check when they were disabled and when the user haven't login for more than 1 month.

The code is the end code. But nomatter what I do I get an error everytime a pass ADUser.Lastlogin.
And that is why you have seen the test scenario, cause I have tried different solutions to see if I did something wrong.
procedure TFrmMain.Button4Click(Sender: TObject);
var i, iDays : integer; adUsr : IADsUser; sDays : string;
begin
  memo1.Lines.Clear;
  ADOQuery1.Close;
  try
    ADOQuery1.SQL.Clear;
    ADOQuery1.SQL.Text := 'SELECT samAccountName, CN FROM '+ Quotedstr('LDAP://OU=USERS,' + edit2.Text) + ' WHERE objectClass='+ Quotedstr('user') + ' ORDER by CN';
    ADOQuery1.Open;
    while not ADOQuery1.Eof do
    begin
      ADsGetObject('LDAP://CN='+ADOQuery1.FieldByName('CN').AsString+',OU=USERS,' + Edit2.Text, IADsUser, adusr);
      if (adusr.AccountDisabled) and (MonthsBetween(Date, ADUsr.LastLogin) > 0) then
      begin
        if not renamefile('\\seluws0187\home_dkso\'+ADOQuery1.FieldByName('samAccountName').AsString,'\\seluws0187\home_dkso\_'+ADOQuery1.FieldByName('samAccountName').AsString) then
        begin
          Memo1.Lines.Add(ADOQuery1.FieldByName('samAccountName').AsString);
        end;
      end;
      ADOQuery1.Next;
    end;
  finally
    ADOQuery1.Close;
  end;
end;

Open in new window

Author

Commented:
I get an error everytime 2nd pass ADUser.lastlogin. I mean, 1st it is ok, but 2nd time I get the error.
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
what if this isn't filled in ?

try
  dt := aduser.LastLogin;
except
  dt := 0;
end;

then use if dt > 0

does it pass for any next records ?
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
remember that when using AD on XP you have a memory leak

it only works as it should on 2003 and up

Author

Commented:
I tried what you have been written. And yes, it pass for some of the disabled users.
        if adusr.AccountDisabled {and (inttostr(MonthsBetween(date,adusr.lastlogin)) > '0')} then
        begin
          try
            dt := ADUsr.LastLogin;
          except
            dt := 0;
          end;
          label8.Caption := DateToStr(dt);

Open in new window

Author

Commented:
You have any idea what cause that?
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
when it is null you will get an error (or not set in AD)


Author

Commented:
But isen't Lastlogin set all the time in AD?
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
obviously not
create a new user and don't login with it yet, then run your code ...

Author

Commented:
I'll raise the points, for a solution or might an explanation why this dosen't work.
MerijnBSr. Software Engineer
CERTIFIED EXPERT

Commented:
> create a new user and don't login with it yet, then run your code

Need more explanation than that?
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
not to offend, but i allready showed you the solution,
you seem not to grasp it ...

only read the LastLogin once in a try except into a variable
and use that variable

try
  dt := aduser.LastLogin;
except
  dt := 0;
end;

raise points ?
admit it, your point giving is ridicolously low for the last questions ...

Author

Commented:
:) I was writeing my comment before I saw your input. :) I will of course try that.
Oracle dba
CERTIFIED EXPERT
Top Expert 2009
Commented:
the error seems to happen for every item read from active directory which is not set

here is my code:
hope you get the idea ...
function TImportThread.ReadProperty(Obj: IAds; propName: string = 'businessRoles'): string;
var
  prop, subprop: OleVariant;
  Temp: string;
  I, J, m, n: integer;
begin
  Temp := '';
  prop := VarArrayOf([propname]);
  Obj.GetInfoEx(prop, 0);
  try
    prop := Obj.GetEx(propname);
    n := VarArrayDimCount(prop);
    if n = 0 then
      Temp := Temp + VarToStr(prop)
    else if n = 1 then
      for I := VarArrayLowBound(prop, 1) to VarArrayHighBound(prop, 1) do
      begin
        subprop := prop[I];
        m := VarArrayDimCount(subprop);
        if m = 0 then
          Temp := Temp + VarToStr(subprop)
        else if m = 1 then
        begin
          for J := VarArrayLowBound(subprop, 1) to VarArrayHighBound(subprop, 1) do
            Temp := Temp + Chr(StrToInt(VarToStr(subprop[J])));
        end;
      end;
  except
    //on E: Exception do
    //  AddLog(logImport, 'error reading property ' + propname + ' : ' + E.Message);
  end;
  if (Temp <> '') and (Temp[1] <> '"') and (Temp[length(Temp)] <> '"') then
    Temp := AnsiQuotedStr(Temp, '"');
  Result := Temp;
end;
 
procedure TImportUsers.ImportUser(Obj: IAds);
var
  AccountName, FirstName, LastName, DisplayName, Descr, EMail, Attribs, aDomain, aPath: string;
  aUser: IADsUser;
 
  function GetProp(UserObj: IAdsUser; PropName: string): string;
  begin
    Result := '';
    try
      Result := UserObj.Get(propName);
    except
    end;
  end;
 
begin
  if not Terminated then
  try
    aUser := Obj as IADsUser;
    AccountName := GetProp(aUser, 'samAccountName');
    FirstName := GetProp(aUser, 'FirstName');
    LastName := GetProp(aUser, 'LastName');
    DisplayName := GetProp(aUser, 'FullName');
    Descr := GetProp(aUser, 'Description');
    Email := GetProp(aUser, 'mail');
    Attribs := ReadProperty(Obj);

Open in new window

Not the solution you were looking for? Getting a personalized solution is easy.

Ask the Experts

Author

Commented:
Please forgive me.
If you can categorise how many points should be given for a question I will be happy to give those points. I don't know how many points there should be for a question. It is not to be an dummy, but I just don't know.

br
QC20N

Author

Commented:
I have created a user and I didn't login in.

And when I run the code it jumps to dt := 0; So I see your point. A user that never have login dosen't have a lastlogin but,  I know for a fact that some disabled users in our AD have logged in, but they fail.

Btw, how do I call:
procedure TImportUsers.ImportUser(Obj: IAds);

ImportUser(XXX);

Author

Commented:
:) I now how call the procedure.

I have added this:

    alastLogin := GetProp(aUser, 'lastLogonTimestamp');

to the code you sent.

And call procedure

        if adusr.AccountDisabled {and (inttostr(MonthsBetween(date,adusr.lastlogin)) > '0')} then
        begin
          ImportUser(adusr);
          try
            dt := ADUsr.LastLogin;
          except
            dt := 0;
          end;


When I run the code it raise this exception:
Raised exception class EVariantTypeCastError
"Could not convert variant of type (Dispatch) into type (String)"

You know why?
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
i just posted some of my code ...
you will have your own code

but you would have to change the proc to suit your needs :


procedure TYourClass.ImportUser(aUser: IADsUser);
  ...
begin
  if not Terminated then
  try
    AccountName := GetProp(aUser, 'samAccountName');


Author

Commented:
True. And most of the code I can use, but I have added an extra line to get the lastlogin.

Author

Commented:
I will try again.

I have added this:

    alastLogin := GetProp(aUser, 'lastLogonTimestamp');

to the code you sent.

And call procedure

        if adusr.AccountDisabled then
        begin
  // The reason why both ImportUser and Try..Except is here. Is only to see wich of them is correct.
          ImportUser(adusr);
          try
            dt := ADUsr.LastLogin;
          except
            dt := 0;
          end;


When I run the code it raise this exception in the ImportUser proc:
Raised exception class EVariantTypeCastError
"Could not convert variant of type (Dispatch) into type (String)"

btw, how can I check the "logoncount"

If I do this:

    ADOQuery1.SQL.Text := 'SELECT LogonCount, samAccountName, distinguishedname, ADsPath,CN FROM '+ Quotedstr('LDAP://OU=USERS,' + edit2.Text) + ' WHERE objectClass='+ Quotedstr('user') + ' ORDER by CN';

and do this:

label8.Caption := ADOQuery1.FieldByName('LogonCount').AsString;

label8.caption is empty even though I know that it should be at least "0"

using AD Explorer from sysInternals to see the value.

Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
some servers don't maintain counting logons
LogonAttemps yes, but that depends also on the policies set

Author

Commented:
Ok. but I can see that the logoncount is in use on our users in the AD.
How can you collect logoncount data from AD? Is that an integer or a string?

Author

Commented:
Well, the integer, string part I found out, but even I can see on one user that the logoncount is 140 I don't get any data from the logoncount.

What my goal is with all this is only to do delete the user when the user haven't been login and is disabled, so if I use your solution and dt is 0. The date will be 1899-30-12. But I really don't know if the user haven't been logged in for more than a month if the date i 1899-30-12.

try
dt := ADUsr.LastLogin;
except
dt := 0;
end;
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
this solution only provided a way of reading data which isn't there ...
it's up to you to combine both ...

screenshot.png
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
use logonCount

Author

Commented:
My good expertfriend, if you see early in this thread, I ask how to use LogonCount, cause I tried this:

"
btw, how can I check the "logoncount"

If I do this:

ADOQuery1.SQL.Text := 'SELECT LogonCount, samAccountName, distinguishedname, ADsPath,CN FROM '+ Quotedstr('LDAP://OU=USERS,' + edit2.Text) + ' WHERE objectClass='+ Quotedstr('user') + ' ORDER by CN';

and do this:

label8.Caption := ADOQuery1.FieldByName('LogonCount').AsString;

label8.caption is empty even though I know that it should be at least "0"

using AD Explorer from sysInternals to see the value.

"
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
small letter l (first letter), caps letter C for count
logonCount
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
not LogonCount and not logoncount ...
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
the damndest thing about Active Directory = case sensitive !

Author

Commented:
This is what my SQL is:
    ADOQuery1.SQL.Clear;
    ADOQuery1.SQL.Text := 'SELECT whenChanged, logonCount, samAccountName, distinguishedname, ADsPath,CN FROM '+ Quotedstr('LDAP://OU=USERS,' + edit2.Text) + ' WHERE objectClass='+ Quotedstr('user') + ' ORDER by CN';
    ADOQuery1.Open;

And I do this:

      ADsGetObject(ADOQuery1.FieldByName('AdsPath').AsString, IADsUser, adusr);
      if adusr <> nil then
      begin
        label8.Caption := ADOQuery1.FieldByName('logonCount').AsString;

but I don't get anything even that I can see by using AD Explorer from sysInternals.

Author

Commented:
Jesus christ, if I use the code you sent previously it works. God damn. you are good.

Author

Commented:
Ups, sorry. No such words. My mistake.

Author

Commented:
Something I don't understand is:

why this dosen't work:
 alastLogin := GetProp(aUser, 'lastLogon');

but this do
dt := ADUsr.LastLogin;

on the same user.
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
you have a type libray imported ?
maybe the LastLogin function is a wrapper around a call to lastLogon
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
PS: God has nothing to do with it

Author

Commented:
"you have a type libray imported ?"
Not sure what you mean
Just for the record. I still don't understand why
dt := ADUsr.LastLogin;

fails even when I know there is a lastLogin date.
Can see this by using AD Explorer from sysInternals
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
just for the record, Delphi + Active Directory
is one of the most difficult and cumbersome things i encountered up to now

just so you know what you are up against too :)
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
these are the units to talk to active directory
first is TLB (type library), which is where the function LastLogin will eventually lead

  ActiveDs_TLB in 'C:\Program Files\borland\Delphi7\Imports\ActiveDs_TLB.pas',
  adshlp in 'adshlp.pas',

Author

Commented:
Yes, those are in my project. :)

Author

Commented:
WHen I look in AD Explorer from sysInternals I can see that lastLogon has 2 values. Could that be the answer why I get an exception:

Raised exception class EVariantTypeCastError
"Could not convert variant of type (Dispatch) into type (String)"

when I do this:
alastLogin := GetProp(aUser, 'lastLogon');
Geert GOracle dba
CERTIFIED EXPERT
Top Expert 2009

Commented:
have you tried debugging with the ReadProperty(adsuser, 'logonCount');

it may be an octetstring ...

thx for the link to the AD Explorer, we had problems we LDAP administrator and this is a real help.
now i don't have to bother the .Net guys anymore to get info

Author

Commented:
I'm not sure how I can call ReadProperty with those parameters. As I see in your code previously ReadProperty is called like this:
Attribs := ReadProperty(Obj);

Btw, the lastLogon is in a integer8 format. Could that be the reason why it fails?

Author

Commented:
And no problem with the link. Happy to help.
Access more of Experts Exchange with a free account
Thanks for using Experts Exchange.

Create a free account to continue.

Limited access with a free account allows you to:

  • View three pieces of content (articles, solutions, posts, and videos)
  • Ask the experts questions (counted toward content limit)
  • Customize your dashboard and profile

*This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

OR

Please enter a first name

Please enter a last name

8+ characters (letters, numbers, and a symbol)

By clicking, you agree to the Terms of Use and Privacy Policy.