using System.Diagnostics.CodeAnalysis; using System.DirectoryServices; using Microsoft.Extensions.Caching.Memory; using System.DirectoryServices.AccountManagement; using Microsoft.Extensions.Options; using DigitalData.Core.Abstraction.Application.DTO; using DigitalData.Core.Abstraction.Application; namespace DigitalData.Core.Application { //TODO: rename as DirectorySearcher [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")] 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 CustomSearchFilters { get; } /// /// Initializes a new instance of the class. /// /// The options for directory search. /// The memory cache. /// /// Thrown if the server name or root directory is not configured. /// public DirectorySearchService(IOptions 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); } /// /// Creates the connections to the server and returns a Boolean value that specifies /// whether the specified username and password are valid. /// /// The username that is validated on the server. See the Remarks section /// for more information on the format of userName. /// The password that is validated on the server. /// True if the credentials are valid; otherwise, false. public bool ValidateCredentials(string userName, string password) { using var context = new PrincipalContext(ContextType.Domain, ServerName, Root); return context.ValidateCredentials(userName, password); } /// /// Creates the connections to the server asynchronously and returns a Boolean value that specifies /// whether the specified username and password are valid. /// /// The username that is validated on the server. See the Remarks section /// for more information on the format of userName. /// The password that is validated on the server. /// True if the credentials are valid; otherwise, false. public Task ValidateCredentialsAsync(string userName, string password) => Task.Run(() => ValidateCredentials(userName, password)); //TODO: remove unnecessary DataResult /// /// Finds all directory entries matching the specified filter. /// /// The search root. /// The search filter. /// The search scope. /// The size limit. /// The properties to load. /// A containing the results. public DataResult> FindAll(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) { List 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>(list); } /// /// Finds all directory entries matching the specified filter asynchronously. /// /// The search root. /// The search filter. /// The search scope. /// The size limit. /// The properties to load. /// A containing the results. public Task>> FindAllAsync(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(() => FindAll(searchRoot, filter, searchScope, sizeLimit, properties)); /// /// Finds all directory entries matching the specified filter, using the user cache. /// /// The username. /// The search filter. /// The search scope. /// The size limit. /// The properties to load. /// A containing the results. public DataResult> 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>(); return FindAll(searchRoot, filter, searchScope, sizeLimit, properties); } /// /// Finds all directory entries matching the specified filter asynchronously, using the user cache. /// /// The username. /// The search filter. /// The search scope. /// The size limit. /// The properties to load. /// A containing the results. public Task>> FindAllByUserCacheAsync(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(() => FindAllByUserCache(username, filter, searchScope, sizeLimit, properties)); /// /// Sets the search root in the cache. /// /// The directory entry username. /// The directory entry password. 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)); } /// /// Gets the search root from the cache. /// /// The directory entry username. /// The cached if found; otherwise, null. public DirectoryEntry? GetSearchRootCache(string username) { _memoryCache.TryGetValue(username, out DirectoryEntry? root); return root; } } }