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,50 @@
<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="bulstrad-settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../../__Libraries/Ablera.Serdica.Common.Tools/Ablera.Serdica.Common.Tools.csproj" />
<ProjectReference Include="..\..\..\..\__Libraries\Ablera.Serdica.Plugin\Ablera.Serdica.Plugin.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>
<Target Name="PostClean" AfterTargets="Clean">
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- obj -->
<RemoveDir Directories="$(BaseOutputPath)" />
<!-- bin -->
</Target>
</Project>

View File

@@ -0,0 +1,29 @@
using Ablera.Serdica.Authority.Plugins.LdapUtilities.Services;
using Ablera.Serdica.Authority.Plugin.Ldap.Models;
using Microsoft.Extensions.Logging;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Attributes;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Contracts;
namespace Ablera.Serdica.Authority.Plugin.Bulstrad;
/// <summary>
/// Customerspecific LDAP user manager that piggybacks on <see cref="LdapUserManager{TAccount}"/>
/// but adds Bulstradspecific semantics:
/// <list type="bullet">
/// <item>Accepts only <see cref="BulstradAdIdentity"/> objects.</item>
/// <item>Denies login if <c>bstDStatus != "active"</c>.</item>
/// <item>Emits extra role/department claims (<c>bstRole</c>, <c>departmentNumber</c>).</item>
/// </list>
/// </summary>
public sealed class BulstradAdIdentityFacade : LdapIdentityFacadeBase<BulstradAdIdentity, string>
{
public BulstradAdIdentityFacade(
ILogger<BulstradAdIdentityFacade> logger,
ILogger<LdapIdentityFacadeBase<BulstradAdIdentity, string>> logger2,
BulstradAsLdapSettingsProvider ldapSettingsProvider,
IEmailNormalizer emailNormalizer,
IUsernameNormalizer usernameNormalizer)
: base(logger2, ldapSettingsProvider, emailNormalizer, usernameNormalizer)
{
}
}

View File

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

View File

