mirror of
https://github.com/LD-Reborn/Berufsschule_HAM.git
synced 2025-12-20 06:51:55 +00:00
Added settings model, added settings migration, added LDAP salted hashes
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
19
src/Models/AdminSettingsModel.cs
Normal file
19
src/Models/AdminSettingsModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user