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,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)') 
 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>
|
||||
@@ -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 – best‑effort 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user