Merge pull request #229 from LD-Reborn/226-feature-backend-settings-model

226 feature backend settings model
This commit is contained in:
LD50
2025-11-02 13:40:02 +01:00
committed by GitHub
6 changed files with 132 additions and 16 deletions

View File

@@ -69,6 +69,7 @@ public class UsersController : Controller
} }
try try
{ {
Task<AdminSettingsModel> settingsTask = _ldap.GetAdminSettingsModelAsync();
string? jpegPhoto = requestModel.JpegPhoto; string? jpegPhoto = requestModel.JpegPhoto;
string? title = requestModel.Title; string? title = requestModel.Title;
string userPassword = requestModel.UserPassword ?? ""; string userPassword = requestModel.UserPassword ?? "";
@@ -79,9 +80,10 @@ public class UsersController : Controller
description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []}; description ??= new() {Address = new(), BirthDate = "", Workplace = "", Groups = []};
if (!userPassword.StartsWith('{')) if (!userPassword.StartsWith('{'))
{ {
AdminSettingsModel settings = await settingsTask;
byte[] passwordBytes = Encoding.UTF8.GetBytes(userPassword); byte[] passwordBytes = Encoding.UTF8.GetBytes(userPassword);
byte[] hashedPassword = SHA256.HashData(passwordBytes); byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet");
userPassword = "{SHA256}" + Convert.ToBase64String(hashedPassword); userPassword = $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}";
} }
LdapAttributeSet attributeSet = LdapAttributeSet attributeSet =
@@ -115,6 +117,7 @@ public class UsersController : Controller
} }
try try
{ {
Task<AdminSettingsModel> settingsTask = _ldap.GetAdminSettingsModelAsync();
string uid = requestModel.Uid; string uid = requestModel.Uid;
UserModel? user = null; UserModel? user = null;
if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0) if (requestModel.NewUid is not null && requestModel.NewUid.Length > 0)
@@ -136,7 +139,11 @@ public class UsersController : Controller
} }
if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0) if (requestModel.UserPassword is not null && requestModel.UserPassword.Length > 0)
{ {
await _ldap.UpdateUser(uid, "userPassword", "{SHA256}" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(requestModel.UserPassword)))); AdminSettingsModel settings = await settingsTask;
byte[] passwordBytes = Encoding.UTF8.GetBytes(requestModel.UserPassword);
byte[] hashedPassword = settings.hashAlgorithm?.ComputeHash(passwordBytes) ?? throw new Exception("Hash algorithm not instantiated yet");
requestModel.UserPassword = $"{{{settings.DefaultHashAlgorithm.ToUpperInvariant()}}}{Convert.ToBase64String(hashedPassword)}";
await _ldap.UpdateUser(uid, "userPassword", requestModel.UserPassword);
} }
string newUid = uid; string newUid = uid;

View File

@@ -0,0 +1,19 @@
using System.Security.Cryptography;
using System.Text.Json;
namespace Berufsschule_HAM.Models;
public class AdminSettingsModel
{
public required string DefaultHashAlgorithm { get; set; }
public int MaxDownloadableUserImageSize { get; set; }
public required string BarcodeType { get; set; }
public required string BarcodeText { get; set; }
public required Dictionary<string, Preset> Presets { get; set; }
public HashAlgorithm? hashAlgorithm;
}
public class Preset
{
public required Dictionary<string, string> Attribute { get; set; }
}

View File

@@ -13,5 +13,6 @@ public class LdapConfig
public string UsersOu { get; set; } = "ou=users"; public string UsersOu { get; set; } = "ou=users";
public string GroupsOu { get; set; } = "ou=groups"; public string GroupsOu { get; set; } = "ou=groups";
public string MigrationsOu { get; set; } = "ou=migrations"; public string MigrationsOu { get; set; } = "ou=migrations";
public string SettingsOu { get; set; } = "ou=settings";
public int ConnectionRetryCount { get; set; } = 10; public int ConnectionRetryCount { get; set; } = 10;
} }

View File

@@ -102,11 +102,20 @@ app.UseStaticFiles(new StaticFileOptions
Public = true, Public = true,
MaxAge = TimeSpan.FromDays(365) MaxAge = TimeSpan.FromDays(365)
}; };
} else if (requestPath.Contains("UserPhoto"))
{
ctx.Context.Response.GetTypedHeaders().CacheControl =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromMinutes(5)
};
} }
} }
}); });
app.UseRouting(); app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
app.UseResponseCaching();
app.UseResponseCompression(); app.UseResponseCompression();
string[] supportedCultures = ["de", "en"]; string[] supportedCultures = ["de", "en"];

View File

