Active Directory User Name from Logon Name

A

Arthur

Hi All,
I would like to get the name of the user given their networkID, is this
something Active Directory would be useful for?(For intranet users)
If so, can you please point me to some sample code/examples?
Thanks in advance,
Arthur
 
R

Richard Mueller

Arthur said:
I would like to get the name of the user given their networkID, is this
something Active Directory would be useful for?(For intranet users)
If so, can you please point me to some sample code/examples?
Thanks in advance,

I believe you want to determine the Common Name of the user (the value of
the cn attribute) from the value of the sAMAccountName attribute (the
"pre-Windows 2000 logon name" of the user). You can use the NameTranslate
object for this. I have VBScript examples at this link:

http://www.rlmueller.net/NameTranslateFAQ.htm
 
A

Arthur

Richard,
Thanks a lot for responding. This might sound ignorant, but is there
any specific namespace for NameTranslate class ? I cant find it under
System.DirectoryServices. Is it possible to post some sample C# code?

Thanks a lot,
Arthur
 
R

Richard Mueller

In vb.net I add a COM reference to Active DS Type Library (activeds.tlb).

Also, here is a link:

http://windowssdk.msdn.microsoft.com/en-us/library/ms679860(VS.80).aspx

Someone else my respond with C# code.

--
Richard
Microsoft MVP Scripting and ADSI
Hilltop Lab - http://www.rlmueller.net
Arthur said:
Richard,
Thanks a lot for responding. This might sound ignorant, but is there
any specific namespace for NameTranslate class ? I cant find it under
System.DirectoryServices. Is it possible to post some sample C# code?

Thanks a lot,
Arthur
 
E

Egbert Nierop \(MVP for IIS\)

Arthur said:
Richard,
Thanks a lot for responding. This might sound ignorant, but is there
any specific namespace for NameTranslate class ? I cant find it under
System.DirectoryServices. Is it possible to post some sample C# code?

If you're not afraid for C# with platform invokes, this is a close to OS
sample.

http://technolog.nl/blogs/eprogramm...out-unsafe-code_3F00_-_2800_update_2900_.aspx


But I'm sure you'll have a lot more use to the GetUserNameEx function, which
just gives you the name of the current user without going to the Active
Directory to 'translate' the name again (this function is a lot more
efficient).

Thanks a lot,
Arthur
 
M

Mark Rae

In vb.net I add a COM reference to Active DS Type Library (activeds.tlb).

??? Surely not ??? Why on earth would you want to invoke Interop for
something like this...???
Someone else my respond with C# code.

The following function will get any property of (pretty much) any AD object
which as a SAMAccountName:

