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,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDynamicLoading>true</EnableDynamicLoading>
<Nullable>enable</Nullable>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
<!--
If the caller passes -p:CopyPluginBinariesToPath=… we respect that.
Otherwise we fall back to the local relative folder used in Visual Studio.
-->
<CopyPluginBinariesToPath Condition="'$(CopyPluginBinariesToPath)' == ''">$([System.IO.Path]::Combine($(MSBuildProjectDirectory),'..','..','Ablera.Serdica.Authority','PluginBinaries','$(MSBuildProjectName)'))</CopyPluginBinariesToPath>
</PropertyGroup>
<!-- Fires after **every** successful build, CLI or IDE -->
<Target Name="CopyPluginBinaries" AfterTargets="Build">
<!-- Every file that just landed in the target directory -->
<ItemGroup>
<BuiltFiles Include="$(TargetDir)**\*.*" />
</ItemGroup>
<Message Importance="high" Text="Copying files: @(BuiltFiles->'%(RecursiveDir)%(Filename)%(Extension)') &#xD;&#xA; to: $(CopyPluginBinariesToPath)" />
<Copy SourceFiles="@(BuiltFiles)" DestinationFolder="$(CopyPluginBinariesToPath)" SkipUnchangedFiles="true" />
</Target>
<ItemGroup>
<Content Include="ldap-settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../../__Libraries/Ablera.Serdica.Authentication/Ablera.Serdica.Authentication.csproj" />
<ProjectReference Include="../../../../__Libraries/Ablera.Serdica.Common.Tools/Ablera.Serdica.Common.Tools.csproj" />
<ProjectReference Include="../../../../__Libraries/Ablera.Serdica.DBModels.Serdica/Ablera.Serdica.DBModels.Serdica.csproj" />
<ProjectReference Include="..\Ablera.Serdica.Authority.Plugins.Base\Ablera.Serdica.Authority.Plugins.Base.csproj" />
<ProjectReference Include="..\Ablera.Serdica.Authority.Plugins.LdapUtilities\Ablera.Serdica.Authority.Plugins.LdapUtilities.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,184 @@
using Ablera.Serdica.Common.Tools.Models.Config;
using Ablera.Serdica.Common.Tools;
using Ablera.Serdica.DBModels.Serdica;
using Ablera.Serdica.Authority.Plugins.Base.Contracts;
using Ablera.Serdica.Authority.Plugins.Base.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Ablera.Serdica.Common.Tools.Expressions.Models;
using Novell.Directory.Ldap;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Contracts;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Attributes;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Models;
namespace Ablera.Serdica.Authority.Plugin.Ldap;
/// <summary>
/// Thin façade that lets the generic pipeline work with <see cref="UserAccount"/> while delegating the real
/// work to an <see cref="IUserManager{OpenLdapAccount,string}"/> that talks to the directory.
/// </summary>
public class IdentityManagementFacade
: IUserManagementFacade<IdentityUser<string>>
{
private readonly LdapSettingsProvider ldapSettingsProvider;
private readonly LdapIdentityFacade userRepository;
public IdentityManagementFacade(
ILogger<GenericJsonSettingsProvider<Serdica.Extensions.Novell.Directory.Ldap.Models.LdapSettings[]>> logger,
IOptions<JsonFileSettingsConfig> options,
IUsernameNormalizer usernameNormalizer,
IEmailNormalizer emailNormalizer,
ILogger<LdapIdentityFacade> logger2,
ILogger<LdapIdentityFacade> logger3)
{
ldapSettingsProvider = new LdapSettingsProvider(
logger,
options);
userRepository = new LdapIdentityFacade(
logger3,
logger2,
ldapSettingsProvider,
emailNormalizer,
usernameNormalizer);
}
#region IAuthService
public async Task<AuthenticationResult> AuthenticateAsync(IdentityUser<string> identityUser,
string password,
bool lockoutOnFailure = false,
CancellationToken ct = default)
{
var ldap = await ResolveAsync(identityUser, ct);
if (ldap is null)
return AuthenticationResult.Fail("USER_NOT_FOUND");
return await userRepository.AuthenticateAsync(ldap, password, lockoutOnFailure, ct);
}
#endregion
#region IUserStore besteffort mapping
public async Task<IdentityUser<string>?> FindByEmailAsync(string email, CancellationToken ct = default)
=> (await userRepository.FindByEmailAsync(email, ct))?.Identity;
public async Task<IdentityUser<string>?> FindByNameAsync(string username, CancellationToken ct = default)
=> (await userRepository.FindByNameAsync(username, ct))?.Identity;
public async Task<IdentityUser<string>?> FindByIdAsync(string id, CancellationToken ct = default)
=> (await userRepository.FindByIdAsync(id, ct))?.Identity;
public async Task<OperationResult> CreateAsync(IdentityUser<string> identityUser, string password, CancellationToken ct = default)
{
if (identityUser == null) return OperationResult.Fail("NULL_USER");
var ldap = new LdapIdentity
{
Identity = identityUser,
Username = identityUser.UserName,
Email = identityUser.Email,
LdapSettings = ldapSettingsProvider.Settings.First()
};
return await userRepository.CreateAsync(ldap, password, ct);
}
public async Task<OperationResult> UpdateAsync(IdentityUser<string> identityUser, CancellationToken ct = default)
{
if (identityUser == null) return OperationResult.Fail("NULL_USER");
var ldapIdentity = await ResolveAsync(identityUser, ct);
if (ldapIdentity == null) return OperationResult.Fail("USER_NOT_FOUND");
ldapIdentity.Username = identityUser.UserName;
ldapIdentity.Email = identityUser.Email;
return await userRepository.UpdateAsync(ldapIdentity, ct);
}
#endregion
#region Claim helpers
public async Task<IReadOnlyCollection<Claim>> GetBaseClaimsAsync(IdentityUser<string> identityUser, CancellationToken ct = default)
{
var ldap = await ResolveAsync(identityUser, ct);
return ldap == null ? Array.Empty<Claim>() : await userRepository.GetBaseClaimsAsync(ldap, ct);
}
public async Task<IReadOnlyCollection<Claim>?> GetRolesClaimsAsync(IdentityUser<string> user, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, ct);
return ldap == null ? null : await userRepository.GetRolesClaimsAsync(ldap, ct);
}
#endregion
#region Password & lock operations delegated
public async Task<OperationResult> ChangePasswordAsync(IdentityUser<string> identityUser, string currentPassword, string newPassword, CancellationToken ct = default)
{
var ldap = await ResolveAsync(identityUser, ct);
return ldap == null
? OperationResult.Fail("USER_NOT_FOUND")
: await userRepository.ChangePasswordAsync(ldap, currentPassword, newPassword, ct);
}
public async Task<OperationResult> ResetPasswordAsync(IdentityUser<string> identityUser, string token, string newPassword, CancellationToken ct = default)
{
var ldap = await ResolveAsync(identityUser, ct);
return ldap == null
? OperationResult.Fail("USER_NOT_FOUND")
: await userRepository.ResetPasswordAsync(ldap, token, newPassword, ct);
}
public async Task<OperationResult> LockAsync(IdentityUser<string> identityUser, CancellationToken ct = default)
{
var ldap = await ResolveAsync(identityUser, ct);
return ldap == null ? OperationResult.Fail("USER_NOT_FOUND") : await userRepository.LockAsync(ldap, ct);
}
public async Task<OperationResult> UnlockAsync(IdentityUser<string> identityUser, CancellationToken ct = default)
{
var ldap = await ResolveAsync(identityUser, ct);
return ldap == null ? OperationResult.Fail("USER_NOT_FOUND") : await userRepository.UnlockAsync(ldap, ct);
}
#endregion
#region Helpers
private async Task<LdapIdentity?> ResolveAsync(IdentityUser<string> u, CancellationToken ct)
{
if (u == null) return null;
if (string.IsNullOrWhiteSpace(u.Email) == false)
{
var ret = await userRepository.FindByEmailAsync(u.Email, ct);
if (ret != null)
{
ret.Identity = u;
return ret;
}
}
if (string.IsNullOrWhiteSpace(u.UserName) == false)
{
var ret = await userRepository.FindByNameAsync(u.UserName, ct);
if (ret != null)
{
ret.Identity = u;
return ret;
}
}
return null;
}
#endregion
}

