434 lines
20 KiB
C#
434 lines
20 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <!-- aidoc:v1 sig=4e5637f -->
|
|
public class LdapLoginService : ILoginService
|
|
{
|
|
/// <summary>
|
|
/// The IAuthorityService is used to manage user authorities in the application.
|
|
/// </summary>
|
|
private readonly IAuthorityService _authorityService;
|
|
|
|
/// <summary>
|
|
/// The LdapConfig contains the necessary configuration for connecting to the LDAP server, such as server address, port, search base, and attribute mappings.
|
|
/// </summary>
|
|
private readonly LdapConfig _ldapConfig;
|
|
|
|
/// <summary>
|
|
/// The ILogger is used for logging information, warnings, and errors related to LDAP login operations.
|
|
/// </summary>
|
|
private readonly ILogger<LdapLoginService> _logger;
|
|
|
|
/// <summary>
|
|
/// The IUserService is used to manage user information in the application, such as retrieving existing users or creating new users based on LDAP information.
|
|
/// </summary>
|
|
private readonly Lazy<IUserService> _userService;
|
|
|
|
|
|
/// <summary>
|
|
/// Constructor for LdapLoginService. It initializes the service with the necessary dependencies and validates the LDAP configuration.
|
|
/// </summary>
|
|
/// <param name="ldapConfig">The LDAP configuration options.</param>
|
|
/// <param name="validator">The validator for the LDAP configuration.</param>
|
|
/// <param name="userService">The user service for managing user information.</param>
|
|
/// <param name="authorityService">The authority service for managing user authorities.</param>
|
|
/// <param name="logger">The logger for logging LDAP login operations.</param>
|
|
/// <!-- aidoc:v1 sig=f1b8e4e body=6c4718f -->
|
|
public LdapLoginService(
|
|
IOptions<LdapConfig> ldapConfig,
|
|
IValidator<LdapConfig> validator,
|
|
Lazy<IUserService> userService,
|
|
IAuthorityService authorityService,
|
|
ILogger<LdapLoginService> logger)
|
|
{
|
|
_userService = userService;
|
|
_authorityService = authorityService;
|
|
_logger = logger;
|
|
_ldapConfig = ldapConfig.Value;
|
|
validator.Validate(_ldapConfig, options => options.ThrowOnFailures());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates that this login service allows password authentication, as it connects to an LDAP server which typically requires a username and password for authentication.
|
|
/// </summary>
|
|
public bool AllowPassword => true;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public UserEnum.LoginMethod Method => UserEnum.LoginMethod.Ldap;
|
|
|
|
/// <summary>
|
|
/// Authenticates a user against the LDAP server using the provided username and password.
|
|
/// </summary>
|
|
/// <param name="username">The username of the user to authenticate.</param>
|
|
/// <param name="password">The password of the user to authenticate.</param>
|
|
/// <returns>The authenticated user.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when there is an error during the login process.</exception>
|
|
/// <exception cref="UserNotFoundException">Thrown when the user is not found in the LDAP directory.</exception>
|
|
/// <!-- aidoc-review:v1 severity=high kind=wrong_exception
|
|
/// "UserNotFoundException is documented as thrown 'when the user is not found in the LDAP directory', but the code actually throws it from the catch (LdapException) block during the bind operation. The 'user not found in directory' case (empty search result) throws LoginServicesException, not UserNotFoundException." -->
|
|
public async Task<User> 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method is not implemented in the LdapLoginService, as the login process is handled through the Login(string username, string password) method.
|
|
/// </summary>
|
|
/// <param name="context">The HTTP context of the request.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
|
|
/// <!-- aidoc-review:v1 severity=medium kind=missing_returns
|
|
/// "The <returns> description says 'A task representing the asynchronous operation' but the method returns Task<User>; the User element of the return type is not documented." -->
|
|
public Task<User> Login(HttpContext context)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// This method is not implemented in the LdapLoginService, as the authentication process is handled through the Login(string username, string password) method.
|
|
/// </summary>
|
|
/// <param name="username">The username of the user to authenticate.</param>
|
|
/// <param name="password">The password of the user to authenticate.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
|
|
/// <!-- aidoc-review:v1 severity=medium kind=wrong_returns
|
|
/// "The method returns Task<User>, but the documentation only states 'A task representing the asynchronous operation' without mentioning that the task produces a User." -->
|
|
public Task<User> Authenticate(string username, string password)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="id">The ID of the user to retrieve.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=877a869 -->
|
|
public Task<User?> GetById(ObjectId id)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="email">The email of the user to retrieve.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=ba92b09 -->
|
|
public Task<User?> GetByEmail(string email)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="username">The username of the user to retrieve.</param>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=5184c30 -->
|
|
public Task<User?> GetByUsername(string username)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <returns>A task representing the asynchronous operation.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is not implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=aa066d6 -->
|
|
public Task<List<User>> GetAllUsers()
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="userEntryLdap">The user information obtained from the LDAP entry.</param>
|
|
/// <param name="entry">The LDAP entry containing the user's information.</param>
|
|
/// <returns>The existing or newly created user with updated authorities.</returns>
|
|
/// <!-- aidoc:v1 sig=87de1c2 body=a43bebd -->
|
|
private async Task<User?> 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;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This method checks the authorities of a user based on the LDAP entry and the application's configuration.
|
|
/// </summary>
|
|
/// <param name="user">The user whose authorities are being checked.</param>
|
|
/// <param name="entry">The LDAP entry containing the user's information.</param>
|
|
/// <returns>A list of updated authorities for the user.</returns>
|
|
/// <!-- aidoc-review:v1 severity=medium kind=stale_summary
|
|
/// "Summary states the method 'checks' authorities, but the method actually synchronizes authorities: it updates existing entries (when CanUpdate is true) and inserts new ones via _authorityService, with side effects beyond a read-only check." -->
|
|
private async Task<List<Authorization>> CheckAuthorities(User user, LdapEntry entry)
|
|
|
|
{
|
|
try
|
|
{
|
|
var authorizationMap = GetAuthoritiesMap(entry, user);
|
|
var authorizationWhiteList = GetAuthoritiesWhiteList(entry, user);
|
|
|
|
var whiteListDisplayIds = new HashSet<string?>(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 [];
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This method retrieves a list of authorities for a user based on a whitelist defined in the application's configuration.
|
|
/// </summary>
|
|
/// <param name="entry">The LDAP entry containing the user's information.</param>
|
|
/// <param name="user">The user whose authorities are being retrieved.</param>
|
|
/// <returns>A list of authorities for the user based on the whitelist.</returns>
|
|
/// <!-- aidoc:v1 sig=a8263f0 body=9e6cc38 -->
|
|
private List<Authorization> GetAuthoritiesWhiteList(LdapEntry entry, User user)
|
|
|
|
{
|
|
try
|
|
{
|
|
var result = new List<Authorization>();
|
|
|
|
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<PermissionEnum.RolesType>(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 [];
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="ldapEntry">The LDAP entry containing the user's information.</param>
|
|
/// <returns>The existing or newly created user with updated authorities.</returns>
|
|
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
|
|
/// "The summary claims the method retrieves a list of authorities and maps LDAP groups to authorities, but the method actually constructs a User object using username, first name, and last name properties from the LDAP entry." -->
|
|
/// <!-- aidoc-review:v1 severity=high kind=wrong_returns
|
|
/// "The <returns> tag claims to return an 'existing or newly created user with updated authorities', but the method always creates a new User and never touches 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;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="ldapEntry">The LDAP entry containing the user's information.</param>
|
|
/// <param name="user">The user whose authorities are being retrieved.</param>
|
|
/// <returns>A list of authorities for the user based on the LDAP entry and the application's configuration.</returns>
|
|
/// <!-- aidoc:v1 sig=1e1f0f2 body=18015c0 -->
|
|
private List<Authorization> GetAuthoritiesMap(LdapEntry ldapEntry, User user)
|
|
|
|
{
|
|
try
|
|
{
|
|
var authorities = new List<Authorization>();
|
|
|
|
if (!_ldapConfig.AuthoritiesMap.Any())
|
|
return authorities;
|
|
|
|
var groupAttr = entry.Attributes[_ldapConfig.GroupsProperty];
|
|
|
|
if (groupAttr == null)
|
|
return authorities;
|
|
|
|
var groups = groupAttr.GetValues(typeof(string))
|
|
.Cast<string>()
|
|
.ToList();
|
|
|
|
foreach (var authorityMap in _ldapConfig.AuthoritiesMap)
|
|
{
|
|
if (groups.Exists(g =>
|
|
string.Equals(g, authorityMap.Group, StringComparison.CurrentCultureIgnoreCase)))
|
|
{
|
|
if (!Enum.TryParse<PermissionEnum.RolesType>(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 [];
|
|
}
|
|
}
|
|
}
|