public static string GetObjectProperty(string pstrObject, string
pstrProperty)
{
/// <summary>
/// Gets a given property value for a given ActiveDirectory object
/// </summary>
/// <param name="pstrObject">The ActiveDirectory object to query</param>
/// <param name="pstrProperty">The property to evaluate</param>
/// <returns>Property value</returns>

DirectorySearcher objSearch = new DirectorySearcher();
SearchResult objResult = null;

try
{
objSearch.Filter = String.Format("(SAMAccountName={0})",
pstrObject);
objSearch.PropertiesToLoad.Add(pstrProperty);
objResult = objSearch.FindOne();
return objResult.Properties[pstrProperty][0].ToString();
}
catch (System.Runtime.InteropServices.COMException)
{
return String.Empty;
}
catch (System.NullReferenceException)
{
return String.Empty;
}
catch (System.ArgumentOutOfRangeException)
{
return String.Empty;
}
catch (Exception)
{
throw;
}
finally
{
if (objResult != null)
{
objResult = null;
}
if (objSearch != null)
{
objSearch.Dispose();
objSearch = null;
}
}

string strUserName = GetObjectProperty ("arthur.surname", "displayName");
 
M

Mark Rae

Note that the "$" is only needed for computer objects.
Indeed.

Using DirectorySearcher is a good method.
Yes.

You will need a reference to System.DirectoryServices.dll.

Obviously. No need for unmanaged resources at all...
 
A

Arthur

Thank you all for you postings. However I am not able to get any value
(code is exactly the same.
I am passing the networkID for the first param & "displayName" for the
second param. Is there something I am missing ?

Please help.
Thanks,
Arthur
 
M

Mark Rae

Thank you all for you postings. However I am not able to get any value
(code is exactly the same.
I am passing the networkID for the first param & "displayName" for the
second param.

I presume you're referring to my GetObjectProperty function...?
Is there something I am missing ?

Almost impossible to tell from here...

When you say the "networkID", is that actually the SAMAccountName?

The code uses exception handling to exit "cleanly" - i.e. if it can't return
the property being requested, it will return an empty string. When you step
through it, what exception is it catching...?
 
A

Arthur

This is the error it throws.

COMException (0x80072020): An operations error occurred]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
+704
System.DirectoryServices.DirectoryEntry.Bind() +10
System.DirectoryServices.DirectoryEntry.get_AdsObject() +10
System.DirectoryServices.DirectorySearcher.FindAll(Boolean
findMoreThanOne) +199
System.DirectoryServices.DirectorySearcher.FindOne() +31

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()

Is the SAMAccountName different from this?

Thanks,
Arthur
 
M

Mark Rae

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()
Ah...

Is the SAMAccountName different from this?

Yes, almost certainly...

Can you give an example of one of the networkID values you are passing...?
 
A

Arthur

So my code looks somewhat like this.

string userId = Request.ServerVariables["LOGON_USER"].ToString();
string userName = GetObjectProperty(userId, "displayName");

Sample networkID = "arthurb"
it does not include the domain name or any other credentials.

How do i get the SAMAccountname?

Thanks,
Arthur

Mark said:
The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()
Ah...

Is the SAMAccountName different from this?

Yes, almost certainly...

Can you give an example of one of the networkID values you are passing...?
 
J

Joe Kaplan

That error in that particular place is a result of failing to bind to the
directory and then attempting a search as an anonyous user. This happens a
LOT in web applications when you are using IWA auth and impersonating or you
are not impersonating and the current process account isn't a valid domain
account.

Also, for the record, IADsNameTranslate is actually an ADSI wrapper around
the DsCrackNames API. It doesn't use the directorysearcher under the hood.
I'm pretty sure my coauthor has a nice C# wrapper around DsCrackNames in the
full samples of the downloadable code from our book's website if you are
interested.

As others have suggested, you can also call IADsNameTranslate via COM
Interop.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services Programming"
http://www.directoryprogramming.net
--
Arthur said:
This is the error it throws.

COMException (0x80072020): An operations error occurred]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
+704
System.DirectoryServices.DirectoryEntry.Bind() +10
System.DirectoryServices.DirectoryEntry.get_AdsObject() +10
System.DirectoryServices.DirectorySearcher.FindAll(Boolean
findMoreThanOne) +199
System.DirectoryServices.DirectorySearcher.FindOne() +31

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()

Is the SAMAccountName different from this?

Thanks,
Arthur


Mark said:
I presume you're referring to my GetObjectProperty function...?


Almost impossible to tell from here...

When you say the "networkID", is that actually the SAMAccountName?

The code uses exception handling to exit "cleanly" - i.e. if it can't
return
the property being requested, it will return an empty string. When you
step
through it, what exception is it catching...?
 
A

Arthur

Thanks Joe for your inputs. I am actually not that familiar with active
directory or its corresponding objects in .net, this is sort of my
first look into it and hence your comments seem a little detailed. Can
I assume that the code/approach I am using for my scenario will not get
me the desired results? I am guessing that what I am trying to do (get
the full name of users trying to access a web application from the
windows logon name/networkID) is a pretty common requirement, is there
anyone out there who has successfully tried this out?

Please help.
Thanks,
Arthur



Joe said:
That error in that particular place is a result of failing to bind to the
directory and then attempting a search as an anonyous user. This happens a
LOT in web applications when you are using IWA auth and impersonating or you
are not impersonating and the current process account isn't a valid domain
account.

Also, for the record, IADsNameTranslate is actually an ADSI wrapper around
the DsCrackNames API. It doesn't use the directorysearcher under the hood.
I'm pretty sure my coauthor has a nice C# wrapper around DsCrackNames in the
full samples of the downloadable code from our book's website if you are
interested.

As others have suggested, you can also call IADsNameTranslate via COM
Interop.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services Programming"
http://www.directoryprogramming.net
--
Arthur said:
This is the error it throws.

COMException (0x80072020): An operations error occurred]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
+704
System.DirectoryServices.DirectoryEntry.Bind() +10
System.DirectoryServices.DirectoryEntry.get_AdsObject() +10
System.DirectoryServices.DirectorySearcher.FindAll(Boolean
findMoreThanOne) +199
System.DirectoryServices.DirectorySearcher.FindOne() +31

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()