View File

@@ -0,0 +1,58 @@
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.Models;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using Ablera.Serdica.Authority.Plugins.LdapUtilities.Services;
namespace Ablera.Serdica.Authority.Plugin.Ldap;
public sealed class LdapIdentityFacade : LdapIdentityFacadeBase<LdapIdentity, string>
{
private readonly ILogger<LdapIdentityFacade> logger;
public LdapIdentityFacade(
ILogger<LdapIdentityFacade> logger,
ILogger<LdapIdentityFacadeBase<LdapIdentity, string>> logger2,
LdapSettingsProvider ldapSettingsProvider,
IEmailNormalizer emailNormalizer,
IUsernameNormalizer usernameNormalizer)
: base(logger2, ldapSettingsProvider, emailNormalizer, usernameNormalizer)
{
this.logger = logger;
}
public override async Task<AuthenticationResult> AuthenticateAsync(LdapIdentity user, string password, bool lockoutOnFailure = false, CancellationToken ct = default)
{
var result = await base.AuthenticateAsync(user, password, lockoutOnFailure, ct);
if (result.Succeeded == false) return result;
if (string.IsNullOrWhiteSpace(password))
return AuthenticationResult.Fail(AuthenticationCode.EmptyCredentials.ToScreamingSnakeCase());
if (string.IsNullOrWhiteSpace(user.DistinguishedName))
return AuthenticationResult.Fail(AuthenticationCode.AccountIsNotAuthenticaAble.ToScreamingSnakeCase());
// Ensure account is active.
if (user.BulstradAccountStatus != null && !string.Equals(user.BulstradAccountStatus, "active", StringComparison.OrdinalIgnoreCase))
{
logger.LogInformation("Bulstrad account {User} is not active (bstDStatus={Status})", user.Username, user.BulstradAccountStatus);
return AuthenticationResult.Fail(AuthenticationCode.AccountIsNotActive.ToScreamingSnakeCase());
}
// Build extra claims and append to principal
var extraClaims = new[]
{
new Claim("bstRole", user.BulstradRole ?? string.Empty),
new Claim("bstContractId", user.BulstradContractId ?? string.Empty),
new Claim("bstManId", user.BulstradManId ?? string.Empty),
new Claim(ClaimTypes.GroupSid, user.BulstradDepartmentNumber ?? string.Empty)
};
return result;
}
}

