From 0aecd9b31cbdbd3d8d956a777d28209ca3357743 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Tue, 11 Mar 2025 16:24:15 +1000 Subject: [PATCH 1/2] Refactor Kerberos and make state Runspace Refactors the Kerberos code to new standards used in some of my projects. Also includes the Kerberos error when failing to find the principal for better debugging purposes. Sets the GlobalState to be Runspace specific rather than process wide. This will allow the module to be used in separate runspaces like with ForEach-Object -Parallel or other Runspace based APIs. --- CHANGELOG.md | 1 + .../Commands/OpenADAuthSupport.cs | 2 +- src/PSOpenAD.Module/Commands/OpenADSession.cs | 2 +- src/PSOpenAD.Module/Completer.cs | 4 +- src/PSOpenAD.Module/OnImportAndRemove.cs | 130 +++++--- src/PSOpenAD.Module/OpenADSessionFactory.cs | 28 +- src/PSOpenAD/Authentication.cs | 5 +- src/PSOpenAD/GlobalState.cs | 62 ++++ src/PSOpenAD/Native/GSSAPI.cs | 2 +- src/PSOpenAD/Native/Kerberos.cs | 293 ------------------ src/PSOpenAD/Native/Kerberos/CCClose.cs | 11 + src/PSOpenAD/Native/Kerberos/CCDefault.cs | 49 +++ src/PSOpenAD/Native/Kerberos/FreeContext.cs | 10 + .../Native/Kerberos/FreeDefaultRealm.cs | 25 ++ .../Native/Kerberos/FreeErrorMessage.cs | 11 + src/PSOpenAD/Native/Kerberos/FreePrincipal.cs | 11 + .../Native/Kerberos/FreeUnparsedName.cs | 25 ++ .../Native/Kerberos/GetCCPrincipal.cs | 51 +++ .../Native/Kerberos/GetDefaultRealm.cs | 38 +++ .../Native/Kerberos/GetErrorMessage.cs | 27 ++ src/PSOpenAD/Native/Kerberos/InitContext.cs | 37 +++ .../Native/Kerberos/KerberosException.cs | 25 ++ src/PSOpenAD/Native/Kerberos/UnparseName.cs | 33 ++ src/PSOpenAD/Native/Kerberos/XFree.cs | 10 + src/PSOpenAD/Schema.cs | 2 +- src/PSOpenAD/Session.cs | 51 ++- tests/OpenADSession.Tests.ps1 | 142 +++++++++ tools/lib.sh | 47 +++ 28 files changed, 753 insertions(+), 381 deletions(-) create mode 100644 src/PSOpenAD/GlobalState.cs delete mode 100644 src/PSOpenAD/Native/Kerberos.cs create mode 100644 src/PSOpenAD/Native/Kerberos/CCClose.cs create mode 100644 src/PSOpenAD/Native/Kerberos/CCDefault.cs create mode 100644 src/PSOpenAD/Native/Kerberos/FreeContext.cs create mode 100644 src/PSOpenAD/Native/Kerberos/FreeDefaultRealm.cs create mode 100644 src/PSOpenAD/Native/Kerberos/FreeErrorMessage.cs create mode 100644 src/PSOpenAD/Native/Kerberos/FreePrincipal.cs create mode 100644 src/PSOpenAD/Native/Kerberos/FreeUnparsedName.cs create mode 100644 src/PSOpenAD/Native/Kerberos/GetCCPrincipal.cs create mode 100644 src/PSOpenAD/Native/Kerberos/GetDefaultRealm.cs create mode 100644 src/PSOpenAD/Native/Kerberos/GetErrorMessage.cs create mode 100644 src/PSOpenAD/Native/Kerberos/InitContext.cs create mode 100644 src/PSOpenAD/Native/Kerberos/KerberosException.cs create mode 100644 src/PSOpenAD/Native/Kerberos/UnparseName.cs create mode 100644 src/PSOpenAD/Native/Kerberos/XFree.cs 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..0815366 --- /dev/null +++ b/src/PSOpenAD/Native/Kerberos/UnparseName.cs @@ -0,0 +1,33 @@ +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; + } + + name = Marshal.PtrToStringUTF8(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() { From 34c14369ec030aeeddc89b5c26c690eb948183c7 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 12 Mar 2025 07:26:03 +1000 Subject: [PATCH 2/2] Ensure name is freed after reading --- src/PSOpenAD/Native/Kerberos/UnparseName.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/PSOpenAD/Native/Kerberos/UnparseName.cs b/src/PSOpenAD/Native/Kerberos/UnparseName.cs index 0815366..2f35b94 100644 --- a/src/PSOpenAD/Native/Kerberos/UnparseName.cs +++ b/src/PSOpenAD/Native/Kerberos/UnparseName.cs @@ -27,7 +27,15 @@ public static bool TryUnparseName( return false; } - name = Marshal.PtrToStringUTF8(namePtr) ?? ""; + try + { + name = Marshal.PtrToStringUTF8(namePtr) ?? ""; + } + finally + { + FreeUnparsedName(context, namePtr); + } + return true; } }