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
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:
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user