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.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using System.DirectoryServices.Protocols; using System.Net; using Authorization = adas_core.Domain.Models.MongoModels.Authorization; 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) { if (_ldapConfig.Server == null) throw new LoginServicesException("LDAP Config not found"); var identifier = new LdapDirectoryIdentifier(_ldapConfig.Server, _ldapConfig.Port ?? 389); var connection = new LdapConnection(identifier); try { if (_ldapConfig.LdapUser != null) { _logger.LogInformation("[LDAP] Using configured LDAP user {LdapUser}", _ldapConfig.LdapUser); connection.Credential = new NetworkCredential(_ldapConfig.LdapUser, _ldapConfig.LdapPassword); } else { var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName) ? _ldapConfig.UserDomainName + @"\" : "") + username; connection.Credential = new NetworkCredential(ldapUser, password); } connection.AuthType = AuthType.Basic; connection.Bind(); } catch (LdapException e) { _logger.LogError("[LDAP] Error binding user {username}", username); throw new UserNotFoundException(username, e); } SearchResultEntry? entry = null; try { var request = new SearchRequest( _ldapConfig.SearchBase, $"({_ldapConfig.UserNameProperty}={username})", SearchScope.Subtree ); var response = (SearchResponse)connection.SendRequest(request); foreach (SearchResultEntry current in response.Entries) { entry = current; break; } } catch (Exception e) { _logger.LogError("[LDAP] Search error for user {username}: {error}", username, e.Message); throw; } if (entry == null) throw new LoginServicesException("LDAP User not found"); var userEntryLdap = GetUser(entry); var user = await GetOrCreateUser(userEntryLdap, entry); _logger.LogInformation("[LDAP] entry found and user {user}", user); connection.Dispose(); 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); 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); var whiteListDisplayIds = new HashSet(authorizationWhiteList.Select(a => a.DisplayId)); var uniqueMapAuthorizations = authorizationMap .Where(a => !whiteListDisplayIds.Contains(a.DisplayId)); 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 error for user {user}: {error}", 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 result = new List(); var whiteList = _ldapConfig.WhiteList.FindAll(u => (u.Name != null && entry.DistinguishedName.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) || (u.Username != null && entry.Attributes[_ldapConfig.UserNameProperty]?[0]?.ToString() ?.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase) == true) ); foreach (var authorityMap in whiteList) { if (!Enum.TryParse(authorityMap.Rol, out _)) continue; result.Add(new Authorization { UserId = user.Id, DisplayId = authorityMap.DisplayId, Rol = authorityMap.Rol }); } return result; } catch (Exception e) { _logger.LogError("[LDAP] GetAuthoritiesWhiteList error: {error}", 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 user = new User { UserName = entry.Attributes[_ldapConfig.UserNameProperty]?[0]?.ToString() ?? "" }; if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty)) { var first = entry.Attributes[_ldapConfig.FirstNameProperty]?[0]?.ToString(); if (first != null) user.Name = first; } if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty)) { var last = entry.Attributes[_ldapConfig.LastNameProperty]?[0]?.ToString(); if (last != null) user.Name = string.IsNullOrEmpty(user.Name) ? last : $"{user.Name} {last}"; } 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 groupAttr = entry.Attributes[_ldapConfig.GroupsProperty]; if (groupAttr == null) return authorities; var groups = groupAttr.GetValues(typeof(string)) .Cast() .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("[LDAP] GetAuthoritiesMap error: {error}", e.Message); return []; } } }