Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/PSOpenAD.Module/Commands/OpenADAuthSupport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/PSOpenAD.Module/Commands/OpenADSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/PSOpenAD.Module/Completer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public IEnumerable<CompletionResult> CompleteArgument(string commandName, string
wordToComplete = "";

HashSet<Uri> 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)) &&
Expand All @@ -38,7 +38,7 @@ public IEnumerable<CompletionResult> CompleteArgument(string commandName, string

string className = GetClassNameForCommand(commandName);

HashSet<string>? classAttributes = GlobalState.SchemaMetadata?.GetClassAttributesInformation(className);
HashSet<string>? classAttributes = GlobalState.GetFromTLS().SchemaMetadata?.GetClassAttributesInformation(className);
if (classAttributes != null)
{
foreach (string attribute in classAttributes)
Expand Down
130 changes: 92 additions & 38 deletions src/PSOpenAD.Module/OnImportAndRemove.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,23 +90,25 @@
{
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",

Check warning on line 109 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L108-L109

Added lines #L108 - L109 were not covered by tests
true, true, "");
GlobalState.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate,
state.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate,

Check warning on line 111 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L111

Added line #L111 was not covered by tests
"GSS-SPNEGO", true, true, "");

const GetDcFlags getDcFlags = GetDcFlags.DS_IS_DNS_NAME | GetDcFlags.DS_ONLY_LDAP_NEEDED |
Expand All @@ -123,21 +126,21 @@
}
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}";

Check warning on line 129 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L129

Added line #L129 was not covered by tests
}

if (!string.IsNullOrWhiteSpace(dcName))
{
GlobalState.DefaultDC = new($"ldap://{dcName}:389/");
state.DefaultDC = new($"ldap://{dcName}:389/");

Check warning on line 134 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L134

Added line #L134 was not covered by tests
}
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";

Check warning on line 138 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L138

Added line #L138 was not covered by tests
}
}
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
Expand All @@ -151,51 +154,40 @@

if (gssapiLib == null)
{
GlobalState.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos,
state.Providers[AuthenticationMethod.Kerberos] = new(AuthenticationMethod.Kerberos,

Check warning on line 157 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L157

Added line #L157 was not covered by tests
"GSSAPI", false, false, "GSSAPI library not found");
GlobalState.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate,
state.Providers[AuthenticationMethod.Negotiate] = new(AuthenticationMethod.Negotiate,

Check warning on line 159 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L159

Added line #L159 was not covered by tests
"GSS-SPNEGO", false, false, "GSSAPI library not found");

GlobalState.DefaultDCError = "Failed to find GSSAPI library";
state.DefaultDCError = "Failed to find GSSAPI library";

Check warning on line 162 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L162

Added line #L162 was not covered by tests
}
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;

Check warning on line 173 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L173

Added line #L173 was not covered by tests
}
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}";
Expand All @@ -208,37 +200,99 @@
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}";

Check warning on line 212 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L212

Added line #L212 was not covered by tests
}
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}";

Check warning on line 216 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L216

Added line #L216 was not covered by tests
}
}
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";

Check warning on line 226 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L226

Added line #L226 was not covered by tests
}
}
}
}

public void OnRemove(PSModuleInfo module)
{
foreach (OpenADSession session in GlobalState.Sessions)
GlobalState state = GlobalState.GetFromTLS();

Check warning on line 234 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L234

Added line #L234 was not covered by tests
foreach (OpenADSession session in state.Sessions)
session.Close();

GlobalState.Sessions = new();
state.Sessions = new();

Check warning on line 238 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L238

Added line #L238 was not covered by tests
Resolver?.Dispose();
}

/// <summary>
/// Attempt to get the default Kerberos realm from the system for the DC lookup.
/// </summary>
/// <param name="realm">The realm if the method returns true.</param>
/// <param name="errorMessage">The error details if the method returns false.</param>
/// <returns>True if the realm was successfully retrieved, otherwise false.</returns>
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;

Check warning on line 264 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L262-L264

Added lines #L262 - L264 were not covered by tests
}
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;

Check warning on line 287 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L285-L287

Added lines #L285 - L287 were not covered by tests
}
}
else
{
errorMessage = $"{defaultRealmException.Message}, {defaultUnparseException.Message}";
return false;

Check warning on line 293 in src/PSOpenAD.Module/OnImportAndRemove.cs

View check run for this annotation

Codecov / codecov/patch

src/PSOpenAD.Module/OnImportAndRemove.cs#L291-L293

Added lines #L291 - L293 were not covered by tests
}
}
}
}
}
28 changes: 17 additions & 11 deletions src/PSOpenAD.Module/OpenADSessionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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))
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -149,6 +153,7 @@ PSCmdlet cmdlet
}

auth = Authenticate(
state,
connection,
uri,
auth,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -327,6 +332,7 @@ out var authEncrypted
/// <param name="encrypted">Whether the auth context will encrypt the messages.</param>
/// <returns>The authentication method used</returns>
private static AuthenticationMethod Authenticate(
GlobalState state,
IADConnection connection,
Uri uri,
AuthenticationMethod auth,
Expand All @@ -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;
Expand All @@ -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";
Expand Down
Loading