Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Multifactor.Core.Ldap;
using Multifactor.Core.Ldap.Name;
using Multifactor.Radius.Adapter.v2.Application.Core.Models.Abstractions;

namespace Multifactor.Radius.Adapter.v2.Application.Core.Extensions;

public static class LdapGlobalCatalogExtensions
{
private const int GlobalCatalogPort = 3268;
private const int GlobalCatalogSslPort = 3269;
private const int LdapPort = 389;
private const int LdapsPort = 636;

/// <summary>
/// LDAP-сервер считается Global Catalog, если в connection-string указан порт 3268/3269
/// </summary>
public static bool IsGlobalCatalog(this ILdapServerConfiguration config)
{
ArgumentNullException.ThrowIfNull(config);
return IsGlobalCatalogPort(new LdapConnectionString(config.ConnectionString).Port);
}

public static bool IsGlobalCatalogPort(int port) => port is GlobalCatalogPort or GlobalCatalogSslPort;

/// <summary>
/// Извлекает DNS-имя домена (например, "child.test.group") из DN пользователя,
/// найденного через GC (например, "CN=User1,OU=Users,DC=child,DC=test,DC=group").
/// </summary>
public static string ExtractDomainDnsName(this DistinguishedName dn)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется что метод не на своем месте

{
ArgumentNullException.ThrowIfNull(dn);

var parts = dn.StringRepresentation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

лучше dn.Components

.Split(',')
.Select(p => p.Trim())
.Where(p => p.StartsWith("DC=", StringComparison.OrdinalIgnoreCase))
.Select(p => p[3..].Trim());

return string.Join(".", parts);
}

/// <summary>
/// Строит connection-string для bind к контроллеру конкретного домена по его DNS-имени.
/// </summary>
public static string ToDomainControllerConnectionString(this LdapConnectionString globalCatalogConnectionString, string domainDnsName)
{
ArgumentNullException.ThrowIfNull(globalCatalogConnectionString);
ArgumentException.ThrowIfNullOrWhiteSpace(domainDnsName);

var isSsl = globalCatalogConnectionString.Port == GlobalCatalogSslPort

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

кажется это функционал для ldap.core

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Да и почти все остальное

|| globalCatalogConnectionString.Scheme.Equals("ldaps", StringComparison.OrdinalIgnoreCase);

var scheme = isSsl ? "ldaps" : "ldap";
var port = isSsl ? LdapsPort : LdapPort;

return $"{scheme}://{domainDnsName}:{port}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public sealed class RadiusPipelineContext
public IForestMetadata? ForestMetadata { get; set; }
public ILdapProfile? LdapProfile { get; set; }
public string MustChangePasswordDomain { get; set; }

/// <summary>
/// Connection-string контроллера домена (389/636), вычисленный из DN пользователя
/// после поиска через Global Catalog. Если задан — используется для bind вместо
/// <see cref="LdapConfiguration"/>.ConnectionString и вместо эвристики по ForestMetadata.
/// </summary>
public string? ResolvedBindConnectionString { get; set; }
public HashSet<string> UserGroups { get; set; } = [];

public RadiusPacket? ResponsePacket { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using Microsoft.Extensions.Logging;
using Multifactor.Core.Ldap;
using Multifactor.Core.Ldap.Name;
using Multifactor.Radius.Adapter.v2.Application.Core.Enum;
using Multifactor.Radius.Adapter.v2.Application.Core.Models;
using Multifactor.Radius.Adapter.v2.Application.Features.PacketHandler.UseCases.FirstFactor.Models;
using Multifactor.Radius.Adapter.v2.Application.Features.PacketHandler.UseCases.FirstFactor.Ports;
using Multifactor.Radius.Adapter.v2.Application.Features.PacketHandler.UseCases.LoadLdapForest.Models;
using System.Diagnostics;
using System.DirectoryServices.Protocols;
using System.Text.RegularExpressions;

Expand Down Expand Up @@ -63,8 +61,26 @@ public Task Execute(RadiusPipelineContext context)
var userIdentity = new UserIdentity(context.RequestPacket.UserName);
var domain = context.ForestMetadata?.DetermineForestDomain(userIdentity);
var formatted = LdapBindNameFormatter.FormatName(context.RequestPacket.UserName!, context.LdapProfile!);
var connectionString = domain?.ConnectionString ?? context.LdapConfiguration!.ConnectionString;
var isValid = ValidateUserCredentials(context, formatted, passphrase.Password, connectionString, context.LdapConfiguration.BindTimeoutSeconds);

// Приоритет источников connection-string для bind:
// 1. Домен, вычисленный из DN пользователя после поиска через Global Catalog;
// 2. Домен, определённый эвристикой по UPN/NetBIOS (механизм trusted domains);
// 3. Connection-string текущего LdapServer-блока.
var connectionString = context.ResolvedBindConnectionString
?? domain?.ConnectionString
?? context.LdapConfiguration!.ConnectionString;

var bindStopwatch = Stopwatch.StartNew();
var isValid = ValidateUserCredentials(context,
formatted,
passphrase.Password,
connectionString,
context.LdapConfiguration.BindTimeoutSeconds);
bindStopwatch.Stop();
_logger.LogInformation(
"LDAP bind for user '{user:l}' to '{ldapUri:l}' took {ElapsedMs} ms. Success: {Success}",
radiusPacket.UserName, connectionString, bindStopwatch.ElapsedMilliseconds, isValid);


if (!isValid)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
using Multifactor.Radius.Adapter.v2.Application.Core.Models.Abstractions;
using System.DirectoryServices.Protocols;
using Multifactor.Radius.Adapter.v2.Application.Core.Models.Abstractions;
using Multifactor.Radius.Adapter.v2.Application.Features.PacketHandler.UseCases.LoadProfile.Models;

namespace Multifactor.Radius.Adapter.v2.Application.Features.PacketHandler.UseCases.LoadProfile.Ports;

public interface IProfileSearch
{
ILdapProfile? Execute(FindUserDto request);

/// <summary>
/// Возвращает ВСЕ записи, подошедшие под фильтр поиска, а не только первую.
/// Нужен при поиске через Global Catalog, где sAMAccountName формально уникален
/// только в рамках домена (не леса).
/// </summary>
IReadOnlyList<ILdapProfile> ExecuteMany(FindUserDto request);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не надо так
Пиши тонкие порты
Выделим отдельный интерфейс


/// <summary>
/// Резолвит NetBIOS-имя домена в его DNS-имя
/// Нужен, чтобы явно указанный пользователем домен в DOMAIN\user не терялся при поиске
/// через Global Catalog. Возвращает null, если сопоставление не найдено.
/// </summary>
string? ResolveDomainDnsNameByNetBiosName(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не имеет отношение к интерфейсу
Скорее к UserIdentity

string connectionString,
AuthType authType,
string userName,
string password,
int bindTimeoutInSeconds,
string netBiosName);
}

Loading