@@ -0,0 +1,159 @@
using Ablera.Serdica.Authority.Plugins.Base.Contracts;
using Ablera.Serdica.Authority.Plugins.Base.Models;
using Ablera.Serdica.Authority.Plugin.Ldap.Models;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Ablera.Serdica.Common.Tools.Extensions;
using Ablera.Serdica.Authority.Plugin.Ldap;
namespace Ablera.Serdica.Authority.Plugin.Bulstrad;
/// <summary>
/// Adapter exposing Bulstradspecific LDAP manager as <see cref="IUserManagementFacade{UserAccount}"/>.
/// </summary>
public class IdentityManagementFacade(BulstradAdIdentityFacade userRepository, BulstradAsLdapSettingsProvider settingsProvider) : IUserManagementFacade<IdentityUser<string>>
{
#region Authentication
public async Task<AuthenticationResult> AuthenticateAsync(IdentityUser<string> user, string password, bool lockoutOnFailure = false, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, ct);
if (ldap == null) return AuthenticationResult.Fail(AuthenticationCode.AccountIsNotFound.ToScreamingSnakeCase());
return await userRepository.AuthenticateAsync(ldap, password, lockoutOnFailure, ct);
}
#endregion
#region Store lookups
public async Task<IdentityUser<string>?> FindByEmailAsync(string email, CancellationToken ct = default)
=> Map(await userRepository.FindByEmailAsync(email, ct));
public async Task<IdentityUser<string>?> FindByNameAsync(string username, CancellationToken ct = default)
=> Map(await userRepository.FindByNameAsync(username, ct));
public async Task<IdentityUser<string>?> FindByIdAsync(string id, CancellationToken ct = default)
=> Map(await userRepository.FindByIdAsync(id, ct));
public async Task<OperationResult> CreateAsync(IdentityUser<string> user, string password, CancellationToken ct = default)
{
if (user == null) return OperationResult.Fail("NULL_USER");
var ldap = new BulstradAdIdentity
{
Username = user.UserName,
Email = user.Email ?? user.UserName,
ObjectClasses = ["top", "person", "organizationalPerson", "inetorgperson"],
Identity = user,
LdapSettings = settingsProvider.Settings.First()
};
return await userRepository.CreateAsync(ldap, password, ct);
}
public async Task<OperationResult> UpdateAsync(IdentityUser<string> user, CancellationToken ct = default)
{
if (user == null) return OperationResult.Fail("NULL_USER");
var ldapIdentity = await ResolveAsync(user, ct);
if (ldapIdentity == null) return OperationResult.Fail("USER_NOT_FOUND");
ldapIdentity.Username = user.UserName;
ldapIdentity.Email = user.Email;
ldapIdentity.ObjectClasses = [ "top", "person", "organizationalPerson", "inetorgperson"];
ldapIdentity.Identity = user;
ldapIdentity.LdapSettings = settingsProvider.Settings.First();
return await userRepository.UpdateAsync(ldapIdentity, ct);
}
#endregion
#region Claims
public async Task<IReadOnlyCollection<Claim>> GetBaseClaimsAsync(IdentityUser<string> user, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, 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
public async Task<OperationResult> ChangePasswordAsync(IdentityUser<string> user, string currentPassword, string newPassword, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, ct);
return ldap == null ? OperationResult.Fail("USER_NOT_FOUND") : await userRepository.ChangePasswordAsync(ldap, currentPassword, newPassword, ct);
}
public async Task<OperationResult> ResetPasswordAsync(IdentityUser<string> user, string token, string newPassword, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, ct);
return ldap == null ? OperationResult.Fail("USER_NOT_FOUND") : await userRepository.ResetPasswordAsync(ldap, token, newPassword, ct);
}
public async Task<OperationResult> LockAsync(IdentityUser<string> user, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, ct);
return ldap == null ? OperationResult.Fail("USER_NOT_FOUND") : await userRepository.LockAsync(ldap, ct);
}
public async Task<OperationResult> UnlockAsync(IdentityUser<string> user, CancellationToken ct = default)
{
var ldap = await ResolveAsync(user, ct);
return ldap == null ? OperationResult.Fail("USER_NOT_FOUND") : await userRepository.UnlockAsync(ldap, ct);
}
#endregion
#region Helper mapping
private async Task<BulstradAdIdentity?> 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;
}
private IdentityUser<string>? Map(BulstradAdIdentity? b)
{
if (b == null) return null;
return new IdentityUser
{
Id = string.Empty,
UserName = b.Username,
Email = b.Email,
NormalizedUserName = b.Username?.ToUpperInvariant(),
NormalizedEmail = b.Email?.ToUpperInvariant(),
EmailConfirmed = true
};
}
#endregion
}

View File

