-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
System.DirectoryServices.Protocols - Night build preview 6 - Compatibility between wldap32.dll and libldap #36947
Comments
Marking as an enhancement instead since in reality what we want here is to not require domain name on user, which might not be possible given the limitations of the underlying native library. |
I've just spent a couple of days getting my LDAP logic to work from both Windows and Linux when talking to my Windows 2016 Active Directory using the S.DS.P 5.0.0 package. The original thread by brunohbrito was extremely helpful. Since that original thread is locked, I thought I'd post my experience here in case it helps someone else or the .NET 6.0 planning. The internal Linux implementation is much more picky about inputs than the Windows implementation. To get things to work from Linux I had to make several As @brunohbrito mentioned, on Linux I had to put a "MyDomain\" prefix on the userName that I passed to NetworkCredential. It would be nice to see his PR get pulled in. To find that domain component in my Linux implementation I anonymously queried the server's base attributes to get defaultNamingContext and supportedCapabilities. Then if supportedCapabilities contains 1.2.840.113556.1.4.800 (i.e., an Active Directory server), I prefix the userName with the first DC part from the defaultNamingContext. To do user searches from Linux I also had to prefix On Linux I can't set LdapConnection.SessionOptions.VerifyServerCertificate (even to a simple "=>true" handler). I assume this is because LDAP_OPT_SSL isn't supported as @joperezr mentions here. Trying to add a handler caused an "LdapException: The LDAP server returned an unknown error." with ErrorCode = -1. A final problem I had on Linux was that it does not allow extra parentheses in search filters (but Windows does). For example, I was building a filter like It's also worth mentioning that passing in custom attributeList values for SearchRequest attribute filtering works as expected, and it works even when requesting attributes that don't exist. So the concern expressed by joperezr wasn't a problem. |
In the interest of helping others, here's a fairly complete example of code that works on both Windows and Linux for me. Program.csusing System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices.Protocols;
using System.Linq;
using System.Text;
namespace LdapTest
{
internal static class Program
{
// https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/assigning-domain-names
private const string _PrimaryDC = "MyPDC";
private const string _SecondaryDC = "MySDC";
private const string _DnsPrefix = "MyPrefix";
private const string _DnsSuffix = "example";
private const string _DnsTopLevel = "com";
private const string _UserName = "KnownUser";
private const string _Password = "<password>";
private static readonly string _DefaultNamingContext = $"DC={_DnsPrefix},DC={_DnsSuffix},DC={_DnsTopLevel}";
private static readonly string _DnsName = $"{_DnsPrefix}.{_DnsSuffix}.{_DnsTopLevel}";
private static readonly string _PrimaryServer = $"{_PrimaryDC}.{_DnsName}";
private static readonly string _SecondaryServer = $"{_SecondaryDC}.{_DnsName}";
private static void Main()
{
try
{
// Search the base OU to get attributes like defaultNamingContext and supportedCapabilities.
// If supportedCapabilities contains 1.2.840.113556.1.4.800 then we know it's an Active Directory per
// https://www.alvestrand.no/objectid/1.2.840.113556.1.4.800.html.
Search(string.Empty, "objectClass=*", SearchScope.Base, "defaultNamingContext", "dnsHostName", "supportedCapabilities");
// On Windows we can do user searches from the defaultNamingContext's container.
// On Linux we have to target the "CN=Users" container, or the search throws: DirectoryOperationException: An operation error occurred.
// Maybe on Linux SearchScope.Subtree is behaving like SearchScope.OneLevel?
Search(
(OperatingSystem.IsWindows() ? string.Empty : "CN=Users,") + _DefaultNamingContext,
"(&(objectCategory=Person)(objectClass=user)(displayName=*Robert*))",
SearchScope.Subtree,
"cn",
"sAMAccountName",
"objectSid");
}
catch (LdapException ex)
{
Console.WriteLine(ex.ErrorCode);
Console.WriteLine(ex.ServerErrorMessage);
Console.WriteLine(ex.ToString());
}
catch (DirectoryOperationException ex)
{
Console.WriteLine(ex.Response);
Console.WriteLine(ex.ToString());
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
if (Debugger.IsAttached)
{
Console.WriteLine("Press any key to close...");
Console.ReadKey(true);
}
}
private static void Search(string targetOu, string query, SearchScope scope, params string[] attributeList)
{
// On Linux we have to give it a FQDN. On Windows we can give it an empty string, and it will locate a DC.
string serverName = DateTime.UtcNow.Second % 2 == 0 ? _PrimaryServer : _SecondaryServer;
Console.WriteLine($"Using server {serverName}");
using LdapConnection connection = new LdapConnection(serverName);
if (!string.IsNullOrEmpty(targetOu))
{
// On Linux we have to set connection.Credential, or user search requests throw DirectoryOperationException: An operation error occurred.
// On Windows, setting Credential is optional (probably because of implicit NTLM authentication).
// On Linux we have to prefix "_DnsPrefix\" on the user name too, based on the tip from brunohbrito here:
// https://github.com/dotnet/runtime/issues/36888#issuecomment-633220620
string userName = OperatingSystem.IsWindows() ? _UserName : $"{_DnsPrefix}\\{_UserName}";
connection.Credential = new(userName, _Password);
}
connection.Bind();
Console.WriteLine("Bind() was successful.");
const string SeparatorLine = "-------------------------------------------------------";
Console.WriteLine($"Search {scope} of '{targetOu}' for '{query}'");
SearchRequest request = new(targetOu, query, scope, attributeList);
var response = (SearchResponse)connection.SendRequest(request);
int resultCount = response.Entries.Count;
Console.WriteLine($"Found {resultCount} entr{(resultCount == 1 ? "y" : "ies")}.");
Console.WriteLine(SeparatorLine);
const int EntryDisplayLimit = 2;
const int AttributeDisplayLimit = 5;
foreach (SearchResultEntry searchEntry in response.Entries.Cast<SearchResultEntry>().Take(EntryDisplayLimit))
{
foreach (DictionaryEntry attributeEntry in searchEntry.Attributes.Cast<DictionaryEntry>().OrderBy(e => e.Key).Take(AttributeDisplayLimit))
{
DirectoryAttribute attribute = (DirectoryAttribute)attributeEntry.Value!;
// Using IList.this[int] gives us access to the raw value, whereas DirectoryAttribute.this[int] tries to
// blindly convert every byte[] to a string. That returns garbage for values like objectSid. This logic
// is better, but it still converts objectGUID to a garbage string.
IList attributeValues = attribute;
IReadOnlyList<object> values = attributeValues.Cast<object>()
.Select(value => value is byte[] bytes
? (bytes.Length > 0 && !bytes.Any(b => b == 0)
? Encoding.UTF8.GetString(bytes)
: ("0x" + string.Concat(bytes.Select(b => b.ToString("X2")))))
: value)
.Distinct()
.ToList();
Console.WriteLine($"\t{attribute.Name} = {string.Join(" | ", values)}");
}
Console.WriteLine(SeparatorLine);
}
}
}
} LdapTest.csproj<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.DirectoryServices.Protocols" Version="5.0.0" />
</ItemGroup>
</Project> |
Thank you @menees. We should also resolve the error code - #43621 (comment) - LDAP_FILTER_ERROR at least gives a bit more clue. |
Yes, that would be great! Seeing the error name would have clued me in to my filter problem much faster. An LdapErrorCode enum with those values would be nice too, with a property or extension to cast/convert LdapException.ErrorCode to LdapErrorCode. |
@danmosemsft - thank you for posting your solution. I have now the same setup, but this after call: |
@mrybson I actually am not familiar with the directory services code. @joperezr would have to help you.
No console output at all? For managed code debugging, you can try lldb if you are willing to try SOS commands (there is a learning curve): https://github.com/dotnet/runtime/blob/master/docs/workflow/debugging/libraries/unix-instructions.md and https://github.com/dotnet/runtime/blob/master/docs/workflow/debugging/coreclr/debugging.md#debugging-coreclr-on-linux-and-macos For native debugging, apparently gdb works as an alternative. There may also be something in syslog: https://stackoverflow.com/questions/6074362/how-to-check-syslog-in-bash-on-linux |
@danmosemsft, I'm sorry I don't have more time to work on this right now. |
@danmosemsft I just found what was the root cause of the issue. So when running from Linux, you can't use TimeLimit property in the SearchRequest object. When you use it calling SendRequest, app stops. |
@mrybson would you mind opening a new issue for that, with repro code? That would be less confusing. |
Just dropping a note, I closed the linked PR #36949 given it was incomplete and stale. There is a note on that PR on what is missing to get it all the way through in case anybody else is interested in picking it up, just feel free to reopen in that case. |
Description
I'm running the following code snippet both for Windows and Linux Environment.
Under Windows environment it's not necessary to concat domain at username. But when running under Linux environment it's necessary, of course a Invalid Credential will be thrown.
Configuration
.NET 5
Linux Debian 10 - Container runtime:5.0-buster-slim
Other information
I was looking at libldap docs.
bind_s
andsimple_bind_s
actually are the same and just support LDAP_AUTH_SIMPLE.So, why we don't concat domain name in every case (when it's available) for Linux?
My suggestions is, at BindHelper() on LdapConnection.cs line 1099, remove this
if
and put it into Linux / Windows specific logic. And also at Linux do the same domain treatment for every scenarios that Domain name is available.The text was updated successfully, but these errors were encountered: