diff --git a/CHANGELOG.md b/CHANGELOG.md index 755f8c3..94fc2cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ + Ensure a failure in a DNS lookup does not stop the module from importing but only errors when the value is used. + Use a case insensitive lookup for requested properties and the returned LDAP attributes + Add fallback for Linux/macOS default realm lookup to use the ccache principal realm if present ++ Properly store AD sessions in a Runspace specific storage allowing multiple runspaces to run in parallel without affecting each other ## v0.5.0 - 2024-03-21 diff --git a/src/PSOpenAD.Module/Commands/OpenADAuthSupport.cs b/src/PSOpenAD.Module/Commands/OpenADAuthSupport.cs index 14567da..a836f20 100644 --- a/src/PSOpenAD.Module/Commands/OpenADAuthSupport.cs +++ b/src/PSOpenAD.Module/Commands/OpenADAuthSupport.cs @@ -10,7 +10,7 @@ public class GetOpenADAuthSupport : PSCmdlet { protected override void EndProcessing() { - foreach (AuthenticationProvider provider in GlobalState.Providers.Values) + foreach (AuthenticationProvider provider in GlobalState.GetFromTLS().Providers.Values) WriteObject(provider); } } diff --git a/src/PSOpenAD.Module/Commands/OpenADSession.cs b/src/PSOpenAD.Module/Commands/OpenADSession.cs index 24c174b..ac25fbe 100644 --- a/src/PSOpenAD.Module/Commands/OpenADSession.cs +++ b/src/PSOpenAD.Module/Commands/OpenADSession.cs @@ -13,7 +13,7 @@ protected override void EndProcessing() { // Ensure the sessions are their own collection to avoid something further down the line mutating the same // list during an enumeration, e.g. 'Get-OpenADSession | Remove-OpenADSession' - WriteObject(GlobalState.Sessions.ToArray(), true); + WriteObject(GlobalState.GetFromTLS().Sessions.ToArray(), true); } } diff --git a/src/PSOpenAD.Module/Completer.cs b/src/PSOpenAD.Module/Completer.cs index 2718c92..e5007a9 100644 --- a/src/PSOpenAD.Module/Completer.cs +++ b/src/PSOpenAD.Module/Completer.cs @@ -16,7 +16,7 @@ public IEnumerable CompleteArgument(string commandName, string wordToComplete = ""; HashSet emitted = new(); - foreach (OpenADSession session in GlobalState.Sessions) + foreach (OpenADSession session in GlobalState.GetFromTLS().Sessions) { if ((session.Uri.ToString().StartsWith(wordToComplete, true, CultureInfo.InvariantCulture) || session.Uri.Host.StartsWith(wordToComplete, true, CultureInfo.InvariantCulture)) && @@ -38,7 +38,7 @@ public IEnumerable CompleteArgument(string commandName, string string className = GetClassNameForCommand(commandName); - HashSet? classAttributes = GlobalState.SchemaMetadata?.GetClassAttributesInformation(className); + HashSet? classAttributes = GlobalState.GetFromTLS().SchemaMetadata?.GetClassAttributesInformation(className); if (classAttributes != null) { foreach (string attribute in classAttributes) diff --git a/src/PSOpenAD.Module/OnImportAndRemove.cs b/src/PSOpenAD.Module/OnImportAndRemove.cs index bfc5292..ccb698c 100644 --- a/src/PSOpenAD.Module/OnImportAndRemove.cs +++ b/src/PSOpenAD.Module/OnImportAndRemove.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Management.Automation; using System.Reflection; @@ -89,23 +90,25 @@ public void OnImport() { Resolver = new NativeResolver(); + GlobalState state = GlobalState.GetFromTLS(); + // While channel binding isn't technically done by both these methods an Active Directory implementation // doesn't validate it's presence so from the purpose of a client it does work even if it's enforced on the // server end. - GlobalState.Providers[AuthenticationMethod.Anonymous] = new(AuthenticationMethod.Anonymous, "ANONYMOUS", + state.Providers[AuthenticationMethod.Anonymous] = new(AuthenticationMethod.Anonymous, "ANONYMOUS", true, false, ""); - GlobalState.Providers[AuthenticationMethod.Simple] = new(AuthenticationMethod.Simple, "PLAIN", true, + state.Providers[AuthenticationMethod.Simple] = new(AuthenticationMethod.Simple, "PLAIN", true, false, ""); - GlobalState.Providers[AuthenticationMethod.Certificate] = new(AuthenticationMethod.Certificate, "EXTERNAL", + state.Providers[AuthenticationMethod.Certificate] = new(AuthenticationMethod.Certificate, "EXTERNAL", true, true, ""); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // Windows always has SSPI available. - GlobalState.GssapiProvider = GssapiProvider.SSPI; - GlobalState.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos, "GSSAPI", + state.GssapiProvider = GssapiProvider.SSPI; + state.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos, "GSSAPI", true, true, ""); - GlobalState.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate, + state.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate, "GSS-SPNEGO", true, true, ""); const GetDcFlags getDcFlags = GetDcFlags.DS_IS_DNS_NAME | GetDcFlags.DS_ONLY_LDAP_NEEDED | @@ -123,21 +126,21 @@ public void OnImport() } catch (Exception e) { - GlobalState.DefaultDCError = $"Failure calling DsGetDcName to get default DC: {e.Message}"; + state.DefaultDCError = $"Failure calling DsGetDcName to get default DC: {e.Message}"; } if (!string.IsNullOrWhiteSpace(dcName)) { - GlobalState.DefaultDC = new($"ldap://{dcName}:389/"); + state.DefaultDC = new($"ldap://{dcName}:389/"); } - else if (string.IsNullOrEmpty(GlobalState.DefaultDCError)) + else if (string.IsNullOrEmpty(state.DefaultDCError)) { - GlobalState.DefaultDCError = "No configured default DC on host"; + state.DefaultDCError = "No configured default DC on host"; } } else { - GlobalState.GssapiProvider = GssapiProvider.None; + state.GssapiProvider = GssapiProvider.None; LibraryInfo? gssapiLib = Resolver.CacheLibrary(GSSAPI.LIB_GSSAPI, new[] { MACOS_GSS_FRAMEWORK, // macOS GSS Framework (technically Heimdal) "libgssapi_krb5.so.2", // MIT krb5 @@ -151,51 +154,40 @@ public void OnImport() if (gssapiLib == null) { - GlobalState.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos, + state.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos, "GSSAPI", false, false, "GSSAPI library not found"); - GlobalState.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate, + state.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate, "GSS-SPNEGO", false, false, "GSSAPI library not found"); - GlobalState.DefaultDCError = "Failed to find GSSAPI library"; + state.DefaultDCError = "Failed to find GSSAPI library"; } else { - GlobalState.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos, + state.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos, "GSSAPI", true, true, ""); - GlobalState.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate, + state.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate, "GSS-SPNEGO", true, true, ""); if (gssapiLib.Path == MACOS_GSS_FRAMEWORK) { - GlobalState.GssapiProvider = GssapiProvider.GSSFramework; + state.GssapiProvider = GssapiProvider.GSSFramework; } else if (NativeLibrary.TryGetExport(gssapiLib.Handle, "krb5_xfree", out var _)) { // While technically exported by the krb5 lib the Heimdal GSSAPI lib depends on it so the same // symbol will be exported there and we can use that to detect if Heimdal is in use. - GlobalState.GssapiProvider = GssapiProvider.Heimdal; + state.GssapiProvider = GssapiProvider.Heimdal; } else { - GlobalState.GssapiProvider = GssapiProvider.MIT; + state.GssapiProvider = GssapiProvider.MIT; } // If the krb5 API is available, attempt to get the default realm used when creating an implicit // session. if (krb5Lib != null) { - string defaultRealm = ""; - using SafeKrb5Context ctx = Kerberos.InitContext(); - try - { - defaultRealm = Kerberos.GetDefaultRealm(ctx); - } - catch (KerberosException e) - { - GlobalState.DefaultDCError = $"Failed to lookup krb5 default_realm: {e.Message}"; - } - - if (!string.IsNullOrWhiteSpace(defaultRealm)) + if (TryGetDefaultKerberosRealm(out var defaultRealm, out var realmException)) { // _ldap._tcp.dc._msdcs.domain.com string baseDomain = $"dc._msdcs.{defaultRealm}"; @@ -208,26 +200,30 @@ public void OnImport() ServiceHostEntry? first = res.OrderBy(r => r.Priority).ThenBy(r => r.Weight).FirstOrDefault(); if (first != null) { - GlobalState.DefaultDC = new($"ldap://{first.HostName}:{first.Port}/"); + state.DefaultDC = new($"ldap://{first.HostName}:{first.Port}/"); } else { - GlobalState.DefaultDCError = $"No SRV records for _ldap._tcp.{baseDomain} found"; + state.DefaultDCError = $"No SRV records for _ldap._tcp.{baseDomain} found"; } } catch (DnsResponseException e) { - GlobalState.DefaultDCError = $"DNS Error looking up SRV records for _ldap._tcp.{baseDomain}: {e.Message}"; + state.DefaultDCError = $"DNS Error looking up SRV records for _ldap._tcp.{baseDomain}: {e.Message}"; } catch (Exception e) { - GlobalState.DefaultDCError = $"Unknown error looking up SRV records for _ldap._tcp.{baseDomain}: {e.GetType().Name} - {e.Message}"; + state.DefaultDCError = $"Unknown error looking up SRV records for _ldap._tcp.{baseDomain}: {e.GetType().Name} - {e.Message}"; } } + else + { + state.DefaultDCError = $"Failed to lookup krb5 default realm: {realmException}"; + } } else { - GlobalState.DefaultDCError = "Failed to find Kerberos library"; + state.DefaultDCError = "Failed to find Kerberos library"; } } } @@ -235,10 +231,68 @@ public void OnImport() public void OnRemove(PSModuleInfo module) { - foreach (OpenADSession session in GlobalState.Sessions) + GlobalState state = GlobalState.GetFromTLS(); + foreach (OpenADSession session in state.Sessions) session.Close(); - GlobalState.Sessions = new(); + state.Sessions = new(); Resolver?.Dispose(); } + + /// + /// Attempt to get the default Kerberos realm from the system for the DC lookup. + /// + /// The realm if the method returns true. + /// The error details if the method returns false. + /// True if the realm was successfully retrieved, otherwise false. + private static bool TryGetDefaultKerberosRealm( + [NotNullWhen(true)] out string? realm, + [NotNullWhen(false)] out string? errorMessage) + { + realm = null; + errorMessage = null; + + using var ctx = Kerberos.InitContext(); + if (Kerberos.TryGetDefaultRealm(ctx, out realm, out var defaultRealmException)) + { + return true; + } + + if (!Kerberos.TryGetDefaultCCache(ctx, out var ccache, out var defaultCCException)) + { + errorMessage = $"{defaultRealmException.Message}, {defaultCCException.Message}"; + return false; + } + using (ccache) + { + if (!Kerberos.TryGetCCachePrincipal(ctx, ccache, out var principal, out var defaultCCPrincipalException)) + { + errorMessage = $"{defaultRealmException.Message}, {defaultCCPrincipalException.Message}"; + return false; + } + + using (principal) + { + if (Kerberos.TryUnparseName(ctx, principal, out var principalName, out var defaultUnparseException)) + { + int realmIdx = principalName.IndexOf('@'); + if (realmIdx != -1) + { + realm = principalName[(realmIdx + 1)..]; + return true; + } + else + { + errorMessage = $"{defaultRealmException.Message}, failed to find principal realm in name '{principalName}'"; + return false; + } + } + else + { + errorMessage = $"{defaultRealmException.Message}, {defaultUnparseException.Message}"; + return false; + } + } + } + } } diff --git a/src/PSOpenAD.Module/OpenADSessionFactory.cs b/src/PSOpenAD.Module/OpenADSessionFactory.cs index 82d0b18..47ec86e 100644 --- a/src/PSOpenAD.Module/OpenADSessionFactory.cs +++ b/src/PSOpenAD.Module/OpenADSessionFactory.cs @@ -23,14 +23,16 @@ internal sealed class OpenADSessionFactory PSCmdlet cmdlet, bool skipCache = false) { Uri ldapUri; + GlobalState state = GlobalState.GetFromTLS(); + if (string.IsNullOrEmpty(server)) { - if (GlobalState.DefaultDC == null) + if (state.DefaultDC == null) { string msg = "Cannot determine default realm for implicit domain controller."; - if (!string.IsNullOrEmpty(GlobalState.DefaultDCError)) + if (!string.IsNullOrEmpty(state.DefaultDCError)) { - msg += $" {GlobalState.DefaultDCError}"; + msg += $" {state.DefaultDCError}"; } cmdlet.WriteError(new ErrorRecord( new ArgumentException(msg), @@ -40,7 +42,7 @@ internal sealed class OpenADSessionFactory return null; } - ldapUri = GlobalState.DefaultDC; + ldapUri = state.DefaultDC; } else if (server.StartsWith("ldap://", true, CultureInfo.InvariantCulture) || server.StartsWith("ldaps://", true, CultureInfo.InvariantCulture)) @@ -74,14 +76,14 @@ internal sealed class OpenADSessionFactory OpenADSession? session = null; if (!skipCache) { - session = GlobalState.Sessions.Find(s => s.Uri == ldapUri); + session = state.Sessions.Find(s => s.Uri == ldapUri); } if (session == null) { try { - return Create(ldapUri, credential, auth, startTls, sessionOptions, cancelToken, cmdlet: cmdlet); + return Create(ldapUri, credential, auth, startTls, sessionOptions, cancelToken, cmdlet: cmdlet, state: state); } catch (LDAPException e) { @@ -112,9 +114,11 @@ internal static OpenADSession Create( bool startTls, OpenADSessionOptions sessionOptions, CancellationToken cancelToken, - PSCmdlet cmdlet - ) + PSCmdlet cmdlet, + GlobalState? state = null) { + state ??= GlobalState.GetFromTLS(); + if (auth == AuthenticationMethod.Certificate && sessionOptions.ClientCertificate is null) { throw new ArgumentException( @@ -149,6 +153,7 @@ PSCmdlet cmdlet } auth = Authenticate( + state, connection, uri, auth, @@ -200,7 +205,7 @@ out var authEncrypted // the required PowerShell type. SchemaMetadata schema = QuerySchema(connection, subschemaSubentry, sessionOptions, cancelToken, cmdlet); - return new OpenADSession(connection, uri, auth, transportIsTls || authSigned, + return new OpenADSession(state, connection, uri, auth, transportIsTls || authSigned, transportIsTls || authEncrypted, sessionOptions.OperationTimeout, defaultNamingContext, schema, supportedControls, dnsHostName); } @@ -327,6 +332,7 @@ out var authEncrypted /// Whether the auth context will encrypt the messages. /// The authentication method used private static AuthenticationMethod Authenticate( + GlobalState state, IADConnection connection, Uri uri, AuthenticationMethod auth, @@ -350,7 +356,7 @@ out bool encrypted // Use Certificate if a client certificate is specified, otherwise favour Negotiate auth if it is // available. Otherwise use Simple if both a credential and the exchange would be encrypted. If all else // fails use an anonymous bind. - AuthenticationProvider nego = GlobalState.Providers[AuthenticationMethod.Negotiate]; + AuthenticationProvider nego = state.Providers[AuthenticationMethod.Negotiate]; if (sessionOptions.ClientCertificate is not null && transportIsTls) { auth = AuthenticationMethod.Certificate; @@ -371,7 +377,7 @@ out bool encrypted cmdlet.WriteVerbose($"Default authentication mechanism has been set to {auth}"); } - AuthenticationProvider selectedAuth = GlobalState.Providers[auth]; + AuthenticationProvider selectedAuth = state.Providers[auth]; if (!selectedAuth.Available) { string msg = $"Authentication {selectedAuth.Method} is not available"; diff --git a/src/PSOpenAD/Authentication.cs b/src/PSOpenAD/Authentication.cs index 75e8b1d..14ec2e5 100644 --- a/src/PSOpenAD/Authentication.cs +++ b/src/PSOpenAD/Authentication.cs @@ -139,7 +139,8 @@ public GssapiContext(string? username, string? password, AuthenticationMethod me _mech = method == AuthenticationMethod.Negotiate ? GSSAPI.SPNEGO : GSSAPI.KERBEROS; _targetSpn = GSSAPI.ImportName(target, GSSAPI.GSS_C_NT_HOSTBASED_SERVICE); - bool isHeimdal = GlobalState.GssapiProvider != GssapiProvider.MIT; + GlobalState state = GlobalState.GetFromTLS(); + bool isHeimdal = state.GssapiProvider != GssapiProvider.MIT; List mechList = new() { _mech }; if (isHeimdal && method == AuthenticationMethod.Negotiate) { @@ -152,7 +153,7 @@ public GssapiContext(string? username, string? password, AuthenticationMethod me _credential = GSSAPI.AcquireCredWithPassword(name, password, 0, mechList, GssapiCredUsage.GSS_C_INITIATE).Creds; - if (GlobalState.GssapiProvider != GssapiProvider.MIT) + if (state.GssapiProvider != GssapiProvider.MIT) { _mech = null; } diff --git a/src/PSOpenAD/GlobalState.cs b/src/PSOpenAD/GlobalState.cs new file mode 100644 index 0000000..f6344bf --- /dev/null +++ b/src/PSOpenAD/GlobalState.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation.Runspaces; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace PSOpenAD; + +internal class RunspaceSpecificStorage +{ + private readonly ConditionalWeakTable> _map = []; + + private readonly Func _factory; + + private readonly LazyThreadSafetyMode _mode = LazyThreadSafetyMode.ExecutionAndPublication; + + public RunspaceSpecificStorage(Func factory) + { + _factory = factory; + } + + public T GetFromTLS() + => GetForRunspace(Runspace.DefaultRunspace); + + public T GetForRunspace(Runspace runspace) + { + return _map.GetValue( + runspace, + _ => new Lazy(() => _factory(), _mode)) + .Value; + } +} + +internal class GlobalState +{ + private static readonly RunspaceSpecificStorage _state = new(() => new()); + + private GlobalState() { } + + /// Client authentication provider details. + public Dictionary Providers = []; + + /// List of sessions that have been opened by the client. + public List Sessions = []; + + /// Keeps the current session count used to uniquely identify each new session. + public int SessionCounter = 1; + + /// Information about LDAP classes and their attributes. + public SchemaMetadata? SchemaMetadata; + + /// The GSSAPI/SSPI provider that is used. + public GssapiProvider GssapiProvider; + + /// The default domain controller hostname to use when none was provided. + public Uri? DefaultDC; + + /// If the default DC couldn't be detected this stores the details. + public string? DefaultDCError; + + public static GlobalState GetFromTLS() => _state.GetFromTLS(); +} diff --git a/src/PSOpenAD/Native/GSSAPI.cs b/src/PSOpenAD/Native/GSSAPI.cs index 0d14df6..8802c11 100644 --- a/src/PSOpenAD/Native/GSSAPI.cs +++ b/src/PSOpenAD/Native/GSSAPI.cs @@ -782,7 +782,7 @@ private static unsafe SafeHandle CreateOIDBuffer(byte* oid, int length) internal static bool IsIntelMacOS() { // macOS on x86_64 need to use a specially packed structure when using GSS.Framework. - return GlobalState.GssapiProvider == GssapiProvider.GSSFramework && ( + return GlobalState.GetFromTLS().GssapiProvider == GssapiProvider.GSSFramework && ( RuntimeInformation.ProcessArchitecture == Architecture.X86 || RuntimeInformation.ProcessArchitecture == Architecture.X64 ); diff --git a/src/PSOpenAD/Native/Kerberos.cs b/src/PSOpenAD/Native/Kerberos.cs deleted file mode 100644 index e52a859..0000000 --- a/src/PSOpenAD/Native/Kerberos.cs +++ /dev/null @@ -1,293 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using System.Security.Authentication; - -namespace PSOpenAD.Native; - -internal static class Kerberos -{ - public const string LIB_KRB5 = "PSOpenAD.libkrb5"; - - [DllImport(LIB_KRB5)] - public static extern int krb5_cc_default( - SafeKrb5Context context, - out nint ccache); - - [DllImport(LIB_KRB5)] - public static extern int krb5_cc_close( - SafeKrb5Context context, - nint ccache); - - [DllImport(LIB_KRB5)] - public static extern int krb5_cc_get_principal( - SafeKrb5Context context, - SafeKrb5CCache ccache, - out nint principal); - - [DllImport(LIB_KRB5)] - public static extern void krb5_free_default_realm( - SafeKrb5Context context, - IntPtr lrealm); - - [DllImport(LIB_KRB5)] - public static extern void krb5_free_context( - IntPtr context); - - [DllImport(LIB_KRB5)] - public static extern void krb5_free_error_message( - SafeKrb5Context ctx, - IntPtr msg); - - [DllImport(LIB_KRB5)] - public static extern void krb5_free_principal( - SafeKrb5Context context, - nint principal); - - [DllImport(LIB_KRB5)] - public static extern void krb5_free_unparsed_name( - SafeKrb5Context context, - nint name); - - [DllImport(LIB_KRB5)] - public static extern int krb5_get_default_realm( - SafeKrb5Context context, - out nint lrealm); - - [DllImport(LIB_KRB5)] - public static extern int krb5_unparse_name( - SafeKrb5Context context, - SafeKrb5Principal principal, - out nint name); - - [DllImport(LIB_KRB5)] - public static extern SafeKrb5ErrorMessage krb5_get_error_message( - SafeKrb5Context ctx, - int code); - - [DllImport(LIB_KRB5)] - public static extern int krb5_init_context( - out SafeKrb5Context context); - - [DllImport(LIB_KRB5)] - public static extern void krb5_xfree( - IntPtr ptr); - - /// Get the default realm of the Kerberos context. - /// - /// The API first tries to look up the default realm configured in the krb5.conf file of the environment. If - /// the file does not exist or does not contain a default_realm entry then it will attempt to extract the - /// default realm from the default principal. If that fails then a KerberosException is thrown. - /// - /// The Kerberos context handle. - /// The default realm. - /// Kerberos error reported, most likely no realm was found. - /// krb5_get_default_realm - public static string GetDefaultRealm(SafeKrb5Context context) - { - int res = krb5_get_default_realm(context, out nint realmHandle); - if (res == 0) - { - using SafeKrb5Realm realm = new(context, realmHandle); - return realm.ToString(); - } - else if (TryGetDefaultPrincipalRealm(context, out string? principalRealm)) - { - return principalRealm; - } - else - { - throw new KerberosException(context, res, "krb5_get_default_realm"); - } - } - - /// Create Kerberos Context. - /// The Kerberos context handle. - /// krb5_init_context - public static SafeKrb5Context InitContext() - { - krb5_init_context(out var ctx); - return ctx; - } - - private static bool TryGetDefaultPrincipalRealm( - SafeKrb5Context context, - [NotNullWhen(true)] out string? realm) - { - realm = null; - - int res = krb5_cc_default(context, out nint ccacheHandle); - if (res != 0) - { - return false; - } - - using SafeKrb5CCache ccache = new(context, ccacheHandle); - res = krb5_cc_get_principal(context, ccache, out nint principalHandle); - if (res != 0) - { - return false; - } - - using SafeKrb5Principal principal = new(context, principalHandle); - res = krb5_unparse_name(context, principal, out nint nameHandle); - if (res != 0) - { - return false; - } - - try - { - string? principalName = Marshal.PtrToStringUTF8(nameHandle); - int? index = principalName?.IndexOf('@'); - if (index >= 0) - { - realm = principalName!.Substring(index.Value + 1); - return true; - } - } - finally - { - if (GlobalState.GssapiProvider == GssapiProvider.MIT) - { - krb5_free_unparsed_name(context, nameHandle); - } - else - { - krb5_xfree(nameHandle); - } - } - - return false; - } -} - -internal class SafeKrb5Context : SafeHandle -{ - internal SafeKrb5Context() : base(IntPtr.Zero, true) { } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - Kerberos.krb5_free_context(handle); - return true; - } -} - -internal class SafeKrb5CCache : SafeHandle -{ - private SafeKrb5Context _context; - - internal SafeKrb5CCache(SafeKrb5Context context, nint ccache) : base(ccache, true) - { - _context = context; - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - Kerberos.krb5_cc_close(_context, handle); - return true; - } -} - -internal class SafeKrb5ErrorMessage : SafeHandle -{ - internal SafeKrb5Context Context = new(); - - internal SafeKrb5ErrorMessage() : base(IntPtr.Zero, true) { } - - public override bool IsInvalid => handle == IntPtr.Zero; - - public override string ToString() - { - return Marshal.PtrToStringUTF8(handle) ?? ""; - } - - protected override bool ReleaseHandle() - { - Kerberos.krb5_free_error_message(Context, handle); - return true; - } -} - -internal class SafeKrb5Principal : SafeHandle -{ - private SafeKrb5Context _context; - - internal SafeKrb5Principal(SafeKrb5Context context, nint principal) : base(principal, true) - { - _context = context; - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - protected override bool ReleaseHandle() - { - Kerberos.krb5_free_principal(_context, handle); - return true; - } -} - -internal class SafeKrb5Realm : SafeHandle -{ - internal SafeKrb5Context _context; - - internal SafeKrb5Realm(SafeKrb5Context context, nint realm) : base(realm, true) - { - _context = context; - } - - public override bool IsInvalid => handle == IntPtr.Zero; - - public override string ToString() - { - return Marshal.PtrToStringUTF8(handle) ?? ""; - } - - protected override bool ReleaseHandle() - { - // Heimdal does not include krb5_free_default_realm and instead uses krb5_xfree. - if (GlobalState.GssapiProvider == GssapiProvider.MIT) - { - Kerberos.krb5_free_default_realm(_context, handle); - } - else - { - Kerberos.krb5_xfree(handle); - } - - return true; - } -} - -public class KerberosException : AuthenticationException -{ - public int ErrorCode { get; } - - public string? ErrorMessage { get; } - - internal KerberosException(SafeKrb5Context context, int error) - : base(GetExceptionMessage(context, error, null, null)) => ErrorCode = error; - - internal KerberosException(SafeKrb5Context context, int error, string method, string? errorMessage = null) - : base(GetExceptionMessage(context, error, method, errorMessage)) - { - ErrorCode = error; - ErrorMessage = errorMessage; - } - - private static string GetExceptionMessage(SafeKrb5Context context, int error, string? method, string? errorMessage) - { - method = String.IsNullOrWhiteSpace(method) ? "Kerberos Call" : method; - using SafeKrb5ErrorMessage krb5Err = Kerberos.krb5_get_error_message(context, error); - - string msg = String.Format("{0} failed ({1}) - {2}", method, error, krb5Err.ToString()); - if (!String.IsNullOrWhiteSpace(errorMessage)) - msg += $" - {errorMessage}"; - - return msg; - } -} diff --git a/src/PSOpenAD/Native/Kerberos/CCClose.cs b/src/PSOpenAD/Native/Kerberos/CCClose.cs new file mode 100644 index 0000000..999df94 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/CCClose.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + public static partial int krb5_cc_close( + SafeKrb5Context context, + nint ccache); +} diff --git a/src/PSOpenAD/Native/Kerberos/CCDefault.cs b/src/PSOpenAD/Native/Kerberos/CCDefault.cs new file mode 100644 index 0000000..a271fe2 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/CCDefault.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial int krb5_cc_default( + SafeKrb5Context context, + out nint ccache); + + public static bool TryGetDefaultCCache( + SafeKrb5Context context, + [NotNullWhen(true)] out SafeKrb5CCache? ccache, + [NotNullWhen(false)] out KerberosException? exception) + { + ccache = null; + exception = null; + + int res = krb5_cc_default(context, out nint ccachePtr); + if (res != 0) + { + exception = KerberosException.Create(context, res, nameof(krb5_cc_default)); + return false; + } + + ccache = new(context, ccachePtr); + return true; + } +} + +internal class SafeKrb5CCache : SafeHandle +{ + private SafeKrb5Context _context; + + internal SafeKrb5CCache(SafeKrb5Context context, nint ccache) : base(ccache, true) + { + _context = context; + } + + public override bool IsInvalid => handle == nint.Zero; + + protected override bool ReleaseHandle() + { + Kerberos.krb5_cc_close(_context, handle); + return true; + } +} diff --git a/src/PSOpenAD/Native/Kerberos/FreeContext.cs b/src/PSOpenAD/Native/Kerberos/FreeContext.cs new file mode 100644 index 0000000..570b0b8 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/FreeContext.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + internal static partial void krb5_free_context( + nint context); +} diff --git a/src/PSOpenAD/Native/Kerberos/FreeDefaultRealm.cs b/src/PSOpenAD/Native/Kerberos/FreeDefaultRealm.cs new file mode 100644 index 0000000..475c81d --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/FreeDefaultRealm.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial void krb5_free_default_realm( + SafeKrb5Context context, + nint lrealm); + + public static void FreeDefaultRealm( + SafeKrb5Context context, + nint realm) + { + if (GlobalState.GetFromTLS().GssapiProvider == GssapiProvider.MIT) + { + krb5_free_default_realm(context, realm); + } + else + { + krb5_xfree(realm); + } + } +} diff --git a/src/PSOpenAD/Native/Kerberos/FreeErrorMessage.cs b/src/PSOpenAD/Native/Kerberos/FreeErrorMessage.cs new file mode 100644 index 0000000..2120cde --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/FreeErrorMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial void krb5_free_error_message( + SafeKrb5Context context, + nint msg); +} diff --git a/src/PSOpenAD/Native/Kerberos/FreePrincipal.cs b/src/PSOpenAD/Native/Kerberos/FreePrincipal.cs new file mode 100644 index 0000000..d12300b --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/FreePrincipal.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + public static partial void krb5_free_principal( + SafeKrb5Context context, + nint principal); +} diff --git a/src/PSOpenAD/Native/Kerberos/FreeUnparsedName.cs b/src/PSOpenAD/Native/Kerberos/FreeUnparsedName.cs new file mode 100644 index 0000000..45986c4 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/FreeUnparsedName.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial void krb5_free_unparsed_name( + SafeKrb5Context context, + nint name); + + public static void FreeUnparsedName( + SafeKrb5Context context, + nint name) + { + if (GlobalState.GetFromTLS().GssapiProvider == GssapiProvider.MIT) + { + krb5_free_unparsed_name(context, name); + } + else + { + krb5_xfree(name); + } + } +} diff --git a/src/PSOpenAD/Native/Kerberos/GetCCPrincipal.cs b/src/PSOpenAD/Native/Kerberos/GetCCPrincipal.cs new file mode 100644 index 0000000..4a402a4 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/GetCCPrincipal.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial int krb5_cc_get_principal( + SafeKrb5Context context, + SafeKrb5CCache ccache, + out nint principal); + + public static bool TryGetCCachePrincipal( + SafeKrb5Context context, + SafeKrb5CCache ccache, + [NotNullWhen(true)] out SafeKrb5Principal? principal, + [NotNullWhen(false)] out KerberosException? exception) + { + principal = null; + exception = null; + + int res = krb5_cc_get_principal(context, ccache, out nint principalPtr); + if (res != 0) + { + exception = KerberosException.Create(context, res, nameof(krb5_cc_get_principal)); + return false; + } + + principal = new(context, principalPtr); + return true; + } +} + +internal class SafeKrb5Principal : SafeHandle +{ + private SafeKrb5Context _context; + + internal SafeKrb5Principal(SafeKrb5Context context, nint principal) : base(principal, true) + { + _context = context; + } + + public override bool IsInvalid => handle == nint.Zero; + + protected override bool ReleaseHandle() + { + Kerberos.krb5_free_principal(_context, handle); + return true; + } +} diff --git a/src/PSOpenAD/Native/Kerberos/GetDefaultRealm.cs b/src/PSOpenAD/Native/Kerberos/GetDefaultRealm.cs new file mode 100644 index 0000000..59d1653 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/GetDefaultRealm.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial int krb5_get_default_realm( + SafeKrb5Context context, + out nint lrealm); + + public static bool TryGetDefaultRealm( + SafeKrb5Context context, + [NotNullWhen(true)] out string? realm, + [NotNullWhen(false)] out KerberosException? exception) + { + realm = null; + exception = null; + + int res = krb5_get_default_realm(context, out nint realmPtr); + if (res != 0) + { + exception = KerberosException.Create(context, res, nameof(krb5_get_default_realm)); + return false; + } + + try + { + realm = Marshal.PtrToStringUTF8(realmPtr) ?? ""; + return true; + } + finally + { + FreeDefaultRealm(context, realmPtr); + } + } +} diff --git a/src/PSOpenAD/Native/Kerberos/GetErrorMessage.cs b/src/PSOpenAD/Native/Kerberos/GetErrorMessage.cs new file mode 100644 index 0000000..58c6a1d --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/GetErrorMessage.cs @@ -0,0 +1,27 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial nint krb5_get_error_message( + SafeKrb5Context context, + int code); + + public static string? GetErrorMessage( + SafeKrb5Context context, + int code) + { + nint msg = krb5_get_error_message(context, code); + + try + { + return Marshal.PtrToStringUTF8(msg); + } + finally + { + krb5_free_error_message(context, msg); + } + } +} diff --git a/src/PSOpenAD/Native/Kerberos/InitContext.cs b/src/PSOpenAD/Native/Kerberos/InitContext.cs new file mode 100644 index 0000000..7b82643 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/InitContext.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + public const string LIB_KRB5 = "PSOpenAD.libkrb5"; + + [LibraryImport(LIB_KRB5)] + private static partial int krb5_init_context( + out nint context); + + public static SafeKrb5Context InitContext() + { + int res = krb5_init_context(out var contextHandle); + SafeKrb5Context context = new(contextHandle); + if (res != 0) + { + throw KerberosException.Create(context, res, nameof(krb5_init_context)); + } + + return context; + } +} + +internal class SafeKrb5Context : SafeHandle +{ + internal SafeKrb5Context(nint context) : base(context, true) { } + + public override bool IsInvalid => handle == nint.Zero; + + protected override bool ReleaseHandle() + { + Kerberos.krb5_free_context(handle); + return true; + } +} diff --git a/src/PSOpenAD/Native/Kerberos/KerberosException.cs b/src/PSOpenAD/Native/Kerberos/KerberosException.cs new file mode 100644 index 0000000..7c4436f --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/KerberosException.cs @@ -0,0 +1,25 @@ +using System.Security.Authentication; + +namespace PSOpenAD.Native; + +internal class KerberosException : AuthenticationException +{ + public int ErrorCode { get; } + + private KerberosException( + string message, + int error) : base(message) + { + ErrorCode = error; + } + + public static KerberosException Create( + SafeKrb5Context context, + int error, + string method) + { + string krb5Err = Kerberos.GetErrorMessage(context, error) ?? "Unknown error"; + string msg = $"{method} failed ({error}) - {krb5Err}"; + return new(msg, error); + } +} diff --git a/src/PSOpenAD/Native/Kerberos/UnparseName.cs b/src/PSOpenAD/Native/Kerberos/UnparseName.cs new file mode 100644 index 0000000..2f35b94 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/UnparseName.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial int krb5_unparse_name( + SafeKrb5Context context, + SafeKrb5Principal principal, + out nint name); + + public static bool TryUnparseName( + SafeKrb5Context context, + SafeKrb5Principal principal, + [NotNullWhen(true)] out string? name, + [NotNullWhen(false)] out KerberosException? exception) + { + name = null; + exception = null; + + int res = krb5_unparse_name(context, principal, out nint namePtr); + if (res != 0) + { + exception = KerberosException.Create(context, res, nameof(krb5_unparse_name)); + return false; + } + + try + { + name = Marshal.PtrToStringUTF8(namePtr) ?? ""; + } + finally + { + FreeUnparsedName(context, namePtr); + } + + return true; + } +} diff --git a/src/PSOpenAD/Native/Kerberos/XFree.cs b/src/PSOpenAD/Native/Kerberos/XFree.cs new file mode 100644 index 0000000..4615e9d --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/XFree.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace PSOpenAD.Native; + +internal partial class Kerberos +{ + [LibraryImport(LIB_KRB5)] + private static partial void krb5_xfree( + nint ptr); +} diff --git a/src/PSOpenAD/Schema.cs b/src/PSOpenAD/Schema.cs index b9414e0..fb4fe4c 100644 --- a/src/PSOpenAD/Schema.cs +++ b/src/PSOpenAD/Schema.cs @@ -298,7 +298,7 @@ public SchemaMetadata( } // Used by argument completors. - GlobalState.SchemaMetadata ??= this; + GlobalState.GetFromTLS().SchemaMetadata ??= this; } public void RegisterTransformer(string attribute, DefaultOverrider.CustomTransform transformer) diff --git a/src/PSOpenAD/Session.cs b/src/PSOpenAD/Session.cs index c5ff8d5..6a1de34 100644 --- a/src/PSOpenAD/Session.cs +++ b/src/PSOpenAD/Session.cs @@ -36,6 +36,8 @@ public sealed class OpenADSessionOptions /// The OpenADSession class used to encapsulate a session with the caller. public sealed class OpenADSession { + private readonly GlobalState _globalState; + /// The unique identifier for the session. public int Id { get; } @@ -79,12 +81,23 @@ public sealed class OpenADSession /// Extended control OIDs supported by the server. internal string[] SupportedControls { get; } - internal OpenADSession(IADConnection connection, Uri uri, AuthenticationMethod auth, bool isSigned, - bool isEncrypted, int operationTimeout, string defaultNamingContext, SchemaMetadata schema, - string[] supportedControls, string dcDnsHostName) + internal OpenADSession( + GlobalState state, + IADConnection connection, + Uri uri, + AuthenticationMethod auth, + bool isSigned, + bool isEncrypted, + int operationTimeout, + string defaultNamingContext, + SchemaMetadata schema, + string[] supportedControls, + string dcDnsHostName) { - Id = GlobalState.SessionCounter; - GlobalState.SessionCounter++; + _globalState = state; + + Id = _globalState.SessionCounter; + _globalState.SessionCounter++; Connection = connection; Uri = uri; @@ -97,7 +110,7 @@ internal OpenADSession(IADConnection connection, Uri uri, AuthenticationMethod a SchemaMetadata = schema; SupportedControls = supportedControls; - GlobalState.Sessions.Add(this); + _globalState.Sessions.Add(this); connection.Session.StateChanged += OnStateChanged; } @@ -111,31 +124,7 @@ private void OnStateChanged(object? sender, SessionState state) { if (state == SessionState.Closed) { - GlobalState.Sessions.Remove(this); + _globalState.Sessions.Remove(this); } } } - -internal static class GlobalState -{ - /// Client authentication provider details. - public static Dictionary Providers = new(); - - /// List of sessions that have been opened by the client. - public static List Sessions = new(); - - /// Keeps the current session count used to uniquely identify each new session. - public static int SessionCounter = 1; - - /// Information about LDAP classes and their attributes. - public static SchemaMetadata? SchemaMetadata; - - /// The GSSAPI/SSPI provider that is used. - public static GssapiProvider GssapiProvider; - - /// The default domain controller hostname to use when none was provided. - public static Uri? DefaultDC; - - /// If the default DC couldn't be detected this stores the details. - public static string? DefaultDCError; -} diff --git a/tests/OpenADSession.Tests.ps1 b/tests/OpenADSession.Tests.ps1 index 34159d9..e481342 100644 --- a/tests/OpenADSession.Tests.ps1 +++ b/tests/OpenADSession.Tests.ps1 @@ -1,6 +1,112 @@ . ([IO.Path]::Combine($PSScriptRoot, 'common.ps1')) Describe "New-OpenADSession over LDAP" -Skip:(-not $PSOpenADSettings.Server) { + BeforeAll { + if (-not $IsWindows) { + Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; + +namespace PSOpenAD.Tests; + +public static class Libc +{ + [DllImport("libc")] + public static extern void setenv(string name, string value); + + [DllImport("libc")] + public static extern void unsetenv(string name); +} +'@ + } + + $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName + $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) + + Function Invoke-WithKrb5Context { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [ScriptBlock] + $ScriptBlock, + + [Parameter()] + [switch] + $ClearCCache, + + [Parameter()] + [string] + $NewRealm, + + [Parameter()] + [string] + $NewHostname + ) + + $origEnv = $env:KRB5_CONFIG + $origCCache = $env:KRB5CCNAME + $ccachePath = $origEnv ?? "/etc/krb5.conf" + + Get-Content -LiteralPath $ccachePath -ErrorAction Stop | + ForEach-Object { + if ($_ -like "*default_realm =*") { + if ($NewRealm) { + "default_realm = $NewRealm" + } + } + else { + $_ + } + } | + Set-Content -LiteralPath "TestDrive:/krb5.test.conf" + $tempKrb5 = (Get-Item -LiteralPath "TestDrive:/krb5.test.conf").FullName + $tempKrb5CCache = "FILE:$(Join-Path ([IO.Path]::GetTempPath()) missing_ccache)" + + $ps = $null + try { + [PSOpenAD.Tests.Libc]::setenv("KRB5_CONFIG", $tempKrb5) + + if ($ClearCCache) { + [PSOpenAD.Tests.Libc]::setenv("KRB5CCNAME", $tempKrb5CCache) + } + if ($NewHostname) { + [PSOpenAD.Tests.Libc]::setenv("_PSOPENAD_MOCK_HOSTNAME", $NewHostname) + } + + $ps = [PowerShell]::Create() + $null = $ps.AddScript('Import-Module -Name $args[0] -Force').AddArgument($manifestPath).AddStatement() + $null = $ps.AddScript('& $args[0].Ast.GetScriptBlock()').AddArgument($ScriptBlock) + + $ps.Invoke() + foreach ($e in $ps.Streams.Error) { + Write-Error -ErrorRecord $e + } + } + finally { + if ($origEnv) { + [PSOpenAD.Tests.Libc]::setenv("KRB5_CONFIG", $origEnv) + } + else { + [PSOpenAD.Tests.Libc]::unsetenv("KRB5_CONFIG") + } + + if ($ClearCCache) { + if ($origCCache) { + [PSOpenAD.Tests.Libc]::setenv("KRB5CCNAME", $origCCache) + } + else { + [PSOpenAD.Tests.Libc]::unsetenv("KRB5CCNAME") + } + } + + if ($NewHostname) { + [PSOpenAD.Tests.Libc]::unsetenv("_PSOPENAD_MOCK_HOSTNAME") + } + + ${ps}?.Dispose() + } + } + } BeforeEach { Get-OpenADSession | Remove-OpenADSession } @@ -16,6 +122,42 @@ Describe "New-OpenADSession over LDAP" -Skip:(-not $PSOpenADSettings.Server) { $sessions -is ([PSOpenAD.OpenADSession]) | Should -BeTrue } + It "Creates a new session using implicit server and ccache fallback" -Skip:( + $IsWindows -or + -not ($PSOpenADSettings.DefaultCredsAvailable -and $PSOpenADSettings.ImplicitServerAvailable) + ) { + Invoke-WithKrb5Context -NewHostName TESTHOST { + $null = Get-OpenADObject -ErrorAction Stop + $sessions = Get-OpenADSession + $sessions.Count | Should -Be 1 + $sessions -is ([PSOpenAD.OpenADSession]) | Should -BeTrue + } + } + + It "Fails to to create a session session with implicit server - missing ccache fallback" -Skip:( + $IsWindows -or + -not ($PSOpenADSettings.DefaultCredsAvailable -and $PSOpenADSettings.ImplicitServerAvailable) + ) { + Invoke-WithKrb5Context -NewHostName TESTHOST -ClearCCache { + $exp = { + $null = Get-OpenADObject -ErrorAction Stop + } | Should -Throw -PassThru + [string]$exp | Should -BeLike "Cannot determine default realm for implicit domain controller. Failed to lookup krb5 default realm: krb5_get_default_realm failed *, krb5_cc_get_principal failed *" + } + } + + It "Fails to to create a session session with implicit server - invalid DNS lookup" -Skip:( + $IsWindows -or + -not ($PSOpenADSettings.DefaultCredsAvailable -and $PSOpenADSettings.ImplicitServerAvailable) + ) { + Invoke-WithKrb5Context -NewHostName TESTHOST -NewRealm FAKE.REALM { + $exp = { + $null = Get-OpenADObject -ErrorAction Stop + } | Should -Throw -PassThru + [string]$exp | Should -Be "Cannot determine default realm for implicit domain controller. No SRV records for _ldap._tcp.dc._msdcs.FAKE.REALM found" + } + } + It "Creates a session using default credentials - " -Skip:(-not $PSOpenADSettings.DefaultCredsAvailable) -TestCases @( @{ AuthType = 'Negotiate' } @{ AuthType = 'Kerberos' } diff --git a/tools/lib.sh b/tools/lib.sh index 71fce27..d0b06e1 100755 --- a/tools/lib.sh +++ b/tools/lib.sh @@ -22,6 +22,7 @@ lib::setup::system_requirements() { lib::setup::system_requirements::el() { dnf install -y \ --nogpgcheck \ + gcc \ epel-release if [ x"${GSSAPI_PROVIDER}" = "xheimdal" ]; then @@ -62,6 +63,52 @@ lib::setup::system_requirements::el() { # Unit tests might run on a different version than the SDK that is installed # this allows it to rull forward to the earliest major version available. export DOTNET_ROLL_FORWARD=Major + + # A test relies on being able to control the hostname returned by gethostname. + # This generates a shim that will return the desired test value before falling + # back to the libc call if unset. + cat > /tmp/gethostname.c << EOF +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +static int (*real_gethostname)(char *, size_t) = NULL; + +int gethostname(char *name, size_t len) { + const char *mock_hostname = getenv("_PSOPENAD_MOCK_HOSTNAME"); + if (mock_hostname) { + size_t mock_len = strlen(mock_hostname); + if (mock_len >= len) { + errno = ENAMETOOLONG; + return -1; + } + + strncpy(name, mock_hostname, len - 1); + name[len - 1] = '\0'; // Ensure null termination + return 0; + } + + if (!real_gethostname) { + real_gethostname = dlsym(RTLD_NEXT, "gethostname"); + if (!real_gethostname) { + fprintf(stderr, "Error loading original gethostname: %s\n", dlerror()); + return -1; + } + } + + return real_gethostname(name, len); +} +EOF + echo "Compiling gethostname shim" + gcc \ + -fPIC -rdynamic -g -Wall -shared -Wl,-soname,libgethostnameshim.so.1 -lc -ldl \ + -o /usr/lib64/libgethostnameshim.so.1 \ + /tmp/gethostname.c + export LD_PRELOAD=/usr/lib64/libgethostnameshim.so.1 } lib::setup::gssapi() {