@@ -11,6 +11,7 @@ public partial class LdapService : IDisposable
{ {
private readonly LdapConfig _opts; private readonly LdapConfig _opts;
private readonly LdapConnection _conn; private readonly LdapConnection _conn;
private AdminSettingsModel? adminSettingsModel;
private ILogger _logger; private ILogger _logger;
public LdapService(IOptions<LdapConfig> options, ILogger<LdapService> logger) public LdapService(IOptions<LdapConfig> options, ILogger<LdapService> logger)
@@ -63,10 +64,12 @@ public partial class LdapService : IDisposable
public string UsersBaseDn => string.IsNullOrEmpty(_opts.UsersOu) ? _opts.BaseDn : $"{_opts.UsersOu},{_opts.BaseDn}"; public string UsersBaseDn => string.IsNullOrEmpty(_opts.UsersOu) ? _opts.BaseDn : $"{_opts.UsersOu},{_opts.BaseDn}";
public string GroupsBaseDn => string.IsNullOrEmpty(_opts.GroupsOu) ? _opts.BaseDn : $"{_opts.GroupsOu},{_opts.BaseDn}"; public string GroupsBaseDn => string.IsNullOrEmpty(_opts.GroupsOu) ? _opts.BaseDn : $"{_opts.GroupsOu},{_opts.BaseDn}";
public string MigrationsBaseDn => string.IsNullOrEmpty(_opts.MigrationsOu) ? _opts.BaseDn : $"{_opts.MigrationsOu},{_opts.BaseDn}"; public string MigrationsBaseDn => string.IsNullOrEmpty(_opts.MigrationsOu) ? _opts.BaseDn : $"{_opts.MigrationsOu},{_opts.BaseDn}";
public string SettingsBaseDn => string.IsNullOrEmpty(_opts.SettingsOu) ? _opts.BaseDn : $"{_opts.SettingsOu},{_opts.BaseDn}";
public string[] UsersAttributes => ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"]; public string[] UsersAttributes => ["cn", "sn", "title", "uid", "jpegPhoto", "userPassword", "description"];
public string[] AssetsAttributes => ["CN", "description", "l", "owner", "serialNumber", "name"]; public string[] AssetsAttributes => ["CN", "description", "l", "owner", "serialNumber", "name"];
public string[] LocationsAttributes => ["l", "description"]; public string[] LocationsAttributes => ["l", "description"];
public string[] GroupsAttributes => ["cn", "gidNumber", "description"]; public string[] GroupsAttributes => ["cn", "gidNumber", "description"];
public Dictionary<string, HashAlgorithm> HashAlgorithms => new() { ["MD5"] = MD5.Create(), ["SHA1"] = SHA1.Create(), ["SHA256"] = SHA256.Create(), ["SHA384"] = SHA384.Create(), ["SHA512"] = SHA512.Create(), ["SSHA"] = SHA1.Create(), ["SSHA256"] = SHA256.Create(), ["SSHA384"] = SHA384.Create(), ["SSHA512"] = SHA512.Create()};
public async Task<IEnumerable<LocationModel>> ListLocationsAsync() public async Task<IEnumerable<LocationModel>> ListLocationsAsync()
{ {
IEnumerable<Dictionary<string, string>> locations = await ListObjectBy(LocationsBaseDn, "", LocationsAttributes); IEnumerable<Dictionary<string, string>> locations = await ListObjectBy(LocationsBaseDn, "", LocationsAttributes);
@@ -115,6 +118,53 @@ public partial class LdapService : IDisposable
return true; return true;
} }
public async Task<AdminSettingsModel> GetAdminSettingsModelAsync()
{
if (adminSettingsModel is not null) return adminSettingsModel;
Dictionary<string, string> objects;
try
{
objects = (await ListObjectBy(_opts.BaseDn, _opts.SettingsOu, ["description"])).First();
adminSettingsModel = JsonSerializer.Deserialize<AdminSettingsModel>(objects["description"]) ?? throw new Exception();
adminSettingsModel.hashAlgorithm = initHashAlgorithm(adminSettingsModel);
return adminSettingsModel;
}
catch (Exception)
{
_logger.LogError("Unable to retrieve Admin settings");
throw new Exception("Unable to retrieve Admin settings");
}
}
public async Task<bool> SetAdminSettingsModelAsync(AdminSettingsModel adminSettingsModel)
{
adminSettingsModel.hashAlgorithm = initHashAlgorithm(adminSettingsModel);
this.adminSettingsModel = adminSettingsModel;
await ConnectAndBind();
try
{
string dn = SettingsBaseDn;
string targetText = JsonSerializer.Serialize(adminSettingsModel);
var modification = new LdapModification(
LdapModification.Replace,
new LdapAttribute("description", targetText)
);
await _conn.ModifyAsync(dn, modification);
}
catch (Exception)
{
return false;
}
return true;
}
private HashAlgorithm initHashAlgorithm(AdminSettingsModel adminSettingsModel)
{
return HashAlgorithms[adminSettingsModel.DefaultHashAlgorithm];
}
public async Task<string> GetAssetCounterAndIncrementAsync() public async Task<string> GetAssetCounterAndIncrementAsync()
{ {
AssetsMetadataModel assetModel = await GetAssetCounterAsync(); AssetsMetadataModel assetModel = await GetAssetCounterAsync();
@@ -292,7 +342,7 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
{ {
return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials }; return new() { Success = false, AuthenticationState = UserNotAuthenticatedReason.InvalidCredentials };
} }
if (CompareStringToSha256(password, user.UserPassword)) if (CompareStringToHashed(password, user.UserPassword))
{ {
return new() { Success = true, UserModel = user }; return new() { Success = true, UserModel = user };
} }
@@ -309,22 +359,26 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
} }
} }
public bool CompareStringToSha256(string sourcePassword, string targetPasswordHashed) public bool CompareStringToHashed(string sourcePassword, string targetPasswordHashed)
{ {
byte[] sourcePasswordBytes = SHA256.HashData(Encoding.UTF8.GetBytes(sourcePassword)); string algorithmName = CurlyBracesExtractor().Match(targetPasswordHashed).Groups[1].Value;
HashAlgorithm hashAlgorithm = HashAlgorithms[algorithmName.ToUpperInvariant()];
byte[] sourcePasswordBytes = Encoding.UTF8.GetBytes(sourcePassword);
byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, "")); byte[] targetPasswordHashedBytes = Convert.FromBase64String(CurlyBracesRemover().Replace(targetPasswordHashed, ""));
if (sourcePasswordBytes.Length != targetPasswordHashedBytes.Length) int hashSize = hashAlgorithm.HashSize / 8;
if (targetPasswordHashedBytes.Length > hashSize) // Has salt
{ {
return false; int saltLength = targetPasswordHashedBytes.Length - hashSize;
byte[] salt = new byte[saltLength];
Array.Copy(targetPasswordHashedBytes, hashSize, salt, 0, saltLength);
Array.Resize(ref targetPasswordHashedBytes, hashSize);
byte[] newSourcePasswordBytes = new byte[sourcePasswordBytes.Length + salt.Length];
Array.Copy(sourcePasswordBytes, 0, newSourcePasswordBytes, 0, sourcePasswordBytes.Length);
Array.Copy(salt, 0, newSourcePasswordBytes, sourcePasswordBytes.Length, salt.Length);
sourcePasswordBytes = newSourcePasswordBytes;
} }
for (int i = 0; i < sourcePasswordBytes.Length; i++) sourcePasswordBytes = hashAlgorithm.ComputeHash(sourcePasswordBytes);
{ return CryptographicOperations.FixedTimeEquals(sourcePasswordBytes, targetPasswordHashedBytes);
if (sourcePasswordBytes[i] != targetPasswordHashedBytes[i])
{
return false;
}
}
return true;
} }
private string PrependRDN(string rdn, string dn) private string PrependRDN(string rdn, string dn)
@@ -466,4 +520,6 @@ public async Task CreateAsset(LdapAttributeSet attributeSet)
[GeneratedRegex(@"\{.*?\}")] [GeneratedRegex(@"\{.*?\}")]
private static partial Regex CurlyBracesRemover(); private static partial Regex CurlyBracesRemover();
[GeneratedRegex(@"\{([^}]*)\}")]
private static partial Regex CurlyBracesExtractor();
} }

View File

@@ -105,6 +105,30 @@ public class MigrationService : IHostedService
return 2; return 2;
} }
public static async Task<int> UpdateFrom2Async(LdapService ldapService)
{
// Create the settings ou
AdminSettingsModel adminSettings = new() {
BarcodeText = "HAM",
BarcodeType = "code128",
MaxDownloadableUserImageSize = 256,
DefaultHashAlgorithm = "SSHA512",
Presets = []
};
string settingsString = JsonSerializer.Serialize(adminSettings);
LdapAttributeSet settingsAttributes =
[
new("objectClass", "organizationalUnit"),
new("objectClass", "top"),
new("ou", "settings"),
new("description", settingsString)
];
await TryCreateObjectIgnoreAlreadyExists(ldapService, ldapService.SettingsBaseDn, settingsAttributes);
return 3;
}
private static async Task TryCreateObjectIgnoreAlreadyExists(LdapService ldapService, string dn, LdapAttributeSet ldapAttributes) private static async Task TryCreateObjectIgnoreAlreadyExists(LdapService ldapService, string dn, LdapAttributeSet ldapAttributes)
{ {
try try