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,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)') 
 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>
|
||||
@@ -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>
|
||||
/// Customer‑specific LDAP user manager that piggy‑backs on <see cref="LdapUserManager{TAccount}"/>
|
||||
/// but adds Bulstrad‑specific 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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 Bulstrad‑specific 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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
/// Strongly‑typed 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; }
|
||||
|
||||
/* ────────────────────────── Human‑readable info ────────────────────────── */
|
||||
[LdapProperty("displayName")]
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
[LdapProperty("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[LdapProperty("info")]
|
||||
public string? Info { get; set; } // free‑form 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; }
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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})))"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user