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"); } /// /// Authenticates a user using the provided HTTP context. The implementation is not yet provided and the method always throws a . /// /// The current carrying the request data used for authentication. /// A that will resolve to the authenticated user once the method is implemented. /// Thrown because the login operation has not been implemented. /// public Task Login(HttpContext context) => throw new LoginServicesException("Not implemented"); /// /// Authenticates a user with the provided and . /// /// The username of the user attempting to authenticate. /// The password of the user attempting to authenticate. /// A that represents the asynchronous authentication operation, yielding the authenticated on success. /// Thrown because the authentication operation is not yet 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"); /// /// Retrieves a matching the supplied email address, or when no user is found. /// /// The email address used to look up the . /// A that resolves to the matching , or if no user exists for the given . /// Thrown because the operation is not implemented. /// public Task GetByEmail(string email) => throw new LoginServicesException("Not implemented"); /// /// Asynchronously retrieves a by the supplied . /// The current implementation always throws because the operation is not implemented. /// /// The username used to look up the . /// A that resolves to the matching , or if no user is found. /// Thrown for every invocation because the operation 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 []; } } /// /// Constructs a from the attributes of the supplied , mapping the LDAP username, first name, and last name properties according to the current configuration. The username falls back to an empty string when the configured attribute is missing, and first/last name values are only applied when their corresponding configuration entries are set and the LDAP entry exposes those attributes, with the last name appended to the first name when both are available. /// /// The whose attributes are read to populate the . /// A populated from the attributes. /// 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 []; } } }