429 lines
19 KiB
C#
429 lines
19 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:v1 sig=bdfb451 body=e3fc1ac -->
|
|
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>
|
|
/// Authenticates a user using the provided HTTP context. The implementation is not yet provided and the method always throws a <see cref="LoginServicesException"/>.
|
|
/// </summary>
|
|
/// <param name="context">The current <see cref="HttpContext"/> carrying the request data used for authentication.</param>
|
|
/// <returns>A <see cref="Task{User}"/> that will resolve to the authenticated user once the method is implemented.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown because the login operation has not been implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=851dc90 -->
|
|
public Task<User> Login(HttpContext context)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// Authenticates a user with the specified <paramref name="username"/> and <paramref name="password"/> credentials.
|
|
/// </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 <see cref="Task{User}"/> that resolves to the authenticated <see cref="User"/>.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown when the method is invoked, as authentication is not implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=a0b1f46 -->
|
|
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>
|
|
/// Retrieves a <see cref="User"/> identified by the supplied email address.
|
|
/// The method is not implemented and always throws a <see cref="LoginServicesException"/>.
|
|
/// </summary>
|
|
/// <param name="email">The email address used to look up the user.</param>
|
|
/// <returns>A task that resolves to the matching <see cref="User"/>, or null if no user is found with the given email.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown because the method is not implemented.</exception>
|
|
/// <!-- aidoc:v1 sig=ba92b09 -->
|
|
public Task<User?> GetByEmail(string email)
|
|
=> throw new LoginServicesException("Not implemented");
|
|
|
|
/// <summary>
|
|
/// Asynchronously retrieves a <see cref="User"/> by the supplied <paramref name="username"/>.
|
|
/// The current implementation always throws <see cref="LoginServicesException"/> because the operation is not implemented.
|
|
/// </summary>
|
|
/// <param name="username">The username used to look up the <see cref="User"/>.</param>
|
|
/// <returns>A <see cref="Task{TResult}"/> that resolves to the matching <see cref="User"/>, or <see langword="null"/> if no user is found.</returns>
|
|
/// <exception cref="LoginServicesException">Thrown for every invocation because the operation 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:v1 sig=d2bafd5 body=4af15eb -->
|
|
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>
|
|
/// Constructs a <see cref="User"/> from the attributes of the supplied <paramref name="ldapEntry"/>, 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.
|
|
/// </summary>
|
|
/// <param name="ldapEntry">The <see cref="LdapEntry"/> whose attributes are read to populate the <see cref="User"/>.</param>
|
|
/// <returns>A <see cref="User"/> populated from the <paramref name="ldapEntry"/> attributes.</returns>
|
|
/// <!-- aidoc:v1 sig=947181a body=33ffec8 -->
|
|
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 [];
|
|
}
|
|
}
|
|
}
|