using adas_core.Authentication.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Exceptions; using adas_core.Domain.Models.MongoModels; using adas_core.LdapLogin.Configuration; using FluentValidation; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using Novell.Directory.Ldap; namespace adas_core.LdapLogin; /// /// Implementation of ILoginService that authenticates users against an LDAP server. /// It retrieves user information and authorities based on the LDAP entry and maps them to the application's user model. /// The service also handles the creation of new users in the application if they do not already exist, based on the LDAP information. /// It uses configuration settings for connecting to the LDAP server and for mapping LDAP attributes to user properties and authorities. /// public class LdapLoginService : ILoginService { /// /// The IAuthorityService is used to manage user authorities in the application. /// private readonly IAuthorityService _authorityService; /// /// The LdapConfig contains the necessary configuration for connecting to the LDAP server, such as server address, port, search base, and attribute mappings. /// private readonly LdapConfig _ldapConfig; /// /// The ILogger is used for logging information, warnings, and errors related to LDAP login operations. /// private readonly ILogger _logger; /// /// The IUserService is used to manage user information in the application, such as retrieving existing users or creating new users based on LDAP information. /// private readonly Lazy _userService; /// /// Constructor for LdapLoginService. It initializes the service with the necessary dependencies and validates the LDAP configuration. /// /// The LDAP configuration options. /// The validator for the LDAP configuration. /// The user service for managing user information. /// The authority service for managing user authorities. /// The logger for logging LDAP login operations. public LdapLoginService( IOptions ldapConfig, IValidator validator, Lazy userService, IAuthorityService authorityService, ILogger logger) { _userService = userService; _authorityService = authorityService; _logger = logger; _ldapConfig = ldapConfig.Value; validator.Validate(_ldapConfig, options => options.ThrowOnFailures()); } /// /// Indicates that this login service allows password authentication, as it connects to an LDAP server which typically requires a username and password for authentication. /// public bool AllowPassword => true; /// /// Indicates that this login service does not allow token-based authentication, as it is designed to authenticate users against an LDAP server using their credentials rather than tokens. /// public UserEnum.LoginMethod Method => UserEnum.LoginMethod.Ldap; /// /// Authenticates a user against the LDAP server using the provided username and password. /// /// The username of the user to authenticate. /// The password of the user to authenticate. /// The authenticated user. /// Thrown when there is an error during the login process. /// Thrown when the user is not found in the LDAP directory. public async Task Login(string username, string password) { // Check for LDAP config if (_ldapConfig.Server == null) throw new LoginServicesException("LDAP Config not found"); var conn = new LdapConnection(); try { await conn.ConnectAsync(_ldapConfig.Server, _ldapConfig.Port ?? 389); } catch (Exception e) { _logger.LogError("[LDAP] Error connecting to {server}, port {port}, Exception: {e}", _ldapConfig.Server, _ldapConfig.Port, e.Message); throw; } if (_ldapConfig.LdapUser != null) { _logger.LogInformation("[LDAP] _ldapConfig.LdapUser is enabled with {LdapUser}", _ldapConfig.LdapUser); await conn.BindAsync(_ldapConfig.LdapUser, _ldapConfig.LdapPassword); } else { var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName) ? _ldapConfig.UserDomainName + @"\" : "") + username; try { await conn.BindAsync(ldapUser, password); } catch (LdapException e) { _logger.LogError("[LDAP] Error binding ldapUser: {LdapUser} and password", ldapUser); throw new UserNotFoundException(username, e); } } var results = await conn.SearchAsync( _ldapConfig.SearchBase, LdapConnection.ScopeSub, $"({_ldapConfig.UserNameProperty}={username})", null, false); LdapEntry? entry = null; while (await results.HasMoreAsync()) { LdapEntry? current = null; try { current = await results.NextAsync(); } catch (LdapException ex) { _logger.LogWarning("[LDAP] Skipping invalid entry: {error}", ex.Message); continue; } if (current != null) { entry = current; break; } } if (entry == null) throw new LoginServicesException("LDAP User not found"); var userEntryLdap = GetUser(entry); var user = await GetOrCreateUser(userEntryLdap, entry); _logger.LogInformation("[LDAP] entry is {entry} and user {user}", entry, user); // user.Authorization.AddRange(GetAuthorities(entry)); conn.Disconnect(); return user ?? throw new LoginServicesException("LDAP User not found"); } /// /// This method is not implemented in the LdapLoginService, as the login process is handled through the Login(string username, string password) method. /// /// The HTTP context of the request. /// A task representing the asynchronous operation. /// Thrown when the method is not implemented. public Task Login(HttpContext context) { throw new LoginServicesException("Not implemented"); } /// /// This method is not implemented in the LdapLoginService, as the authentication process is handled through the Login(string username, string password) method. /// /// The username of the user to authenticate. /// The password of the user to authenticate. /// A task representing the asynchronous operation. /// Thrown when the method is not implemented. public Task Authenticate(string username, string password) { throw new LoginServicesException("Not implemented"); } /// /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method. /// /// The ID of the user to retrieve. /// A task representing the asynchronous operation. /// Thrown when the method is not implemented. public Task GetById(ObjectId id) { throw new LoginServicesException("Not implemented"); } /// /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method. /// /// The email of the user to retrieve. /// A task representing the asynchronous operation. /// Thrown when the method is not implemented. public Task GetByEmail(string email) { throw new LoginServicesException("Not implemented"); } /// /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method. /// /// The username of the user to retrieve. /// A task representing the asynchronous operation. /// Thrown when the method is not implemented. public Task GetByUsername(string username) { throw new LoginServicesException("Not implemented"); } /// /// This method is not implemented in the LdapLoginService, as the user retrieval process is handled through the Login(string username, string password) method and the GetOrCreateUser(User userEntryLdap, LdapEntry entry) method. /// /// A task representing the asynchronous operation. /// Thrown when the method is not implemented. public Task> GetAllUsers() { throw new LoginServicesException("Not implemented"); } /// /// This method retrieves an existing user from the application based on the information obtained from the LDAP entry, or creates a new user if one does not already exist. /// It also checks and updates the user's authorities based on the LDAP entry and the application's configuration. /// /// The user information obtained from the LDAP entry. /// The LDAP entry containing the user's information. /// The existing or newly created user with updated authorities. private async Task GetOrCreateUser(User userEntryLdap, LdapEntry entry) { var userToReturn = (await _userService.Value.GetUserByUserName(userEntryLdap.UserName) ?? await _userService.Value.GetUserByName(userEntryLdap.Name)) ?? await _userService.Value.CreateUser(userEntryLdap); if (userToReturn == null) return userToReturn; userToReturn.Authorization = []; var authorities = await CheckAuthorities(userToReturn, entry); userToReturn.Authorization.AddRange(authorities); //foreach (var authorization in userToReturn.Authorization) //{ // Enum.TryParse(authorization.Rol, out var compareRole); // if (compareRole == RolesType.Admin) userToReturn.Rol = RolesType.Admin; //} return userToReturn; } /// /// This method checks the authorities of a user based on the LDAP entry and the application's configuration. /// /// The user whose authorities are being checked. /// The LDAP entry containing the user's information. /// A list of updated authorities for the user. private async Task> CheckAuthorities(User user, LdapEntry entry) { try { var authorizationMap = GetAuthoritiesMap(entry, user); var authorizationWhiteList = GetAuthoritiesWhiteList(entry, user); _logger.LogInformation( "[LDAP] user {user} authorizationMap count is {authorizationMap}, authorizationWhiteList count is {authorizationWhiteList}", user.UserName, authorizationMap.Count, authorizationWhiteList.Count); // Primero, creamos un HashSet con los DisplayID de la lista blanca para búsqueda eficiente var whiteListDisplayIds = new HashSet(authorizationWhiteList.Select(a => a.DisplayId)); // Filtramos los elementos de authorizationMap que no están en la lista blanca, basándonos en DisplayID var uniqueMapAuthorizations = authorizationMap.Where(a => !whiteListDisplayIds.Contains(a.DisplayId)); // Finalmente, combinamos los elementos únicos de authorizationMap con los de authorizationWhiteList var combinedList = authorizationWhiteList.Concat(uniqueMapAuthorizations).ToList(); var userAuthorities = await _authorityService.GetUserAuthorities(user.Id); foreach (var auth in combinedList) { var authFound = userAuthorities.Find(c => c.DisplayId == auth.DisplayId); if (authFound is { CanUpdate: true }) { authFound.Rol = auth.Rol; await _authorityService.updateOne(authFound); } else { await _authorityService.InsertOne(auth); } } return await _authorityService.GetUserAuthorities(user.Id); } catch (Exception e) { _logger.LogError("[LDAP] CheckAuthorities for user {user} has exception {ex}", user.UserName, e.Message); return []; } } /// /// This method retrieves a list of authorities for a user based on a whitelist defined in the application's configuration. /// /// The LDAP entry containing the user's information. /// The user whose authorities are being retrieved. /// A list of authorities for the user based on the whitelist. private List GetAuthoritiesWhiteList(LdapEntry entry, User user) { try { var userWhiteList = new List(); var userNameProperty = _ldapConfig.UserNameProperty; var whiteList = _ldapConfig.WhiteList.FindAll(u => (u.Name != null && entry.Dn.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) || (u.Username != null && userNameProperty != null && entry.GetAttributeSet().TryGetValue(userNameProperty, out var attr) && attr.StringValue != null && attr.StringValue.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase)) ); if (whiteList.Count == 0) return userWhiteList; foreach (var authorityMap in whiteList) { if (!Enum.TryParse(authorityMap.Rol, out _)) continue; userWhiteList.Add(new Authorization { UserId = user.Id, DisplayId = authorityMap.DisplayId, Rol = authorityMap.Rol }); } return userWhiteList; } catch (Exception e) { _logger.LogError("[LDAP] GetAuthoritiesWhiteList for user {user} has exception {ex}", user.UserName, e.Message); return []; } } /// /// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities. /// /// The LDAP entry containing the user's information. /// The existing or newly created user with updated authorities. private User GetUser(LdapEntry ldapEntry) { var attributes = ldapEntry.GetAttributeSet(); var user = new User(); // UserName if (!string.IsNullOrWhiteSpace(_ldapConfig.UserNameProperty) && attributes.TryGetValue(_ldapConfig.UserNameProperty, out var userAttr) && userAttr?.StringValue != null) { user.UserName = userAttr.StringValue; } else { user.UserName = ""; } _logger.LogInformation("[LDAP] GetUser UserName is {UserName} ", user.UserName); // First name if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty) && attributes.TryGetValue(_ldapConfig.FirstNameProperty, out var firstNameAttr) && firstNameAttr?.StringValue != null) { user.Name = firstNameAttr.StringValue; } // Last name if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty) && attributes.TryGetValue(_ldapConfig.LastNameProperty, out var lastNameAttr) && lastNameAttr?.StringValue != null) { user.Name = string.IsNullOrEmpty(user.Name) ? lastNameAttr.StringValue : $"{user.Name} {lastNameAttr.StringValue}"; } return user; } /// /// This method retrieves a list of authorities for a user based on the LDAP entry and the application's configuration for mapping LDAP groups to authorities. /// /// The LDAP entry containing the user's information. /// The user whose authorities are being retrieved. /// A list of authorities for the user based on the LDAP entry and the application's configuration. private List GetAuthoritiesMap(LdapEntry ldapEntry, User user) { try { var authorities = new List(); if (!_ldapConfig.AuthoritiesMap.Any()) return authorities; var attributes = ldapEntry.GetAttributeSet(); if (!string.IsNullOrWhiteSpace(_ldapConfig.GroupsProperty) || !attributes.TryGetValue(_ldapConfig.GroupsProperty!, out var groupsAttr) || groupsAttr?.StringValueArray == null) { return authorities; } var groups = groupsAttr.StringValueArray.ToList(); foreach (var authorityMap in _ldapConfig.AuthoritiesMap) { if (groups.Exists(g => string.Equals(g, authorityMap.Group, StringComparison.CurrentCultureIgnoreCase))) { if (!Enum.TryParse(authorityMap.Rol, out _)) continue; authorities.Add(new Authorization { UserId = user.Id, DisplayId = authorityMap.DisplayId, Rol = authorityMap.Rol }); } } return authorities; } catch (Exception e) { _logger.LogError("Error getting Authorities Map. Return new empty list. Exception: {e}", e); return []; } } }