Is the SAMAccountName different from this?

Thanks,
Arthur


Mark said:
Thank you all for you postings. However I am not able to get any value
(code is exactly the same.
I am passing the networkID for the first param & "displayName" for the
second param.

I presume you're referring to my GetObjectProperty function...?

Is there something I am missing ?

Almost impossible to tell from here...

When you say the "networkID", is that actually the SAMAccountName?

The code uses exception handling to exit "cleanly" - i.e. if it can't
return
the property being requested, it will return an empty string. When you
step
through it, what exception is it catching...?
 
J

Joe Kaplan

This is done frequently. There are really two ways to skin the cat:

- Do an LDAP query using the DirectorySearcher
- Use the DsCrackNames API (or IADsNameTranslate, which is the same thing)

Both of these things will work fine, although DsCrackNames has a more
limited set of naming attributes it can return. The DirectorySearcher can
issue any LDAP query, so it is more flexible.

Both have different network requirements, as LDAP goes over port 389 (or
636) and DsCrackNames is an RPC call.

They both have similar but different context requirements, in that they need
to know what domain to contact and what credentials to use to perform the
query. There are a bunch of options for how this stuff may be specified and
the credentials part is the thing that usually trips people up in the
context of a web application since there are so many options in ASP.NET with
impersonation and all the different ways the process model can be
configured.

From my perspective, the most important thing to decide first is what
security architecture do you want to use, trusted subsystem or delegated.
With trusted subsystem, you use a fixed service account to perform the
query. With delegated, you use the credentials of the authenticated user
for querying the directory. Both are perfectly valid approaches and both
can be made to work. This is really no different than when designing a SQL
Server backend and picking the connection string to use based on the
security model that you'll use in SQL Server.

I actually wrote a whole book about this which you might find useful,
although I'm perfectly happy to answer your questions here.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services Programming"
http://www.directoryprogramming.net
--
Arthur said:
Thanks Joe for your inputs. I am actually not that familiar with active
directory or its corresponding objects in .net, this is sort of my
first look into it and hence your comments seem a little detailed. Can
I assume that the code/approach I am using for my scenario will not get
me the desired results? I am guessing that what I am trying to do (get
the full name of users trying to access a web application from the
windows logon name/networkID) is a pretty common requirement, is there
anyone out there who has successfully tried this out?

Please help.
Thanks,
Arthur



Joe said:
That error in that particular place is a result of failing to bind to the
directory and then attempting a search as an anonyous user. This happens
a
LOT in web applications when you are using IWA auth and impersonating or
you
are not impersonating and the current process account isn't a valid
domain
account.

Also, for the record, IADsNameTranslate is actually an ADSI wrapper
around
the DsCrackNames API. It doesn't use the directorysearcher under the
hood.
I'm pretty sure my coauthor has a nice C# wrapper around DsCrackNames in
the
full samples of the downloadable code from our book's website if you are
interested.

As others have suggested, you can also call IADsNameTranslate via COM
Interop.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services
Programming"
http://www.directoryprogramming.net
--
Arthur said:
This is the error it throws.

COMException (0x80072020): An operations error occurred]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
+704
System.DirectoryServices.DirectoryEntry.Bind() +10
System.DirectoryServices.DirectoryEntry.get_AdsObject() +10
System.DirectoryServices.DirectorySearcher.FindAll(Boolean
findMoreThanOne) +199
System.DirectoryServices.DirectorySearcher.FindOne() +31

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()

