using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Net;
using Serilog;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
{
///
/// Validates credentials via LDAP bind and resolves group membership to application roles.
///
public class LdapAuthenticationProvider : IUserAuthenticationProvider, IRoleProvider
{
private static readonly ILogger Log = Serilog.Log.ForContext();
private readonly LdapConfiguration _config;
private readonly Dictionary _groupToRole;
public LdapAuthenticationProvider(LdapConfiguration config)
{
_config = config;
_groupToRole = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
{ config.ReadOnlyGroup, AppRoles.ReadOnly },
{ config.WriteOperateGroup, AppRoles.WriteOperate },
{ config.WriteTuneGroup, AppRoles.WriteTune },
{ config.WriteConfigureGroup, AppRoles.WriteConfigure },
{ config.AlarmAckGroup, AppRoles.AlarmAck }
};
}
public IReadOnlyList GetUserRoles(string username)
{
try
{
using (var connection = CreateConnection())
{
// Bind with service account to search
connection.Bind(new NetworkCredential(_config.ServiceAccountDn, _config.ServiceAccountPassword));
var request = new SearchRequest(
_config.BaseDN,
$"(cn={EscapeLdapFilter(username)})",
SearchScope.Subtree,
"memberOf");
var response = (SearchResponse)connection.SendRequest(request);
if (response.Entries.Count == 0)
{
Log.Warning("LDAP search returned no entries for {Username}", username);
return new[] { AppRoles.ReadOnly }; // safe fallback
}
var entry = response.Entries[0];
var memberOf = entry.Attributes["memberOf"];
if (memberOf == null || memberOf.Count == 0)
{
Log.Debug("No memberOf attributes for {Username}, defaulting to ReadOnly", username);
return new[] { AppRoles.ReadOnly };
}
var roles = new List();
for (var i = 0; i < memberOf.Count; i++)
{
var dn = memberOf[i]?.ToString() ?? "";
// Extract the OU/CN from the memberOf DN (e.g., "ou=ReadWrite,ou=groups,dc=...")
var groupName = ExtractGroupName(dn);
if (groupName != null && _groupToRole.TryGetValue(groupName, out var role)) roles.Add(role);
}
if (roles.Count == 0)
{
Log.Debug("No matching role groups for {Username}, defaulting to ReadOnly", username);
roles.Add(AppRoles.ReadOnly);
}
Log.Debug("LDAP roles for {Username}: [{Roles}]", username, string.Join(", ", roles));
return roles;
}
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to resolve LDAP roles for {Username}, defaulting to ReadOnly", username);
return new[] { AppRoles.ReadOnly };
}
}
public bool ValidateCredentials(string username, string password)
{
try
{
var bindDn = _config.BindDnTemplate.Replace("{username}", username);
using (var connection = CreateConnection())
{
connection.Bind(new NetworkCredential(bindDn, password));
}
Log.Debug("LDAP bind succeeded for {Username}", username);
return true;
}
catch (LdapException ex)
{
Log.Debug("LDAP bind failed for {Username}: {Error}", username, ex.Message);
return false;
}
catch (Exception ex)
{
Log.Warning(ex, "LDAP error during credential validation for {Username}", username);
return false;
}
}
private LdapConnection CreateConnection()
{
var identifier = new LdapDirectoryIdentifier(_config.Host, _config.Port);
var connection = new LdapConnection(identifier)
{
AuthType = AuthType.Basic,
Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds)
};
connection.SessionOptions.ProtocolVersion = 3;
return connection;
}
private static string? ExtractGroupName(string dn)
{
// Parse "ou=ReadWrite,ou=groups,dc=..." or "cn=ReadWrite,..."
if (string.IsNullOrEmpty(dn)) return null;
var parts = dn.Split(',');
if (parts.Length == 0) return null;
var first = parts[0].Trim();
var eqIdx = first.IndexOf('=');
return eqIdx >= 0 ? first.Substring(eqIdx + 1) : null;
}
private static string EscapeLdapFilter(string input)
{
return input
.Replace("\\", "\\5c")
.Replace("*", "\\2a")
.Replace("(", "\\28")
.Replace(")", "\\29")
.Replace("\0", "\\00");
}
}
}