@@ -0,0 +1,120 @@
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.AspNetCore.Identity;
namespace Ablera.Serdica.Authority.Plugin.Ldap.Models;
/// <summary>
/// Stronglytyped projection of a Bulstrad AD user entry.
/// </summary>
public class BulstradAdIdentity : ILdapIdentity<string>
{
/* ────────────────────────── Core identification ────────────────────────── */
[LdapProperty("sAMAccountName")]
public required string Username { get; set; }
[LdapProperty("userPrincipalName")]
public string? Email { get; set; }
[LdapProperty("cn")]
public string? CommonName { get; set; }
[LdapProperty("givenName")]
public string? GivenName { get; set; }
[LdapProperty("sn")]
public string? Surname { get; set; }
/* ────────────────────────── Humanreadable info ────────────────────────── */
[LdapProperty("displayName")]
public string? DisplayName { get; set; }
[LdapProperty("description")]
public string? Description { get; set; }
[LdapProperty("info")]
public string? Info { get; set; } // freeform notes
/* ────────────────────────── DN & object identity ───────────────────────── */
public string? DistinguishedName { get; set; } // NOTE: This is populated by the Novel.LdapEntry.DN
/// </summary>
[LdapProperty("objectClass")]
public string[]? ObjectClasses { get; set; } // multivalued
[LdapProperty("objectGUID")]
public Guid? ObjectGuid { get; set; }
[LdapProperty("objectSid")]
public string? ObjectSid { get; set; }
[LdapProperty("objectCategory")]
public string? ObjectCategory { get; set; }
/* ────────────────────────── Group memberships ──────────────────────────── */
[LdapProperty("memberOf")]
public string[]? MemberOf { get; set; }
[LdapProperty("primaryGroupID")]
public int? PrimaryGroupId { get; set; }
/* ────────────────────────── Account state & counters ───────────────────── */
[LdapProperty("userAccountControl")]
public int? UserAccountControl { get; set; }
[LdapProperty("accountExpires")]
public long? AccountExpires { get; set; }
[LdapProperty("lockoutTime")]
public long? LockoutTime { get; set; }
[LdapProperty("badPwdCount")]
public int? BadPasswordCount { get; set; }
[LdapProperty("logonCount")]
public int? LogonCount { get; set; }
[LdapProperty("pwdLastSet")]
public long? PwdLastSet { get; set; }
[LdapProperty("lastLogon")]
public long? LastLogon { get; set; }
[LdapProperty("lastLogonTimestamp")]
public long? LastLogonTimestamp { get; set; }
[LdapProperty("lastLogoff")]
public long? LastLogoff { get; set; }
/* ────────────────────────── Audit / replication ────────────────────────── */
[LdapProperty("whenCreated")]
public DateTime? WhenCreated { get; set; }
[LdapProperty("whenChanged")]
public DateTime? WhenChanged { get; set; }
[LdapProperty("uSNCreated")]
public long? UsnCreated { get; set; }
[LdapProperty("uSNChanged")]
public long? UsnChanged { get; set; }
[LdapProperty("dSCorePropagationData")]
public string[]? DsCorePropagationData { get; set; }
/* ────────────────────────── Misc technical fields ──────────────────────── */
[LdapProperty("instanceType")]
public int? InstanceType { get; set; }
[LdapProperty("protocolSettings")]
public string[]? ProtocolSettings { get; set; }
[LdapProperty("msDS-SupportedEncryptionTypes")]
public int? SupportedEncryptionTypes { get; set; }
/* ────────────────────────── Infrastructure hooks ───────────────────────── */
public required IdentityUser<string> Identity { get; set; }
public required LdapSettings LdapSettings { get; set; }
}

View File

@@ -0,0 +1,23 @@
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.Bulstrad;
using Microsoft.AspNetCore.Identity;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Attributes;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Normalizers;
using Ablera.Serdica.Extensions.Novell.Directory.Ldap.Contracts;
namespace Ablera.Serdica.Identity.Plugin.Bulstrad;
public class ServiceRegistrator : IPluginServiceRegistrator
{
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
=> services
// Add Ldap plugin registrations
.AddSingleton<IEmailNormalizer, EmailNormalizer>()
.AddSingleton<IUsernameNormalizer, UsernameNormalizer>()
// Bulstrad plugin specific
.AddSingleton<BulstradAsLdapSettingsProvider>()
.AddScoped<BulstradAdIdentityFacade>()
.AddScoped<IUserManagementFacade<IdentityUser<string>>, Ablera.Serdica.Authority.Plugin.Bulstrad.IdentityManagementFacade>();
}

View File

@@ -0,0 +1,15 @@
[
{
"FriendlyName": "Bulstrad_AD",
"Url": "10.239.82.101",
"IsActiveDirectory": true,
"NormalizeEmailToDomain": "bulstrad.bg",
"Port": 3892,
"Ssl": false,
"DnTemplate": "CN={0},OU=_Ablera,OU=Regions,OU=_Bulstrad,DC=bulstrad,DC=bg",
"BindDn": "CN=Serdika,OU=_Ablera,OU=Regions,OU=_Bulstrad,DC=bulstrad,DC=bg",
"BindCredentials": "Ab123ra456",
"SearchBase": "OU=Regions,OU=_Bulstrad,DC=bulstrad,DC=bg",
"SearchFilter": "(&(objectClass=person)(|(userPrincipalName={0})(mail={0})))"
}
]