Is the SAMAccountName different from this?

Thanks,
Arthur


Mark Rae wrote:

Thank you all for you postings. However I am not able to get any
value
(code is exactly the same.
I am passing the networkID for the first param & "displayName" for
the
second param.

I presume you're referring to my GetObjectProperty function...?

Is there something I am missing ?

Almost impossible to tell from here...

When you say the "networkID", is that actually the SAMAccountName?

The code uses exception handling to exit "cleanly" - i.e. if it can't
return
the property being requested, it will return an empty string. When you
step
through it, what exception is it catching...?
 
A

Arthur

Thanks Joe, that cleared things quite a bit.

I would prefer to go with the delegated (assuming you mean the logged
on user & not the account under which the .net app is running ? )..can
you please point me to some code?

Going back to your previous post, I have disabled anonymous access &
enabled Integrated windows authentication.

I greatly appreciate all the help as I have been going back and forth
with this for almost a week now.

Thanks,
Arthur




Joe said:
This is done frequently. There are really two ways to skin the cat:

- Do an LDAP query using the DirectorySearcher
- Use the DsCrackNames API (or IADsNameTranslate, which is the same thing)

Both of these things will work fine, although DsCrackNames has a more
limited set of naming attributes it can return. The DirectorySearcher can
issue any LDAP query, so it is more flexible.

Both have different network requirements, as LDAP goes over port 389 (or
636) and DsCrackNames is an RPC call.

They both have similar but different context requirements, in that they need
to know what domain to contact and what credentials to use to perform the
query. There are a bunch of options for how this stuff may be specified and
the credentials part is the thing that usually trips people up in the
context of a web application since there are so many options in ASP.NET with
impersonation and all the different ways the process model can be
configured.

From my perspective, the most important thing to decide first is what
security architecture do you want to use, trusted subsystem or delegated.
With trusted subsystem, you use a fixed service account to perform the
query. With delegated, you use the credentials of the authenticated user
for querying the directory. Both are perfectly valid approaches and both
can be made to work. This is really no different than when designing a SQL
Server backend and picking the connection string to use based on the
security model that you'll use in SQL Server.

I actually wrote a whole book about this which you might find useful,
although I'm perfectly happy to answer your questions here.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services Programming"
http://www.directoryprogramming.net
--
Arthur said:
Thanks Joe for your inputs. I am actually not that familiar with active
directory or its corresponding objects in .net, this is sort of my
first look into it and hence your comments seem a little detailed. Can
I assume that the code/approach I am using for my scenario will not get
me the desired results? I am guessing that what I am trying to do (get
the full name of users trying to access a web application from the
windows logon name/networkID) is a pretty common requirement, is there
anyone out there who has successfully tried this out?

Please help.
Thanks,
Arthur



Joe said:
That error in that particular place is a result of failing to bind to the
directory and then attempting a search as an anonyous user. This happens
a
LOT in web applications when you are using IWA auth and impersonating or
you
are not impersonating and the current process account isn't a valid
domain
account.

Also, for the record, IADsNameTranslate is actually an ADSI wrapper
around
the DsCrackNames API. It doesn't use the directorysearcher under the
hood.
I'm pretty sure my coauthor has a nice C# wrapper around DsCrackNames in
the
full samples of the downloadable code from our book's website if you are
interested.

As others have suggested, you can also call IADsNameTranslate via COM
Interop.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services
Programming"
http://www.directoryprogramming.net
--
This is the error it throws.

COMException (0x80072020): An operations error occurred]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
+704
System.DirectoryServices.DirectoryEntry.Bind() +10
System.DirectoryServices.DirectoryEntry.get_AdsObject() +10
System.DirectoryServices.DirectorySearcher.FindAll(Boolean
findMoreThanOne) +199
System.DirectoryServices.DirectorySearcher.FindOne() +31

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()

