179 lines
8.9 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.DirectoryServices;
using Microsoft.Extensions.Caching.Memory;
using System.DirectoryServices.AccountManagement;
using Microsoft.Extensions.Options;
using DigitalData.Core.Application.Abstraction.DTO;
using DigitalData.Core.Application.Abstraction;
namespace DigitalData.Core.Application
{
//TODO: rename as DirectorySearcher
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
public class DirectorySearchService : IDirectorySearchService
{
private readonly IMemoryCache _memoryCache;
public string ServerName { get; }
public string Root { get; }
public string SearchRootPath { get; }
private readonly DateTimeOffset? _userCacheExpiration;
public Dictionary<string, string> CustomSearchFilters { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DirectorySearchService"/> class.
/// </summary>
/// <param name="options">The options for directory search.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the server name or root directory is not configured.
/// </exception>
public DirectorySearchService(IOptions<DirectorySearchOptions> options, IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
var dirSearchOptions = options.Value;
ServerName = dirSearchOptions.ServerName;
Root = dirSearchOptions.Root;
SearchRootPath = $"LDAP://{ServerName}/{Root}";
CustomSearchFilters = dirSearchOptions.CustomSearchFilters;
if(dirSearchOptions.UserCacheExpirationDays is double expirationDays)
_userCacheExpiration = DateTimeOffset.Now.Date.AddDays(expirationDays);
}
/// <summary>
/// Creates the connections to the server and returns a Boolean value that specifies
/// whether the specified username and password are valid.
/// </summary>
/// <param name="userName">The username that is validated on the server. See the Remarks section
/// for more information on the format of userName.</param>
/// <param name="password">The password that is validated on the server.</param>
/// <returns>True if the credentials are valid; otherwise, false.</returns>
public bool ValidateCredentials(string userName, string password)
{
using var context = new PrincipalContext(ContextType.Domain, ServerName, Root);
return context.ValidateCredentials(userName, password);
}
/// <summary>
/// Creates the connections to the server asynchronously and returns a Boolean value that specifies
/// whether the specified username and password are valid.
/// </summary>
/// <param name="userName">The username that is validated on the server. See the Remarks section
/// for more information on the format of userName.</param>
/// <param name="password">The password that is validated on the server.</param>
/// <returns>True if the credentials are valid; otherwise, false.</returns>
public Task<bool> ValidateCredentialsAsync(string userName, string password) => Task.Run(()
=> ValidateCredentials(userName, password));
//TODO: remove unnecessary DataResult
/// <summary>
/// Finds all directory entries matching the specified filter.
/// </summary>
/// <param name="searchRoot">The search root.</param>
/// <param name="filter">The search filter.</param>
/// <param name="searchScope">The search scope.</param>
/// <param name="sizeLimit">The size limit.</param>
/// <param name="properties">The properties to load.</param>
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
public DataResult<IEnumerable<ResultPropertyCollection>> FindAll(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties)
{
List<ResultPropertyCollection> list = new();
using var searcher = new DirectorySearcher()
{
Filter = filter,
SearchScope = searchScope,
SizeLimit = sizeLimit,
SearchRoot = searchRoot
};
if (properties.Length > 0)
{
searcher.PropertiesToLoad.Clear();
foreach (var property in properties)
if(property is not null)
searcher.PropertiesToLoad.Add(property);
}
foreach (SearchResult result in searcher.FindAll())
{
ResultPropertyCollection rpc = result.Properties;
list.Add(rpc);
}
return Result.Success<IEnumerable<ResultPropertyCollection>>(list);
}
/// <summary>
/// Finds all directory entries matching the specified filter asynchronously.
/// </summary>
/// <param name="searchRoot">The search root.</param>
/// <param name="filter">The search filter.</param>
/// <param name="searchScope">The search scope.</param>
/// <param name="sizeLimit">The size limit.</param>
/// <param name="properties">The properties to load.</param>
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
public Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllAsync(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(()
=> FindAll(searchRoot, filter, searchScope, sizeLimit, properties));
/// <summary>
/// Finds all directory entries matching the specified filter, using the user cache.
/// </summary>
/// <param name="username">The username.</param>
/// <param name="filter">The search filter.</param>
/// <param name="searchScope">The search scope.</param>
/// <param name="sizeLimit">The size limit.</param>
/// <param name="properties">The properties to load.</param>
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
public DataResult<IEnumerable<ResultPropertyCollection>> FindAllByUserCache(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties)
{
_memoryCache.TryGetValue(username, out DirectoryEntry? searchRoot);
if (searchRoot is null)
return Result.Fail<IEnumerable<ResultPropertyCollection>>();
return FindAll(searchRoot, filter, searchScope, sizeLimit, properties);
}
/// <summary>
/// Finds all directory entries matching the specified filter asynchronously, using the user cache.
/// </summary>
/// <param name="username">The username.</param>
/// <param name="filter">The search filter.</param>
/// <param name="searchScope">The search scope.</param>
/// <param name="sizeLimit">The size limit.</param>
/// <param name="properties">The properties to load.</param>
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
public Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllByUserCacheAsync(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(()
=> FindAllByUserCache(username, filter, searchScope, sizeLimit, properties));
/// <summary>
/// Sets the search root in the cache.
/// </summary>
/// <param name="username">The directory entry username.</param>
/// <param name="password">The directory entry password.</param>
public void SetSearchRootCache(string username, string password)
{
if (_userCacheExpiration is DateTimeOffset cacheExpiration)
_memoryCache.Set(key: username, new DirectoryEntry(path: SearchRootPath, username: username, password: password), absoluteExpiration: cacheExpiration);
else
_memoryCache.Set(key: username, new DirectoryEntry(path: SearchRootPath, username: username, password: password));
}
/// <summary>
/// Gets the search root from the cache.
/// </summary>
/// <param name="username">The directory entry username.</param>
/// <returns>The cached <see cref="DirectoryEntry"/> if found; otherwise, null.</returns>
public DirectoryEntry? GetSearchRootCache(string username)
{
_memoryCache.TryGetValue(username, out DirectoryEntry? root);
return root;
}
}
}