View File

@@ -0,0 +1,28 @@
using Ablera.Serdica.Common.Tools;
using Ablera.Serdica.Common.Tools.Models.Config;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Models;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Contracts;
namespace Ablera.Serdica.Authority.Plugin.Ldap;
public class LdapSettingsProvider : GenericJsonSettingsProvider<LdapSettings[]>, ILdapSettingsProvider
{
public const string JsonFileName = "ldap-settings.json";
public static readonly string JsonFilePath =
Path.GetDirectoryName(typeof(LdapSettingsProvider).Assembly.Location)
?? AppContext.BaseDirectory;
public LdapSettingsProvider(
ILogger<GenericJsonSettingsProvider<LdapSettings[]>> logger,
IOptions<JsonFileSettingsConfig> options)
: base(logger, options, JsonFileName, null, JsonFilePath)
{
}
}

View File

@@ -0,0 +1,19 @@
using Ablera.Serdica.Plugin.Contracts;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ablera.Serdica.Authority.Plugins.Base.Contracts;
using Ablera.Serdica.Authority.Plugin.Ldap;
using Ablera.Serdica.DBModels.Serdica;
using Microsoft.AspNetCore.Identity;
using Ablera.Serdica.DependencyInjection;
namespace Ablera.Serdica.Identity.Plugin.Ldap;
public class ServiceRegistrator : IPluginServiceRegistrator
{
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
=> services
.RegisterLdapExtensionServices(configuration)
.AddSingleton<LdapSettingsProvider>()
.AddScoped<IUserManagementFacade<IdentityUser<string>>, IdentityManagementFacade>();
}

View File

@@ -0,0 +1,21 @@
[
{
"FriendlyName": "Bulstrad_LDAP",
"Url": "10.239.82.101",
"IsActiveDirectory": false,
"NormalizeEmailToDomain": "bulstrad.bg",
"Port": 389,
"Ssl": false,
"DnTemplate": "uid={0},ou=partners,dc=ext,dc=bulstrad,dc=bg",
"BindDn": "uid=badm,ou=people,dc=ext,dc=bulstrad,dc=bg",
"BindCredentials": "!QAZ2wsxT6y",
"SearchBase": "ou=partners,dc=ext,dc=bulstrad,dc=bg",
"SearchFilter": "(&(objectClass=person)(uid={0}))",
"ExtraAttributes": [
"passwordExpirationTime",
"departmentNumber",
"bstDStatus",
"bstRole"
]
}
]