I have a need to manage Kerberos Resource Based Delegation in C# (I know it's easier in Powershell but that is not the requirement). The attribute on the user/computer/service accounts is msDS-AllowedToActOnBehalfOfOtherIdentity, but this seems to be some COM object which I can't seem to deal with in C#:
static void Main(string[] args)
{
string ou = #"OU=some,OU=ou,DC=corp,DC=com";
string cn = #"someaccount";
DirectoryEntry de = new DirectoryEntry();
de.Username = #"CORP\userwithOUrights";
de.Password = #"password";
de.AuthenticationType = AuthenticationTypes.Secure;
de.Path = $"LDAP://CN={cn},{ou}";
Object a = de.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"];
}
After this, a doesn't seem to be anything I can do much with, unlike other properties. It is some COM object and I need to get the accounts which are in there. Powershell reports that this property returns a System.DirectoryServices.ActiveDirectorySecurity object and I see useful methods in this class for decoding the binary format which is stored in AD etc. But this does not seem to be the return type from the property call in C#.
Update: All of this is now better documented in an article on my website: Handling NT Security Descriptor attributes
According to this the "attribute syntax" for that attribute is 2.5.5.15. According to this, that means it's a "String(NT-Sec-Desc)". According to this, that means it's a IADsSecurityDescriptor COM object.
You can add a COM reference in your project to "Active DS Type library" and cast it directly to IADsSecurityDescriptor, like this:
var act = (ActiveDs.IADsSecurityDescriptor)
de.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value;
Console.WriteLine(act.Owner);
The Owner property gives you a DOMAIN\Username.
According to this random code I found, it seems you can also use the RawSecurityDescriptor class to interact with it. There is a constructor that takes a plain string, but you also can't seem to get the raw string from the attribute from DirectoryEntry.
But I did remember that sometimes DirectorySearcher will give you values in a different type than DirectoryEntry (doesn't make sense, but it's true). That appears to be true here. DirectorySearcher gives this attribute to you as a byte[], and RawSecurityDescriptor does have a constructor that takes a byte[].
So it seems you can do something like this:
string ou = #"OU=some,OU=ou,DC=corp,DC=com";
string cn = #"someaccount";
var search = new DirectorySearcher(new DirectoryEntry($"LDAP://{ou}"), $"(cn={cn})");
search.PropertiesToLoad.Add("msDS-AllowedToActOnBehalfOfOtherIdentity");
var result = search.FindOne();
var act = new RawSecurityDescriptor(
(byte[]) result.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"][0], 0);
Console.WriteLine(act.Owner);
//make changes to act.DiscretionaryAcl
byte[] descriptor_buffer = new byte[act.BinaryLength];
act.GetBinaryForm(descriptor_buffer, 0);
var de = result.GetDirectoryEntry();
de.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value = descriptor_buffer;
de.CommitChanges();
In this, act.Owner is an account SID.
Related
I've tryed a lot of others solutions, and I still didn't make it work. Can someone help me please.
my code is like that:
I saw something about secureString, I tryed to use it but it still didn't work.
I saw too another solution that says to use var rather than string in the variables. Didn't work
I dont know if I'm doing something wrong or if those solutions that dosoen't work.
public bool RedefinirSenha(string pUsuario, string pSenhaAtual, string pNovaSenha)
{
var NovaSenha = pNovaSenha;
var SenhaAtual = pSenhaAtual;
var Usuario = pUsuario;
//string Pwd = String.Format(#"""{0}""", NovaSenha);
//byte[] pwdCerto = System.Text.Encoding.Unicode.GetBytes(Pwd);
try
{
string LDAP = myLDAPpath;
DirectoryEntry ADcon = new DirectoryEntry(LDAP, Usuario, SenhaAtual, AuthenticationTypes.Secure);
if (ADcon != null)
{
DirectorySearcher search = new DirectorySearcher(ADcon);
search.Filter = "(SAMAccountName=" + Usuario + ")";
SearchResult result = search.FindOne();
if (result != null)
{
DirectoryEntry userEntry = result.GetDirectoryEntry();
if (userEntry != null)
{
try
{
userEntry.Invoke("ChangePassword", new object[] { SenhaAtual, NovaSenha }, AuthenticationTypes.Secure);
userEntry.Properties["LockOutTime"].Value = 0;
userEntry.CommitChanges();
userEntry.Close();
return true;
}
catch (Exception INex)
{
this.Erro = INex.Message + "COD:\r\n" + INex.InnerException;
userEntry.Close();
return false;
}
}
}
}
return true;
}
catch (Exception ex)
{
this.Erro = ex.Message;
return false;
}
}
First, there will be no difference at runtime if you declare the variables as var or string. Using the var keyword lets the compiler decide what the type is. Because you're assigning a string to it, then it is a string too. In most cases, var is fine. There are only very rare cases when you need to explicitly specify the type.
Second, DirectoryEntry.Invoke is defined like this:
public object Invoke (string methodName, params object[] args);
That may seem like you need to pass an object array, but that is not the case. The params keyword is a way to allow you to pass multiple parameters that get used inside the method as an array. So when you call it like this:
userEntry.Invoke("ChangePassword", new object[] { SenhaAtual, NovaSenha }, AuthenticationTypes.Secure);
The first parameter is an object array and the second parameter is AuthenticationTypes.Secure, then both of those get put inside the args array for use inside the Invoke method. But that is not what ChangePassword looks for. If this doesn't make sense to you, read the documentation for the params keyword and it should help.
When you call .Invoke("ChangePassword", ...), it calls the native Windows IADsUser.ChangePassword method. That takes two parameters: a string with the old password and a string with the new password - not an object array and an AuthenticationTypes value. So you should be calling it like this:
userEntry.Invoke("ChangePassword", SenhaAtual, NovaSenha);
You don't need to worry about the authentication because the password can only be changed over a secure connection. In the documentation, it says it behaves the same way as (IADsUser.SetPassword](https://learn.microsoft.com/en-ca/windows/win32/api/iads/nf-iads-iadsuser-setpassword), where it attempts several different ways to achieve a secure connection for you.
There is another way to change the password if the DirectoryEntry connection is already over a secure connection. A secure connection can either be using Kerberos, which can be done using AuthenticationTypes.Sealing (this is best if you are on the same network as the domain controller):
var ADcon = new DirectoryEntry(LDAP, Usuario, SenhaAtual, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
Or if by using LDAPS (LDAP over SSL), which you can use just by specifying port 636 in the LDAP path (this is the only way if you are not on the same network as the domain controller):
var ADcon = new DirectoryEntry("LDAP://example.com:636", Usuario, SenhaAtual);
If you do that, then you can change the password by updating the unicodePwd attribute directly, in the very specific way it wants it (enclosed in quotes and encoded in UTF-16), like this:
userEntry.Properties["unicodePwd"].Remove(Encoding.Unicode.GetBytes($"\"{SenhaAtual}\""));
userEntry.Properties["unicodePwd"].Add(Encoding.Unicode.GetBytes($"\"{NovaSenha}\""));
This should perform slightly faster since all of the work (changing the password and setting lockOutTime) is done over one network request instead of two.
I'd like to implement a function that searches an ldap server (name, phone number etc.)
Here's what I wrote (the server address is a phony one, but the real one has the same pattern)
DirectoryEntry de = new DirectoryEntry("LDAP://aet7ldap.phony.com")
DirectorySearcher ds = new DirectorySearcher(de);
var test = ds.FindAll();
I know that there are other constructors (string path, string user, string password), but I don't know my username or password and I'm not sure if I need one. So please help me figure out how to do it without these parameters (if that is possible).
I've tried to write a filter as well but that's another thing, because first I need to get my connection right. But can I assume that I have to use these parameters (or column names?) I keep reading everywhere (such as 'gn' for given name and so on)?
sounds like you're trying to connect to Active Directory using the Directory entry method to find a user, you haven't mentioned if you're doing anything to the user you're looking for once they're found, so I'll just give you code that finds the user for you.
using(DirectoryEntry de = new DirectoryEntry("LDAP://servername/DC=phony,DC=com"))
using(DirectorySearcher ds = new DirectorySearcher(de))
{
ds.Filter="(&(objectClass=user)(sAMAccountName="username"))";
//I don't know exactly what criteria you're using to find the user
ds.Filter="(&(objectClass=user)(distinguishedname="")(givenname=""))"
ds.SearchScope = SearchScope.Subtree;
//performing the search and assigning the result to result
SearchResult result = ds.FindOne();
if (result != null)
{
using(DirectoryEntry user = result.GetDirectoryEntry())
{
//put code here to deal with the user as you see fit.
}
lblOutput.Text = "User " + userName + " was found.";
}
}
The Filter is the most important part to find the user you're looking for, the & means and so in the first example I gave in the code above, you're looking for an object with the class user AND the username username. Its easy enough to figure out. Here is a link to a list of all Active Directory attributes, you should be able to find what you're looking for there.
http://www.selfadsi.org/user-attributes.htm
Regards,
Tory Hill
I'm messing about with the Bing Maps WPF control and SOAP services, trying to reverse geocode a point. I tried to implement some code from this project, specifically this block of code:
private string ReverseGeocodePoint(string locationString)
{
string results = "";
ReverseGeocodeRequest reverseGeocodeRequest = new ReverseGeocodeRequest();
// Set the credentials using a valid Bing Maps key
reverseGeocodeRequest.Credentials = new GeocodeService.Credentials();
reverseGeocodeRequest.Credentials.ApplicationId = key;
// Set the point to use to find a matching address
GeocodeService.Location point = new GeocodeService.Location();
string[] digits = locationString.Split(';');
point.Latitude = double.Parse(digits[0].Trim());
point.Longitude = double.Parse(digits[1].Trim());
reverseGeocodeRequest.Location = point;
// Make the reverse geocode request
GeocodeServiceClient geocodeService = new GeocodeServiceClient();
GeocodeResponse geocodeResponse = geocodeService.ReverseGeocode(reverseGeocodeRequest);
if (geocodeResponse.Results.Length > 0)
results = geocodeResponse.Results[0].DisplayName;
else
results = "No Results found";
return results;
}
However, when I try to implement it, I'm being told: Error: The type or namespace name 'Credentials' does not exist in the namespace 'GeocodeTest.GeocodeService' (are you missing an assembly reference?) This error occurs on the line:
reverseGeocodeRequest.Credentials = new GeocodeService.Credentials();
Curious, I decided to take a look at the service reference through the object browser. Apparently, my GeocodeService doesn't even have a Credentials member. So now the question is:
Should I add the member myself? If so, how would I do this?
If not, what alternatives to do have in terms of providing credentials?
The Credentials namespace/type doesn't exist for the service reference GeocodeText.GeocodeService. Instead, you have to use the Credentials from the Bing Maps WPF Control. The statement would look like so:
reverseGeocodeRequest.Credentials = new Microsoft.Maps.MapControl.WPF.Credentials();
Im wondering how to get a list of all computers / machines / pc from active directory?
(Trying to make this page a search engine bait, will reply myself. If someone has a better reply il accept that )
If you have a very big domain, or your domain has limits configured on how how many items can be returned per search, you might have to use paging.
using System.DirectoryServices; //add to references
public static List<string> GetComputers()
{
List<string> ComputerNames = new List<string>();
DirectoryEntry entry = new DirectoryEntry("LDAP://YourActiveDirectoryDomain.no");
DirectorySearcher mySearcher = new DirectorySearcher(entry);
mySearcher.Filter = ("(objectClass=computer)");
mySearcher.SizeLimit = int.MaxValue;
mySearcher.PageSize = int.MaxValue;
foreach(SearchResult resEnt in mySearcher.FindAll())
{
//"CN=SGSVG007DC"
string ComputerName = resEnt.GetDirectoryEntry().Name;
if (ComputerName.StartsWith("CN="))
ComputerName = ComputerName.Remove(0,"CN=".Length);
ComputerNames.Add(ComputerName);
}
mySearcher.Dispose();
entry.Dispose();
return ComputerNames;
}
What EKS suggested is correct, but is performing a little bit slow.
The reason for that is the call to GetDirectoryEntry() on each result. This creates a DirectoryEntry object, which is only needed if you need to modify the active directory (AD) object. It's OK if your query would return a single object, but when listing all object in AD, this greatly degrades performance.
If you only need to query AD, its better to just use the Properties collection of the result object. This will improve performance of the code several times.
This is explained in documentation for SearchResult class:
Instances of the SearchResult class are very similar to instances of
DirectoryEntry class. The crucial difference is that the
DirectoryEntry class retrieves its information from the Active
Directory Domain Services hierarchy each time a new object is
accessed, whereas the data for SearchResult is already available in
the SearchResultCollection, where it gets returned from a query that
is performed with the DirectorySearcher class.
Here is an example on how to use the Properties collection:
public static List<string> GetComputers()
{
List<string> computerNames = new List<string>();
using (DirectoryEntry entry = new DirectoryEntry("LDAP://YourActiveDirectoryDomain.no")) {
using (DirectorySearcher mySearcher = new DirectorySearcher(entry)) {
mySearcher.Filter = ("(objectClass=computer)");
// No size limit, reads all objects
mySearcher.SizeLimit = 0;
// Read data in pages of 250 objects. Make sure this value is below the limit configured in your AD domain (if there is a limit)
mySearcher.PageSize = 250;
// Let searcher know which properties are going to be used, and only load those
mySearcher.PropertiesToLoad.Add("name");
foreach(SearchResult resEnt in mySearcher.FindAll())
{
// Note: Properties can contain multiple values.
if (resEnt.Properties["name"].Count > 0)
{
string computerName = (string)resEnt.Properties["name"][0];
computerNames.Add(computerName);
}
}
}
}
return computerNames;
}
Documentation for SearchResult.Properties
Note that properties can have multiple values, that is why we use Properties["name"].Count to check the number of values.
To improve things even further, use the PropertiesToLoad collection to let the searcher know what properties you are going to use in advance. This allows the searcher to only read the data that is actually going to be used.
Note that the DirectoryEntry and DirectorySearcher objects should
be properly disposed in order to release all resources used. Its best
done with a using clause.
An LDAP query like: (objectCategory=computer) should do the trick.
if you only want to get the enabled computers:
(&(objectclass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
I have read the following properties from AD,
TerminalServicesProfilePath
TerminalServicesHomeDirectory
TerminalServicesHomeDrive
I've tried DirectoryEntry and DirectorySearcher. But they does not include the properties.
I found some example in vbscript and VC to read them.
However I failed to make it working in C#. Am I missing some tricky thing?
EDIT: Am I have to run it on "Windows Server" to make it works? Can it be read from win XP?
I think you can use the InvokeGet method on your DirectoryEntry, passing in the name of the property you want to read.
2008-12-10 11:50 CET — Edited in response to the comment
If I specify a garbage property name, I get the same COM exception. Are you sure the properties you're trying to retrieve are part of the AD schema?
Just to make sure, the code I'm using is as follows:
using (DirectorySearcher searcher = new DirectorySearcher("(cn=Test)"))
{
SearchResult result = searcher.FindOne();
if (result != null)
{
DirectoryEntry entry = result.GetDirectoryEntry();
string s = entry.InvokeGet("TerminalServicesHomeDrive") as string;
MessageBox.Show(s ?? "null");
}
}
I don't remember exactly, but it's something like this:
//user is a DirectoryEntry
IADsTSUserEx adsiUser = (IADsTSUserEx)user.NativeObject;
then you can get the TerminalServices properties you want via adsiUser.
From my experience you're better off developing on a Windows Server with access to AD due to the libraries you use. Then you'll probably make the above work, too :)
This works for me:
DirectoryEntry user = new DirectoryEntry("LDAP://" + sLDAP_SERVER + "/cn=" + SAMAccount + "," + sLdapFullPath, sUser, sPwd);
//ActiveDs.IADsUser iADsUser = (ActiveDs.IADsUser)user.NativeObject;
ActiveDs.IADsUser cont = null;
cont = user.NativeObject as ActiveDs.IADsUser;
TSUSEREXLib.IADsTSUserEx m_TsUser = (TSUSEREXLib.IADsTSUserEx)cont;
int m_TSLogonDisabled = 0;
m_TsUser.AllowLogon = m_TSLogonDisabled;