b8d3fa2ba1
Auth-007's sAMAccountName match assumed the search base covers one AD domain; across a forest (Global Catalog) it could accept another domain's same-named account and return its groups. For a u:DOMAIN\name Who-am-I answer the entry must now also be that domain's account: AD's msDS-PrincipalName (objectSid-derived DOMAIN\name, read by a base search through the new ILdapConnection.Read) must equal the answer where offered; otherwise the name must have exactly one holder under SearchBase and it must be the entry found. Three forest cases fail pre-fix; single-domain and sAMAccountName-search compat cases pass both ways. README notes this and Auth-009. (scadaproj#50) Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
71 lines
3.1 KiB
C#
71 lines
3.1 KiB
C#
namespace ZB.MOM.WW.Auth.Ldap.Internal;
|
|
|
|
using System.Net.Security;
|
|
using ZB.MOM.WW.Auth.Abstractions.Ldap;
|
|
|
|
/// <summary>
|
|
/// A single LDAP search result entry: its DN and a flat attribute bag.
|
|
/// </summary>
|
|
internal sealed record LdapSearchEntry(
|
|
string Dn,
|
|
IReadOnlyDictionary<string, IReadOnlyList<string>> Attributes);
|
|
|
|
/// <summary>
|
|
/// Abstraction over a single LDAP connection. Allows unit-testing
|
|
/// <c>LdapAuthService</c> without a live directory server.
|
|
/// </summary>
|
|
internal interface ILdapConnection : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Opens (and optionally upgrades to TLS) a connection to the given host.
|
|
/// </summary>
|
|
/// <param name="host">The LDAP server hostname or IP.</param>
|
|
/// <param name="port">The LDAP server port.</param>
|
|
/// <param name="transport">The transport security mode.</param>
|
|
/// <param name="allowInsecure">
|
|
/// When <see langword="true"/> AND no <paramref name="serverCertificateValidationCallback"/> is
|
|
/// supplied, TLS server-certificate validation is bypassed (dev/test only). Ignored when a
|
|
/// validation callback is supplied (the callback wins) or for plaintext transport.
|
|
/// </param>
|
|
/// <param name="timeoutMs">The connection/operation timeout in milliseconds.</param>
|
|
/// <param name="serverCertificateValidationCallback">
|
|
/// Optional TLS server-certificate validation callback. When <see langword="null"/>, the OS trust
|
|
/// store is used (the client does not blind-accept).
|
|
/// </param>
|
|
void Connect(
|
|
string host,
|
|
int port,
|
|
LdapTransport transport,
|
|
bool allowInsecure,
|
|
int timeoutMs,
|
|
RemoteCertificateValidationCallback? serverCertificateValidationCallback);
|
|
|
|
/// <summary>Binds with the supplied DN and password. Throws <c>LdapException</c> on bad credentials.</summary>
|
|
void Bind(string dn, string password);
|
|
|
|
/// <summary>Executes a subtree search and returns all matching entries.</summary>
|
|
IReadOnlyList<LdapSearchEntry> Search(string searchBase, string filter, IReadOnlyList<string> attributes);
|
|
|
|
/// <summary>
|
|
/// Reads one entry by DN (a base-scope search) and returns the requested attributes the server
|
|
/// sent, or <see langword="null"/> when the entry does not exist. Throws <c>LdapException</c> on
|
|
/// any other failure. Used for constructed attributes such as AD's <c>msDS-PrincipalName</c>,
|
|
/// which a server may compute for a base read but not for a subtree search.
|
|
/// </summary>
|
|
LdapSearchEntry? Read(string dn, IReadOnlyList<string> attributes);
|
|
|
|
/// <summary>
|
|
/// RFC 4532 "Who am I?": the authorization identity the directory associates with the current
|
|
/// bind, as the server sends it (<c>dn:<dn></c>, <c>u:DOMAIN\name</c>, or empty for an
|
|
/// anonymous bind). Throws <c>LdapException</c> when the server does not support the operation
|
|
/// (GLAuth answers protocol error 2).
|
|
/// </summary>
|
|
string WhoAmI();
|
|
}
|
|
|
|
/// <summary>Factory that produces <see cref="ILdapConnection"/> instances.</summary>
|
|
internal interface ILdapConnectionFactory
|
|
{
|
|
ILdapConnection Create();
|
|
}
|