fix: make Admin LDAP sign-in work against GLAuth
Three bugs blocked sign-in entirely: - Login.razor is static-SSR but its form model lacked [SupplyParameterFromForm], so the posted username/password never bound — SignInAsync saw empty fields and bailed before LDAP was contacted. Annotate the model; seed it in OnInitialized since BL0008 forbids an initializer on a [SupplyParameterFromForm] property. - appsettings.json ServiceAccountDn used ou=svcaccts, which GLAuth reads as a (non-existent) group — the service-account bind failed with "Group not found". Use cn=serviceaccount,dc=lmxopcua,dc=local. - LdapAuthService resolved the user DN by searching (uid=...), but GLAuth keys users by cn. Add an LdapOptions.UserNameAttribute knob (default cn for GLAuth; set sAMAccountName for Active Directory) and use it for the search filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,10 +47,17 @@
|
|||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Input _input = new();
|
// Static-SSR form post: the model must be [SupplyParameterFromForm] or the
|
||||||
|
// submitted username/password never bind back onto _input. The property
|
||||||
|
// cannot carry an initializer (BL0008) — seed it in OnInitialized instead.
|
||||||
|
[SupplyParameterFromForm]
|
||||||
|
private Input _input { get; set; } = default!;
|
||||||
|
|
||||||
private string? _error;
|
private string? _error;
|
||||||
private bool _busy;
|
private bool _busy;
|
||||||
|
|
||||||
|
protected override void OnInitialized() => _input ??= new();
|
||||||
|
|
||||||
private async Task SignInAsync()
|
private async Task SignInAsync()
|
||||||
{
|
{
|
||||||
_error = null;
|
_error = null;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public sealed class LdapAuthService(IOptions<LdapOptions> options, ILogger<LdapA
|
|||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
conn.Bind(_options.ServiceAccountDn, _options.ServiceAccountPassword), ct);
|
conn.Bind(_options.ServiceAccountDn, _options.ServiceAccountPassword), ct);
|
||||||
|
|
||||||
var filter = $"(uid={EscapeLdapFilter(username)})";
|
var filter = $"({_options.UserNameAttribute}={EscapeLdapFilter(username)})";
|
||||||
var results = await Task.Run(() =>
|
var results = await Task.Run(() =>
|
||||||
conn.Search(_options.SearchBase, LdapConnection.ScopeSub, filter, ["dn"], false), ct);
|
conn.Search(_options.SearchBase, LdapConnection.ScopeSub, filter, ["dn"], false), ct);
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ public sealed class LdapAuthService(IOptions<LdapOptions> options, ILogger<LdapA
|
|||||||
return results.Next().Dn;
|
return results.Next().Dn;
|
||||||
|
|
||||||
throw new LdapException("User not found", LdapException.NoSuchObject,
|
throw new LdapException("User not found", LdapException.NoSuchObject,
|
||||||
$"No entry for uid={username}");
|
$"No entry for {filter}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(_options.SearchBase)
|
return string.IsNullOrWhiteSpace(_options.SearchBase)
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ public sealed class LdapOptions
|
|||||||
public string DisplayNameAttribute { get; set; } = "cn";
|
public string DisplayNameAttribute { get; set; } = "cn";
|
||||||
public string GroupAttribute { get; set; } = "memberOf";
|
public string GroupAttribute { get; set; } = "memberOf";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute the service-account search matches the login name against to resolve the
|
||||||
|
/// user's DN. <c>cn</c> for GLAuth (the dev default); set <c>sAMAccountName</c> for
|
||||||
|
/// Active Directory.
|
||||||
|
/// </summary>
|
||||||
|
public string UserNameAttribute { get; set; } = "cn";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps LDAP group name → Admin role. Group match is case-insensitive. A user gets every
|
/// Maps LDAP group name → Admin role. Group match is case-insensitive. A user gets every
|
||||||
/// role whose source group is in their membership list. Example dev mapping:
|
/// role whose source group is in their membership list. Example dev mapping:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"UseTls": false,
|
"UseTls": false,
|
||||||
"AllowInsecureLdap": true,
|
"AllowInsecureLdap": true,
|
||||||
"SearchBase": "dc=lmxopcua,dc=local",
|
"SearchBase": "dc=lmxopcua,dc=local",
|
||||||
"ServiceAccountDn": "cn=serviceaccount,ou=svcaccts,dc=lmxopcua,dc=local",
|
"ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local",
|
||||||
"ServiceAccountPassword": "serviceaccount123",
|
"ServiceAccountPassword": "serviceaccount123",
|
||||||
"DisplayNameAttribute": "cn",
|
"DisplayNameAttribute": "cn",
|
||||||
"GroupAttribute": "memberOf",
|
"GroupAttribute": "memberOf",
|
||||||
|
|||||||
Reference in New Issue
Block a user