Is the SAMAccountName different from this?

Thanks,
Arthur


Mark Rae wrote:

Thank you all for you postings. However I am not able to get any
value
(code is exactly the same.
I am passing the networkID for the first param & "displayName" for
the
second param.

I presume you're referring to my GetObjectProperty function...?

Is there something I am missing ?

Almost impossible to tell from here...

When you say the "networkID", is that actually the SAMAccountName?

The code uses exception handling to exit "cleanly" - i.e. if it can't
return
the property being requested, it will return an empty string. When you
step
through it, what exception is it catching...?
 
J

Joe Kaplan

Ok, in the delegated model, you don't do much from either the DsCrackNames
(IADsNameTranslate) or the DirectorySearcher perspective. I'll just talk
about the DirectorySearcher example, as DsCrackNames would depend a lot on
how you are setting up the wrapper.

The security context and domain identification info for a DirectorySearcher
object is determined by the DirectoryEntry object that is used as the
SearchRoot property. When you want to use the credentials of the currently
security context (in this case, the thread in ASP.NET that is impersonating
the logged on user), you just specify "null" for the username and password
and specify AuthenticationTypes.Secure:

DirectoryEntry searchRoot = new DirectoryEntry(path, null, null,
AuthenticationTypes.Secure);

To answer your question, let's say you wanted to query AD with a fixed
service account (trusted subsystem) instead of the authenticated user's
credentials. In this case, you could simply disable impersonation in
ASP.NET and then the current security context would be that of the process,
not of the user. If you are using IIS 6, then this is the app pool
identity. That is NETWORK SERVICE by default and it uses the credentials of
the machine account on the network, so the credentials would appear to AD as
the machine account for the web server. It would need the permissions to
execute whatever query you want to do (which it probably has by default).

The path is a composite piece of information and determines the protocol to
use (LDAP or GC, which uses LDAP to search the global catalog for the forest
on the GC port 3268), the domain or server to use and the path into the AD
store that will be used as the search base:

<protocol>://<server or domain>/<store path>

Note that the server or domain part is optional in some cases, as Windows
knows how to infer a server to use from the security context of the current
thread. This is called a "serverless" bind and looks like this:

<protocol>://<store path>

You'll see a lot of examples that look like that. In your case, you should
be able to do a serverless bind since presumably the current security
context is that of a domain user, so Windows should be able to discover
their domain and then infer a DC to query from that.

Ok, all that said now, the real problem you are going to have in the
delegated model is flowing the user's credentials from the browser to the
web server to the AD. Since you are using IWA auth, you will have what is
called a "double hop" in distributed authentication lingo. The bottom line
is that these don't work unless you have Kerberos delegation enabled.
Kerberos delegation is a can of worms to get working, but it can definitely
be done successfully.

There have been MANY newsgroup posts and kbase articles written that discuss
Kerberos delegation in detail, so I'd suggest you do some searches first and
come back with questions.

If you don't want to have to get Kerberos delegation working, then an option
is to switch to the trusted subsystem model and query using the process
account.

We also discuss all of this stuff in MUCH more detail in Ch 3 and 8 of our
book.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services Programming"
http://www.directoryprogramming.net
--
Arthur said:
Thanks Joe, that cleared things quite a bit.

I would prefer to go with the delegated (assuming you mean the logged
on user & not the account under which the .net app is running ? )..can
you please point me to some code?

Going back to your previous post, I have disabled anonymous access &
enabled Integrated windows authentication.

I greatly appreciate all the help as I have been going back and forth
with this for almost a week now.

Thanks,
Arthur




Joe said:
This is done frequently. There are really two ways to skin the cat:

- Do an LDAP query using the DirectorySearcher
- Use the DsCrackNames API (or IADsNameTranslate, which is the same
thing)

Both of these things will work fine, although DsCrackNames has a more
limited set of naming attributes it can return. The DirectorySearcher
can
issue any LDAP query, so it is more flexible.

Both have different network requirements, as LDAP goes over port 389 (or
636) and DsCrackNames is an RPC call.

They both have similar but different context requirements, in that they
need
to know what domain to contact and what credentials to use to perform the
query. There are a bunch of options for how this stuff may be specified
and
the credentials part is the thing that usually trips people up in the
context of a web application since there are so many options in ASP.NET
with
impersonation and all the different ways the process model can be
configured.

From my perspective, the most important thing to decide first is what
security architecture do you want to use, trusted subsystem or delegated.
With trusted subsystem, you use a fixed service account to perform the
query. With delegated, you use the credentials of the authenticated user
for querying the directory. Both are perfectly valid approaches and both
can be made to work. This is really no different than when designing a
SQL
Server backend and picking the connection string to use based on the
security model that you'll use in SQL Server.

I actually wrote a whole book about this which you might find useful,
although I'm perfectly happy to answer your questions here.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services
Programming"
http://www.directoryprogramming.net
--
Arthur said:
Thanks Joe for your inputs. I am actually not that familiar with active
directory or its corresponding objects in .net, this is sort of my
first look into it and hence your comments seem a little detailed. Can
I assume that the code/approach I am using for my scenario will not get
me the desired results? I am guessing that what I am trying to do (get
the full name of users trying to access a web application from the
windows logon name/networkID) is a pretty common requirement, is there
anyone out there who has successfully tried this out?

Please help.
Thanks,
Arthur



Joe Kaplan wrote:
That error in that particular place is a result of failing to bind to
the
directory and then attempting a search as an anonyous user. This
happens
a
LOT in web applications when you are using IWA auth and impersonating
or
you
are not impersonating and the current process account isn't a valid
domain
account.

Also, for the record, IADsNameTranslate is actually an ADSI wrapper
around
the DsCrackNames API. It doesn't use the directorysearcher under the
hood.
I'm pretty sure my coauthor has a nice C# wrapper around DsCrackNames
in
the
full samples of the downloadable code from our book's website if you
are
interested.

As others have suggested, you can also call IADsNameTranslate via COM
Interop.

Joe K.

--
Joe Kaplan-MS MVP Directory Services Programming
Co-author of "The .NET Developer's Guide to Directory Services
Programming"
http://www.directoryprogramming.net
--
This is the error it throws.

COMException (0x80072020): An operations error occurred]
System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
+704
System.DirectoryServices.DirectoryEntry.Bind() +10
System.DirectoryServices.DirectoryEntry.get_AdsObject() +10
System.DirectoryServices.DirectorySearcher.FindAll(Boolean
findMoreThanOne) +199
System.DirectoryServices.DirectorySearcher.FindOne() +31

The networkID i am passing to the function is value from
Request.ServerVariables["LOGON_USER"].ToString()

Is the SAMAccountName different from this?

Thanks,
Arthur


Mark Rae wrote:

Thank you all for you postings. However I am not able to get any
value
(code is exactly the same.
I am passing the networkID for the first param & "displayName"
for
the
second param.

I presume you're referring to my GetObjectProperty function...?

Is there something I am missing ?

Almost impossible to tell from here...

When you say the "networkID", is that actually the SAMAccountName?

The code uses exception handling to exit "cleanly" - i.e. if it
can't
return
the property being requested, it will return an empty string. When
you
step
through it, what exception is it catching...?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top