up
Some checks failed
Build Test Deploy / build-test (push) Has been cancelled
Build Test Deploy / authority-container (push) Has been cancelled
Build Test Deploy / docs (push) Has been cancelled
Build Test Deploy / deploy (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
root
2025-10-10 06:53:40 +00:00
parent 3aed135fb5
commit df5984d07e
1081 changed files with 97764 additions and 61389 deletions

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\__Libraries\Ablera.Serdica.Extensions.Novell.Directory.Ldap\Ablera.Serdica.Extensions.Novell.Directory.Ldap.csproj" />
<ProjectReference Include="..\Ablera.Serdica.Authority.Plugins.Base\Ablera.Serdica.Authority.Plugins.Base.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,345 @@
using System.Diagnostics;
using System.Security.Claims;
using Ablera.Serdica.Authentication.Extensions;
using Ablera.Serdica.Authority.Plugins.Base.Contracts;
using Ablera.Serdica.Authority.Plugins.Base.Models;
using Ablera.Serdica.Common.Tools.Extensions;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Attributes;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Contracts;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Extensions;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Models;
using Microsoft.Extensions.Logging;
using Novell.Directory.Ldap;
namespace Ablera.Serdica.Authority.Plugins.LdapUtilities.Services;
public abstract class LdapIdentityFacadeBase<TLdapIdentity, TKeyType>(
ILogger<LdapIdentityFacadeBase<TLdapIdentity, TKeyType>> logger,
ILdapSettingsProvider ldapSettingsProvider,
IEmailNormalizer emailNormalizer,
IUsernameNormalizer usernameNormalizer)
: IAuthService<TLdapIdentity>,
IUserRepository<TLdapIdentity>,
IClaimStore<TLdapIdentity>,
IPasswordManager<TLdapIdentity>,
IAccountLockManager<TLdapIdentity>
where TLdapIdentity : class, ILdapIdentity<TKeyType>
where TKeyType : IEquatable<TKeyType>
{
#region IAuthService
public virtual async Task<AuthenticationResult> AuthenticateAsync(TLdapIdentity ldapIdentity, string password, bool lockoutOnFailure = false, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(password))
return AuthenticationResult.Fail(AuthenticationCode.EmptyCredentials.ToScreamingSnakeCase());
if (string.IsNullOrWhiteSpace(ldapIdentity.DistinguishedName))
return AuthenticationResult.Fail(AuthenticationCode.AccountIsNotAuthenticaAble.ToScreamingSnakeCase());
try
{
using var conn = new LdapConnection { SecureSocketLayer = ldapIdentity.LdapSettings.Ssl };
await conn.ConnectAsync(ldapIdentity.LdapSettings.Url, ldapIdentity.LdapSettings.Port, ct);
await conn.BindAsync(ldapIdentity.DistinguishedName, password, ct);
if (!conn.Bound)
return AuthenticationResult.Fail(AuthenticationCode.InvalidPassword.ToScreamingSnakeCase());
var baseClaims = await GetBaseClaimsAsync(ldapIdentity).ConfigureAwait(true);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
baseClaims, GetType().Namespace));
return AuthenticationResult.Success(principal);
}
catch (LdapException ex) when (ex.ResultCode == LdapException.InvalidCredentials)
{
return AuthenticationResult.Fail(AuthenticationCode.InvalidPassword.ToScreamingSnakeCase());
}
catch (LdapException ex) when (IsAccountLocked(ex))
{
return AuthenticationResult.Fail(AuthenticationCode.AccountIsLocked.ToScreamingSnakeCase());
}
catch (Exception e)
{
logger.LogError(e, "LDAP auth error for {User}", ldapIdentity.Username);
return AuthenticationResult.Fail(AuthenticationCode.GenericError.ToScreamingSnakeCase());
}
}
#endregion
#region IUserStore
public Task<TLdapIdentity?> FindByIdAsync(string id, CancellationToken ct = default)
=> FindAccountAsync(id, FindAccountType.Id, ct);
public Task<TLdapIdentity?> FindByEmailAsync(string email, CancellationToken ct = default)
=> FindAccountAsync(email, FindAccountType.Email, ct);
public Task<TLdapIdentity?> FindByNameAsync(string username, CancellationToken ct = default)
=> FindAccountAsync(username, FindAccountType.Username, ct);
public async Task<OperationResult> CreateAsync(TLdapIdentity user, string password, CancellationToken ct = default)
{
if (user is null) return OperationResult.Fail("NULL_USER");
if (string.IsNullOrWhiteSpace(password)) return OperationResult.Fail("EMPTY_PASSWORD");
string userDn = string.Format(user.LdapSettings.DnTemplate, user.Username);
try
{
using var conn = new LdapConnection { SecureSocketLayer = user.LdapSettings.Ssl };
await conn.ConnectAsync(user.LdapSettings.Url, user.LdapSettings.Port, ct);
await conn.BindAsync(user.LdapSettings.BindDn, user.LdapSettings.BindCredentials, ct);
var attrs = new LdapAttributeSet();
user.GetLdapAttributes<TLdapIdentity, TKeyType>()
.ToList()
.ForEach(a => attrs.Add(a));
attrs.Add(new LdapAttribute("userPassword", password));
await conn.AddAsync(new LdapEntry(userDn, attrs));
return OperationResult.Success();
}
catch (LdapException ex) when (ex.ResultCode == LdapException.EntryAlreadyExists)
{
return OperationResult.Fail("ALREADY_EXISTS");
}
catch (Exception e)
{
logger.LogError(e, "LDAP create failed for {User}", user.Username);
return OperationResult.Fail("LDAP_ERROR");
}
}
public async Task<OperationResult> UpdateAsync(TLdapIdentity user, CancellationToken ct = default)
{
if (user is null) return OperationResult.Fail("NULL_USER");
var ldapIdentity = await FindAccountAsync(user.Username, FindAccountType.Username, ct);
if (ldapIdentity is null) return OperationResult.Fail("USER_NOT_FOUND");
string userDn = string.Format(ldapIdentity.LdapSettings.DnTemplate, user.Username!);
try
{
using var conn = await ConnectServiceAsync(ldapIdentity.LdapSettings, ct);
var mods = ldapIdentity.GetLdapAttributes<TLdapIdentity, TKeyType>()
.Select(a => new LdapModification(LdapModification.Replace, a))
.ToArray();
await conn.ModifyAsync(userDn, mods);
return OperationResult.Success();
}
catch (Exception e)
{
logger.LogError(e, "LDAP update failed for {User}", user.Username);
return OperationResult.Fail("LDAP_ERROR");
}
}
#endregion
#region IClaimStore
public Task<IReadOnlyCollection<Claim>> GetBaseClaimsAsync(TLdapIdentity ldapIdentity, CancellationToken ct = default)
=> Task.FromResult(
ldapIdentity.Identity.BuildClaims(ldapIdentity.Username, ldapIdentity.GivenName, ldapIdentity.Surname));
public Task<IReadOnlyCollection<Claim>?> GetRolesClaimsAsync(TLdapIdentity user, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyCollection<Claim>?>(null);
#endregion
#region IPasswordManager
public async Task<OperationResult> ChangePasswordAsync(TLdapIdentity ldapIdentity,
string currentPassword,
string newPassword,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(newPassword))
return OperationResult.Fail("EMPTY_NEW_PASSWORD");
var auth = await AuthenticateAsync(ldapIdentity, currentPassword, false, ct);
if (!auth.Succeeded) return OperationResult.Fail("INVALID_CURRENT_PASSWORD");
string userDn = string.Format(ldapIdentity.LdapSettings.DnTemplate, ldapIdentity.Username);
try
{
using var conn = await ConnectServiceAsync(ldapIdentity.LdapSettings, ct);
await conn.ModifyAsync(userDn,
new LdapModification(LdapModification.Replace,
new LdapAttribute("userPassword", newPassword)));
return OperationResult.Success();
}
catch (Exception e)
{
logger.LogError(e, "LDAP change password failed for {User}", ldapIdentity.Username);
return OperationResult.Fail("LDAP_ERROR");
}
}
public async Task<OperationResult> ResetPasswordAsync(TLdapIdentity ldapIdentity,
string resetToken,
string newPassword,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(newPassword))
return OperationResult.Fail("EMPTY_NEW_PASSWORD");
// @TODO:
// Implement token issue and check to check resetToken
string userDn = string.Format(ldapIdentity.LdapSettings.DnTemplate, ldapIdentity.Username);
try
{
using var conn = await ConnectServiceAsync(ldapIdentity.LdapSettings, ct);
await conn.ModifyAsync(userDn,
new LdapModification(LdapModification.Replace,
new LdapAttribute("userPassword", newPassword)));
return OperationResult.Success();
}
catch (Exception e)
{
logger.LogError(e, "LDAP change password failed for {User}", ldapIdentity.Username);
return OperationResult.Fail("LDAP_ERROR");
}
}
#endregion
#region IAccountLockManager
public async Task<OperationResult> LockAsync(TLdapIdentity ldapIdentity, CancellationToken ct = default)
{
string userDn = string.Format(ldapIdentity.LdapSettings.DnTemplate, ldapIdentity.Username);
try
{
using var conn = await ConnectServiceAsync(ldapIdentity.LdapSettings, ct);
LdapModification mod = ldapIdentity.LdapSettings.IsActiveDirectory
? new LdapModification(LdapModification.Replace,
new LdapAttribute("userAccountControl", "514"))
: new LdapModification(LdapModification.Replace,
new LdapAttribute("pwdAccountLockedTime",
DateTime.UtcNow.ToString("yyyyMMddHHmmss'Z'")));
await conn.ModifyAsync(userDn, mod);
return OperationResult.Success();
}
catch (Exception e)
{
logger.LogWarning(e, "LDAP lock not supported for {User}", ldapIdentity.Username);
return OperationResult.Fail("LOCK_FAILED");
}
}
public async Task<OperationResult> UnlockAsync(TLdapIdentity ldapIdentity, CancellationToken ct = default)
{
string userDn = string.Format(ldapIdentity.LdapSettings.DnTemplate, ldapIdentity.Username);
try
{
using var conn = await ConnectServiceAsync(ldapIdentity.LdapSettings, ct);
LdapModification mod = ldapIdentity.LdapSettings.IsActiveDirectory
? new LdapModification(LdapModification.Replace,
new LdapAttribute("userAccountControl", "512"))
: new LdapModification(LdapModification.Delete,
new LdapAttribute("pwdAccountLockedTime"));
await conn.ModifyAsync(userDn, mod);
return OperationResult.Success();
}
catch (Exception e)
{
logger.LogWarning(e, "LDAP unlock did not work for {User}", ldapIdentity.Username);
return OperationResult.Fail("UNLOCK_FAILED");
}
}
#endregion
#region Helpers discovery & connection
private async Task<TLdapIdentity?> FindAccountAsync(
string login,
FindAccountType findAccountType,
CancellationToken ct = default)
{
foreach (var settings in ldapSettingsProvider.Settings)
{
try
{
logger.LogInformation(
"FindAccountAsync called by: {Caller}",
new StackTrace(1, true).ToString()
);
logger.LogInformation(
"Trying LDAP server {FriendlyName} ({Url}) for user {Login} (Type: {Type}, RequestId: {RequestId})",
settings.FriendlyName,
settings.Url,
login,
findAccountType,
Activity.Current?.Id ?? "no-activity"
);
using var conn = await ConnectServiceAsync(settings, ct);
var normalizedLogin = findAccountType switch
{
FindAccountType.Email => emailNormalizer.Normalize(settings, login),
FindAccountType.Username => usernameNormalizer.Normalize(settings, login),
_ => login
};
string filter = string.Format(settings.SearchFilter, normalizedLogin);
var res = await conn.SearchAsync(settings.SearchBase,
LdapConnection.ScopeSub,
filter,
null,
false, ct);
if (!await res.HasMoreAsync()) continue;
var entry = await res.NextAsync();
// C# does not allow to add new() constraint add implicitly new these here
var ldapIdentity = Activator.CreateInstance<TLdapIdentity>()!;
// 1. copy the raw entry first
ldapIdentity.DistinguishedName = entry.Dn;
ldapIdentity.LdapSettings = settings;
ldapIdentity.MergeLdapEntry<TLdapIdentity,TKeyType>(entry);
return ldapIdentity;
}
catch (Exception ex)
{
logger.LogWarning(ex, "LDAP discovery failed on server {Url}", settings.Url);
}
}
return null;
}
private async Task<LdapConnection> ConnectServiceAsync(LdapSettings cfg, CancellationToken ct)
{
var conn = new LdapConnection { SecureSocketLayer = cfg.Ssl };
await conn.ConnectAsync(cfg.Url, cfg.Port, ct);
await conn.BindAsync(cfg.BindDn, cfg.BindCredentials, ct);
return conn;
}
private static bool IsAccountLocked(LdapException ex)
=> ex.ResultCode == 49 && ex.Message.Contains("775", StringComparison.Ordinal);
#endregion
}