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"); } } }