From e0ea1c3681d771200d62eae8e8158b0c5ffac6c3 Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Tue, 29 Sep 2026 16:41:36 -0400 Subject: [PATCH 1/7] Fix DTO & Mocks v1 --- .../LdapData/MockLdapDataSeeder.cs | 233 ++++---- .../MockLdapAttributeAdapter.cs | 2 +- .../MockLdapConnectionFactoryAdapter.cs | 23 +- .../MockLdapPersistentConnectionAdapter.cs | 16 +- ...kLdapPersistentConnectionFactoryAdapter.cs | 10 +- .../NovellLdapConnectionFactoryAdapter.cs | 18 +- .../Bitai.LDAPHelper.DTO.csproj | 6 +- .../LDAPMsADUserAccount.cs | 554 +++++++++++------- src/Bitai.LDAPHelper/AccountManager.cs | 228 ++++--- src/Bitai.LDAPHelper/Authenticator.cs | 6 +- src/Bitai.LDAPHelper/SearchLimits.cs | 14 + src/Bitai.LDAPHelper/Searcher.cs | 144 +++-- tests/Bitai.LDAPHelper.Tests/BaseTests.cs | 6 +- 13 files changed, 756 insertions(+), 504 deletions(-) diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs index a4961ea..f8d64eb 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs @@ -1,3 +1,4 @@ +using System.Xml.Linq; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; using Microsoft.Extensions.Logging; @@ -87,19 +88,19 @@ private void SeedDomainStructure() { // Create domain roots (no RID needed for domains) var domains = new[] { - "DC=holding,DC=latam,DC=com", - "DC=us,DC=latam,DC=com", - "DC=pe,DC=latam,DC=com", - "DC=br,DC=latam,DC=com", - "DC=mx,DC=latam,DC=com", - "DC=cl,DC=latam,DC=com" + "DC=va,DC=bitai,DC=com", + "DC=us,DC=bitai,DC=com", + "DC=pe,DC=bitai,DC=com", + "DC=br,DC=bitai,DC=com", + "DC=mx,DC=bitai,DC=com", + "DC=cl,DC=bitai,DC=com" }; foreach (var domainDN in domains) { var domain = new MockLdapEntryAdapter(domainDN); domain.AddAttribute("objectClass", new[] { "top", "domain", "domainDNS" }); domain.AddAttribute("dc", domainDN.Split(',')[0].Replace("DC=", "")); - domain.AddAttribute("distinguishedName", domainDN); + domain.AddAttribute("containerDN", domainDN); _dataStore.AddOrUpdateEntry(domain); } } @@ -107,44 +108,44 @@ private void SeedDomainStructure() { private void SeedOrganizationalUnits() { var ous = new[] { - // IT Department structure - "OU=IT,DC=holding,DC=latam,DC=com", - "OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", - "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", - "OU=Juniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", - "OU=Interships,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", - "OU=Support,OU=IT,DC=holding,DC=latam,DC=com", - "OU=Interships,OU=Support,OU=IT,DC=us,DC=latam,DC=com", - "OU=Interships,OU=Support,OU=IT,DC=pe,DC=latam,DC=com", - - // US Region - "OU=IT,DC=us,DC=latam,DC=com", - "OU=Sales,DC=us,DC=latam,DC=com", - "OU=Marketing,DC=us,DC=latam,DC=com", - "OU=HR,DC=us,DC=latam,DC=com", + // VA Region + "OU=IT,DC=va,DC=bitai,DC=com", + "OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", + "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", + "OU=Juniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", + "OU=Interships,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", + "OU=Support,OU=IT,DC=va,DC=bitai,DC=com", + + // US Region + "OU=IT,DC=us,DC=bitai,DC=com", + "OU=Interships,OU=Support,OU=IT,DC=us,DC=bitai,DC=com", + "OU=Sales,DC=us,DC=bitai,DC=com", + "OU=Marketing,DC=us,DC=bitai,DC=com", + "OU=HR,DC=us,DC=bitai,DC=com", // PE Region - "OU=IT,DC=pe,DC=latam,DC=com", - "OU=Sales,DC=pe,DC=latam,DC=com", - "OU=Marketing,DC=pe,DC=latam,DC=com", - "OU=HR,DC=pe,DC=latam,DC=com", + "OU=IT,DC=pe,DC=bitai,DC=com", + "OU=Sales,DC=pe,DC=bitai,DC=com", + "OU=Marketing,DC=pe,DC=bitai,DC=com", + "OU=HR,DC=pe,DC=bitai,DC=com", + "OU=Interships,OU=Support,OU=IT,DC=pe,DC=bitai,DC=com", // BR Region - "OU=IT,DC=br,DC=latam,DC=com", - "OU=Sales,DC=br,DC=latam,DC=com", + "OU=IT,DC=br,DC=bitai,DC=com", + "OU=Sales,DC=br,DC=bitai,DC=com", // MX Region - "OU=IT,DC=mx,DC=latam,DC=com", - "OU=Sales,DC=mx,DC=latam,DC=com", + "OU=IT,DC=mx,DC=bitai,DC=com", + "OU=Sales,DC=mx,DC=bitai,DC=com", // CL Region - "OU=IT,DC=cl,DC=latam,DC=com", - "OU=Sales,DC=cl,DC=latam,DC=com", + "OU=IT,DC=cl,DC=bitai,DC=com", + "OU=Sales,DC=cl,DC=bitai,DC=com", // Built-in containers - "CN=Users,DC=holding,DC=latam,DC=com", - "CN=Computers,DC=holding,DC=latam,DC=com", - "CN=Builtin,DC=holding,DC=latam,DC=com" + "CN=Users,DC=va,DC=bitai,DC=com", + "CN=Computers,DC=va,DC=bitai,DC=com", + "CN=Builtin,DC=va,DC=bitai,DC=com" }; foreach (var ouDN in ous) { @@ -153,7 +154,7 @@ private void SeedOrganizationalUnits() { var ouName = ouDN.Split(',')[0].Replace("OU=", "").Replace("CN=", ""); ou.AddAttribute("ou", ouName); ou.AddAttribute("name", ouName); - ou.AddAttribute("distinguishedName", ouDN); + ou.AddAttribute("containerDN", ouDN); _dataStore.AddOrUpdateEntry(ou); } @@ -166,37 +167,37 @@ private void SeedOrganizationalUnits() { private void SeedStandardUsers() { var users = new List { - // IT DevOps - Holding - new UserData("James", "Dockers", "james.dockers", "HOLDING", "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Senior DevOps Engineer"), - new UserData("Sara", "Pikes", "sara.pikes", "HOLDING", "OU=Juniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Junior DevOps Engineer"), - new UserData("Robert", "Miller", "robert.miller", "HOLDING", "OU=Interships,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Intern"), + // IT DevOps - VA + new UserData("James", "Dockers", "james.dockers", "BITAIVA", "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Senior DevOps Engineer"), + new UserData("Sara", "Pikes", "sara.pikes", "BITAIVA", "OU=Juniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Junior DevOps Engineer"), + new UserData("Robert", "Miller", "robert.miller", "BITAIVA", "OU=Interships,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Intern"), // IT Support - US - new UserData("Maria", "Gonzalez", "maria.gonzalez", "US", "OU=Support,OU=IT,DC=us,DC=latam,DC=com", "Support Lead"), - new UserData("John", "Smith", "john.smith", "US", "OU=Interships,OU=Support,OU=IT,DC=us,DC=latam,DC=com", "Support Intern"), + new UserData("Maria", "Gonzalez", "maria.gonzalez", "US", "OU=Support,OU=IT,DC=us,DC=bitai,DC=com", "Support Lead"), + new UserData("John", "Smith", "john.smith", "US", "OU=Interships,OU=Support,OU=IT,DC=us,DC=bitai,DC=com", "Support Intern"), // IT Support - PE - new UserData("Carlos", "Rodriguez", "carlos.rodriguez", "PE", "OU=Support,OU=IT,DC=pe,DC=latam,DC=com", "Support Analyst"), - new UserData("Ana", "Martinez", "ana.martinez", "PE", "OU=Interships,OU=Support,OU=IT,DC=pe,DC=latam,DC=com", "Support Intern"), + new UserData("Carlos", "Rodriguez", "carlos.rodriguez", "PE", "OU=Support,OU=IT,DC=pe,DC=bitai,DC=com", "Support Analyst"), + new UserData("Ana", "Martinez", "ana.martinez", "PE", "OU=Interships,OU=Support,OU=IT,DC=pe,DC=bitai,DC=com", "Support Intern"), // Regional IT - new UserData("Paulo", "Silva", "paulo.silva", "BR", "OU=IT,DC=br,DC=latam,DC=com", "IT Administrator"), - new UserData("Miguel", "Sanchez", "miguel.sanchez", "MX", "OU=IT,DC=mx,DC=latam,DC=com", "IT Administrator"), - new UserData("Fernando", "Lopez", "fernando.lopez", "CL", "OU=IT,DC=cl,DC=latam,DC=com", "IT Administrator"), + new UserData("Paulo", "Silva", "paulo.silva", "BR", "OU=IT,DC=br,DC=bitai,DC=com", "IT Administrator"), + new UserData("Miguel", "Sanchez", "miguel.sanchez", "MX", "OU=IT,DC=mx,DC=bitai,DC=com", "IT Administrator"), + new UserData("Fernando", "Lopez", "fernando.lopez", "CL", "OU=IT,DC=cl,DC=bitai,DC=com", "IT Administrator"), // Additional users for search demos - new UserData("Isaac", "Newton", "isaac.newton", "HOLDING", "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Principal Engineer"), - new UserData("Manuel", "Cordoba", "manuel.cordoba", "HOLDING", "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Tech Lead"), - new UserData("Saint", "Seiya", "saint.seiya", "HOLDING", "OU=Juniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Junior Developer", memberOf: new string[] { "CN=Users,DC=holding,DC=latam,DC=com" }), - new UserData("Ken", "Master", "ken.master", "HOLDING", "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Security Specialist", memberOf: new string[] { "CN=Administrators,CN=Builtin,DC=holding,DC=latam,DC=com" }), + new UserData("Isaac", "Newton", "isaac.newton", "BITAIVA", "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Principal Engineer"), + new UserData("Manuel", "Cordoba", "manuel.cordoba", "BITAIVA", "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Tech Lead"), + new UserData("Saint", "Seiya", "saint.seiya", "BITAIVA", "OU=Juniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Junior Developer", memberOf: new string[] { "CN=Users,DC=va,DC=bitai,DC=com" }), + new UserData("Ken", "Master", "ken.master", "BITAIVA", "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Security Specialist", memberOf: new string[] { "CN=Administrators,CN=Builtin,DC=va,DC=bitai,DC=com" }), // Additional users for realistic environment - new UserData("Alice", "Wonderland", "alice.wonder", "HOLDING", "OU=Support,OU=IT,DC=holding,DC=latam,DC=com", "Support Analyst"), - new UserData("Bruce", "Wayne", "bruce.wayne", "US", "OU=IT,DC=us,DC=latam,DC=com", "Security Architect"), - new UserData("Clark", "Kent", "clark.kent", "US", "OU=IT,DC=us,DC=latam,DC=com", "Journalist"), - new UserData("Diana", "Prince", "diana.prince", "PE", "OU=IT,DC=pe,DC=latam,DC=com", "Security Consultant"), - new UserData("Barry", "Allen", "barry.allen", "BR", "OU=IT,DC=br,DC=latam,DC=com", "Network Engineer"), - new UserData("Arthur", "Curry", "arthur.curry", "MX", "OU=IT,DC=mx,DC=latam,DC=com", "Infrastructure Engineer"), + new UserData("Alice", "Wonderland", "alice.wonder", "BITAIVA", "OU=Support,OU=IT,DC=va,DC=bitai,DC=com", "Support Analyst"), + new UserData("Bruce", "Wayne", "bruce.wayne", "US", "OU=IT,DC=us,DC=bitai,DC=com", "Security Architect"), + new UserData("Clark", "Kent", "clark.kent", "US", "OU=IT,DC=us,DC=bitai,DC=com", "Journalist"), + new UserData("Diana", "Prince", "diana.prince", "PE", "OU=IT,DC=pe,DC=bitai,DC=com", "Security Consultant"), + new UserData("Barry", "Allen", "barry.allen", "BR", "OU=IT,DC=br,DC=bitai,DC=com", "Network Engineer"), + new UserData("Arthur", "Curry", "arthur.curry", "MX", "OU=IT,DC=mx,DC=bitai,DC=com", "Infrastructure Engineer"), }; foreach (var user in users) { @@ -210,8 +211,8 @@ private void SeedDemoSpecificUsers() { "Victor", "Bastidas", "victor.bastidas", - "HOLDING", - "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", + "BITAIVA", + "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Senior DevOps Engineer" ); CreateMockUser(newUser); @@ -221,8 +222,8 @@ private void SeedDemoSpecificUsers() { "Red", "Robbin", "red.robbin", - "HOLDING", - "OU=Interships,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", + "BITAIVA", + "OU=Interships,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Intern" ); CreateMockUser(passwordResetUser); @@ -233,7 +234,7 @@ private void SeedDemoSpecificUsers() { "Pikes", "sara.pikes", "US", - "OU=Interships,OU=Support,OU=IT,DC=us,DC=latam,DC=com", + "OU=Interships,OU=Support,OU=IT,DC=us,DC=bitai,DC=com", "Support Intern", userAccountControl: "514" // Already disabled ); @@ -245,7 +246,7 @@ private void SeedDemoSpecificUsers() { "Cuy", "magic.cuy", "PE", - "OU=Interships,OU=Support,OU=IT,DC=pe,DC=latam,DC=com", + "OU=Interships,OU=Support,OU=IT,DC=pe,DC=bitai,DC=com", "Support Intern" ); CreateMockUser(removeUser); @@ -255,8 +256,8 @@ private void SeedDemoSpecificUsers() { "Ken", "Master", "ken.master", - "HOLDING", - "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", + "BITAIVA", + "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Security Specialist" ); CreateMockUser(groupMemberUser); @@ -283,8 +284,8 @@ private void CreateMockUser(UserData user) { userEntry.AddAttribute("givenName", user.FirstName); userEntry.AddAttribute("displayName", $"{user.FirstName} {user.LastName}"); userEntry.AddAttribute("name", $"{user.FirstName} {user.LastName}"); - userEntry.AddAttribute("mail", $"{user.SAMAccountName}@{user.Domain.ToLower()}.latam.com"); - userEntry.AddAttribute("userPrincipalName", $"{user.SAMAccountName}@{user.Domain.ToLower()}.latam.com"); + userEntry.AddAttribute("mail", $"{user.SAMAccountName}@{user.Domain.ToLower()}.bitai.com"); + userEntry.AddAttribute("userPrincipalName", $"{user.SAMAccountName}@{user.Domain.ToLower()}.bitai.com"); userEntry.AddAttribute("userAccountControl", user.UserAccountControl); userEntry.AddAttribute("objectClass", new[] { "top", "person", "organizationalPerson", "user" }); userEntry.AddAttribute("whenCreated", DateTime.UtcNow.AddDays(-_random.Next(1, 365)).ToString("yyyyMMddHHmmss.0Z")); @@ -292,7 +293,7 @@ private void CreateMockUser(UserData user) { userEntry.AddAttribute("distinguishedName", distinguishedName); userEntry.AddAttribute("title", user.Title); userEntry.AddAttribute("department", user.Department ?? "IT"); - userEntry.AddAttribute("company", $"{user.Domain} LATAM"); + userEntry.AddAttribute("company", $"{user.Domain} BITAI"); userEntry.AddAttribute("telephoneNumber", $"+1-555-{_random.Next(100, 999)}-{_random.Next(1000, 9999)}"); if (!string.IsNullOrEmpty(user.Manager)) { @@ -315,32 +316,32 @@ private void SeedGroups() { var groups = new List { // Global groups - new GroupData("Domain Admins", "CN=Users,DC=holding,DC=latam,DC=com", "DomainAdmins", "Domain Administrators Group"), - new GroupData("Domain Users", "CN=Users,DC=holding,DC=latam,DC=com", "DomainUsers", "All domain users"), - new GroupData("Domain Computers", "CN=Users,DC=holding,DC=latam,DC=com", "DomainComputers", "All domain computers"), + new GroupData("Domain Admins", "CN=Users,DC=va,DC=bitai,DC=com", "DomainAdmins", "Domain Administrators Group"), + new GroupData("Domain Users", "CN=Users,DC=va,DC=bitai,DC=com", "DomainUsers", "All domain users"), + new GroupData("Domain Computers", "CN=Users,DC=va,DC=bitai,DC=com", "DomainComputers", "All domain computers"), // IT Department groups - new GroupData("IT Admins", "OU=IT,DC=holding,DC=latam,DC=com", "ITAdmins", "IT Administrators"), - new GroupData("DevOps Engineers", "OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "DevOpsEng", "DevOps Engineering Team"), - new GroupData("Senior DevOps", "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "SeniorDevOps", "Senior DevOps Engineers"), - new GroupData("Junior DevOps", "OU=Juniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "JuniorDevOps", "Junior DevOps Engineers"), - new GroupData("Interns", "OU=Interships,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Interns", "Intern Program Participants"), + new GroupData("IT Admins", "OU=IT,DC=va,DC=bitai,DC=com", "ITAdmins", "IT Administrators"), + new GroupData("DevOps Engineers", "OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "DevOpsEng", "DevOps Engineering Team"), + new GroupData("Senior DevOps", "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "SeniorDevOps", "Senior DevOps Engineers"), + new GroupData("Junior DevOps", "OU=Juniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "JuniorDevOps", "Junior DevOps Engineers"), + new GroupData("Interns", "OU=Interships,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Interns", "Intern Program Participants"), // Support groups - new GroupData("Support Team", "OU=Support,OU=IT,DC=holding,DC=latam,DC=com", "SupportTeam", "Support Department"), - new GroupData("US Support", "OU=Support,OU=IT,DC=us,DC=latam,DC=com", "USSupport", "US Support Team"), - new GroupData("PE Support", "OU=Support,OU=IT,DC=pe,DC=latam,DC=com", "PESupport", "Peru Support Team"), + new GroupData("Support Team", "OU=Support,OU=IT,DC=va,DC=bitai,DC=com", "SupportTeam", "Support Department"), + new GroupData("US Support", "OU=Support,OU=IT,DC=us,DC=bitai,DC=com", "USSupport", "US Support Team"), + new GroupData("PE Support", "OU=Support,OU=IT,DC=pe,DC=bitai,DC=com", "PESupport", "Peru Support Team"), // Regional groups - new GroupData("US IT Team", "OU=IT,DC=us,DC=latam,DC=com", "USITTeam", "US IT Department"), - new GroupData("PE IT Team", "OU=IT,DC=pe,DC=latam,DC=com", "PEITTeam", "Peru IT Department"), - new GroupData("BR IT Team", "OU=IT,DC=br,DC=latam,DC=com", "BRITTeam", "Brazil IT Department"), - new GroupData("MX IT Team", "OU=IT,DC=mx,DC=latam,DC=com", "MXITTeam", "Mexico IT Department"), - new GroupData("CL IT Team", "OU=IT,DC=cl,DC=latam,DC=com", "CLITTeam", "Chile IT Department"), + new GroupData("US IT Team", "OU=IT,DC=us,DC=bitai,DC=com", "USITTeam", "US IT Department"), + new GroupData("PE IT Team", "OU=IT,DC=pe,DC=bitai,DC=com", "PEITTeam", "Peru IT Department"), + new GroupData("BR IT Team", "OU=IT,DC=br,DC=bitai,DC=com", "BRITTeam", "Brazil IT Department"), + new GroupData("MX IT Team", "OU=IT,DC=mx,DC=bitai,DC=com", "MXITTeam", "Mexico IT Department"), + new GroupData("CL IT Team", "OU=IT,DC=cl,DC=bitai,DC=com", "CLITTeam", "Chile IT Department"), // Security groups - new GroupData("Administrators", "CN=Builtin,DC=holding,DC=latam,DC=com", "Administrators", "Built-in Administrators Group"), - new GroupData("Security Analysts", "OU=IT,DC=holding,DC=latam,DC=com", "SecurityAnalysts", "Security Team"), + new GroupData("Administrators", "CN=Builtin,DC=va,DC=bitai,DC=com", "Administrators", "Built-in Administrators Group"), + new GroupData("Security Analysts", "OU=IT,DC=va,DC=bitai,DC=com", "SecurityAnalysts", "Security Team"), }; foreach (var group in groups) { @@ -352,8 +353,8 @@ private void SeedDemoSpecificGroups() { // Groups referenced in JSON config var demoSpecificGroups = new[] { - new GroupData("Administrators", "CN=Builtin,DC=holding,DC=latam,DC=com", "Administrators", "Built-in Administrators"), - new GroupData("DevOps Leaders", "OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "DevOpsLeaders", "DevOps Leadership Team") + new GroupData("Administrators", "CN=Builtin,DC=va,DC=bitai,DC=com", "Administrators", "Built-in Administrators"), + new GroupData("DevOps Leaders", "OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "DevOpsLeaders", "DevOps Leadership Team") }; foreach (var group in demoSpecificGroups) { @@ -384,7 +385,7 @@ private void CreateMockGroup(GroupData group) { groupEntry.AddAttribute("objectClass", new[] { "top", "group" }); groupEntry.AddAttribute("groupType", "-2147483640"); // Universal security group groupEntry.AddAttribute("whenCreated", DateTime.UtcNow.AddDays(-_random.Next(1, 365)).ToString("yyyyMMddHHmmss.0Z")); - groupEntry.AddAttribute("distinguishedName", groupDistinguishedName); + groupEntry.AddAttribute("containerDN", groupDistinguishedName); _dataStore.AddOrUpdateEntry(groupEntry); _logger.LogDebug($"Created group: {group.SAMAccountName} ({groupDistinguishedName}) with RID: {rid}"); @@ -398,19 +399,19 @@ private void SeedComputerEntries() { var computers = new[] { // Servers - new ComputerData("DEVSERVER01", "OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Development Server", "server"), - new ComputerData("DBSERVER01", "OU=IT,DC=holding,DC=latam,DC=com", "Database Server", "server"), - new ComputerData("WEBSERVER01", "OU=IT,DC=us,DC=latam,DC=com", "Web Server US", "server"), - new ComputerData("APPSERVER01", "OU=IT,DC=pe,DC=latam,DC=com", "Application Server PE", "server"), - new ComputerData("FILESERVER01", "OU=IT,DC=br,DC=latam,DC=com", "File Server BR", "server"), - new ComputerData("MAILSERVER01", "OU=IT,DC=mx,DC=latam,DC=com", "Mail Server MX", "server"), - new ComputerData("MONITOR01", "OU=IT,DC=cl,DC=latam,DC=com", "Monitoring Server", "server"), + new ComputerData("DEVSERVER01", "OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Development Server", "server"), + new ComputerData("DBSERVER01", "OU=IT,DC=va,DC=bitai,DC=com", "Database Server", "server"), + new ComputerData("WEBSERVER01", "OU=IT,DC=us,DC=bitai,DC=com", "Web Server US", "server"), + new ComputerData("APPSERVER01", "OU=IT,DC=pe,DC=bitai,DC=com", "Application Server PE", "server"), + new ComputerData("FILESERVER01", "OU=IT,DC=br,DC=bitai,DC=com", "File Server BR", "server"), + new ComputerData("MAILSERVER01", "OU=IT,DC=mx,DC=bitai,DC=com", "Mail Server MX", "server"), + new ComputerData("MONITOR01", "OU=IT,DC=cl,DC=bitai,DC=com", "Monitoring Server", "server"), // Workstations - new ComputerData("WS-USA-001", "CN=Computers,DC=us,DC=latam,DC=com", "USA Workstation 001", "workstation"), - new ComputerData("WS-PE-001", "CN=Computers,DC=pe,DC=latam,DC=com", "Peru Workstation 001", "workstation"), - new ComputerData("WS-BR-001", "CN=Computers,DC=br,DC=latam,DC=com", "Brazil Workstation 001", "workstation"), - new ComputerData("DEV-LAPTOP-001", "OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com", "Developer Laptop", "workstation"), + new ComputerData("WS-USA-001", "CN=Computers,DC=us,DC=bitai,DC=com", "USA Workstation 001", "workstation"), + new ComputerData("WS-PE-001", "CN=Computers,DC=pe,DC=bitai,DC=com", "Peru Workstation 001", "workstation"), + new ComputerData("WS-BR-001", "CN=Computers,DC=br,DC=bitai,DC=com", "Brazil Workstation 001", "workstation"), + new ComputerData("DEV-LAPTOP-001", "OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com", "Developer Laptop", "workstation"), }; foreach (var computer in computers) { @@ -434,7 +435,7 @@ private void CreateMockComputer(ComputerData computer) { computerEntry.AddAttribute("objectGuid", Guid.NewGuid().ToByteArray()); computerEntry.AddAttribute("sAMAccountName", $"{computer.Name}$"); computerEntry.AddAttribute("sAMAccountType", "805306369"); // Computer account - computerEntry.AddAttribute("cn", computer.Name); + computerEntry.AddAttribute("Name", computer.Name); computerEntry.AddAttribute("name", computer.Name); computerEntry.AddAttribute("displayName", computer.Description); computerEntry.AddAttribute("description", computer.Description); @@ -442,9 +443,9 @@ private void CreateMockComputer(ComputerData computer) { computerEntry.AddAttribute("userAccountControl", "4096"); // Workstation/server account computerEntry.AddAttribute("operatingSystem", computer.Type == "server" ? "Windows Server 2022" : "Windows 11 Pro"); computerEntry.AddAttribute("operatingSystemVersion", "10.0 (20348)"); - computerEntry.AddAttribute("dNSHostName", $"{computer.Name}.latam.com"); + computerEntry.AddAttribute("dNSHostName", $"{computer.Name}.bitai.com"); computerEntry.AddAttribute("whenCreated", DateTime.UtcNow.AddDays(-_random.Next(1, 365)).ToString("yyyyMMddHHmmss.0Z")); - computerEntry.AddAttribute("distinguishedName", distinguishedName); + computerEntry.AddAttribute("containerDN", distinguishedName); _dataStore.AddOrUpdateEntry(computerEntry); _logger.LogDebug($"Created computer: {computer.Name} ({distinguishedName}) with RID: {rid}"); @@ -543,13 +544,13 @@ private void SeedAdditionalRelationships() { // Add manager relationships var managers = new Dictionary { - { "james.dockers", "CN=Isaac Newton,OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com" }, - { "sara.pikes", "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com" }, - { "robert.miller", "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com" }, - { "maria.gonzalez", "CN=Bruce Wayne,OU=IT,DC=us,DC=latam,DC=com" }, - { "carlos.rodriguez", "CN=Diana Prince,OU=IT,DC=pe,DC=latam,DC=com" }, - { "saint.seiya", "CN=Manuel Cordoba,OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com" }, - { "ken.master", "CN=Isaac Newton,OU=Seniors,OU=DevOps,OU=IT,DC=holding,DC=latam,DC=com" }, + { "james.dockers", "CN=Isaac Newton,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com" }, + { "sara.pikes", "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com" }, + { "robert.miller", "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com" }, + { "maria.gonzalez", "CN=Bruce Wayne,OU=IT,DC=us,DC=bitai,DC=com" }, + { "carlos.rodriguez", "CN=Diana Prince,OU=IT,DC=pe,DC=bitai,DC=com" }, + { "saint.seiya", "CN=Manuel Cordoba,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com" }, + { "ken.master", "CN=Isaac Newton,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com" }, }; foreach (var relation in managers) { @@ -660,6 +661,8 @@ private class UserData public string Manager { get; set; } public string[] MemberOf { get; set; } + + public UserData(string firstName, string lastName, string samAccountName, string domain, string containerDN, string title, string department = "IT", string userAccountControl = "512", string manager = null, string[] memberOf = null) { @@ -679,18 +682,20 @@ public UserData(string firstName, string lastName, string samAccountName, private class GroupData { public string Name { get; set; } - public string ContainerDistinguishedName { get; set; } + public string ContainerDN { get; set; } public string SAMAccountName { get; set; } public string Description { get; set; } public string GeneratedDistinguishedName { get; private set; } - public GroupData(string name, string distinguishedName, string samAccountName, string description) { + + + public GroupData(string name, string containerDN, string samAccountName, string description) { Name = name; - ContainerDistinguishedName = distinguishedName; + ContainerDN = containerDN; SAMAccountName = samAccountName; Description = description; - GeneratedDistinguishedName = $"CN={name},{distinguishedName}"; + GeneratedDistinguishedName = $"CN={name},{containerDN}"; } } diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs index 61af718..042a3e6 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapAttributeAdapter.cs @@ -228,7 +228,7 @@ public bool HasValue(byte[] value) { } /// - /// Returns the base name of the attribute (e.g., "cn" from "cn;lang-ja;phonetic") + /// Returns the base name of the attribute (e.g., "Name" from "Name;lang-ja;phonetic") /// public string GetBaseName() { return _baseName; diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs index f3b4167..fbf0305 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs @@ -1,5 +1,3 @@ -using Bitai.LDAPHelper.LdapAdapters; - namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// @@ -7,11 +5,9 @@ namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// public class MockLdapConnectionFactoryAdapter : ILdapConnectionFactoryAdapter { - private readonly MockLdapConnectionAdapter _connection; - - public MockLdapConnectionFactoryAdapter(MockLdapConnectionAdapter connection) { - _connection = connection; - } + //public MockLdapConnectionFactoryAdapter(MockLdapConnectionAdapter connection) { + // connection = connection; + //} public async Task CreateConnectionAsync( IConnectionInfo connectionInfo, @@ -19,13 +15,16 @@ public async Task CreateConnectionAsync( string password, bool bindRequired = true) { - _connection.ConnectionTimeout = connectionInfo.ConnectionTimeout; - _connection.SecureSocketLayer = connectionInfo.UseSSL; + var connection = new MockLdapConnectionAdapter() + { + ConnectionTimeout = connectionInfo.ConnectionTimeout, + SecureSocketLayer = connectionInfo.UseSSL + }; - await _connection.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); + await connection.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); try { - await _connection.BindAsync(userAccount, password); + await connection.BindAsync(userAccount, password); } catch (LdapOperationException) { if (bindRequired) @@ -35,6 +34,6 @@ public async Task CreateConnectionAsync( throw; } - return (ILdapConnectionAdapter)_connection; + return (ILdapConnectionAdapter)connection; } } diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs index e6a6b08..684d7d9 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs @@ -11,11 +11,15 @@ public class MockLdapPersistentConnectionAdapter : MockLdapConnectionAdapter { private readonly MockLdapDataStore _dataStore; + + public MockLdapPersistentConnectionAdapter() : base() { - _dataStore = MockLdapDataStore.Instance; + _dataStore = MockLdapDataStore.Instance; } + + public override Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAdapter attributes) { var mockAttributes = (MockLdapAttributeSetAdapter)attributes; @@ -170,6 +174,8 @@ public override Task SearchAsync( return Task.FromResult(mockQueue); } + + private bool MatchesFilter(MockLdapEntryAdapter entry, string filter) { // Simple wildcard matching for demo purposes @@ -184,10 +190,10 @@ private bool MatchesFilter(MockLdapEntryAdapter entry, string filter) var value = ExtractFilterValue(filter, "distinguishedName"); return MatchWildcard(entry.DistinguishedName, value); } - else if (filter.Contains("(cn=")) + else if (filter.Contains("(Name=")) { - var value = ExtractFilterValue(filter, "cn"); - var cn = entry.GetAttributeSet().GetAttribute("cn")?.StringValue; + var value = ExtractFilterValue(filter, "Name"); + var cn = entry.GetAttributeSet().GetAttribute("Name")?.StringValue; return MatchWildcard(cn, value); } else if (filter.Contains("(objectSid=")) @@ -247,6 +253,8 @@ private bool MatchWildcard(string value, string pattern) return value.Equals(pattern, StringComparison.OrdinalIgnoreCase); } + + private static string ConvertByteToStringSid(byte[] sidBytes) { short subAuthorityCount = 0; diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs index d0d93ed..d74a370 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs @@ -1,4 +1,6 @@ using Bitai.LDAPHelper.LdapAdapters; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging; namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; @@ -8,10 +10,16 @@ namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; public class MockLdapPersistentConnectionFactoryAdapter : ILdapConnectionFactoryAdapter { private readonly MockLdapPersistentConnectionAdapter _connection; + private readonly ILogger _logger; - public MockLdapPersistentConnectionFactoryAdapter() + public MockLdapPersistentConnectionFactoryAdapter(ILogger logger, ILogger seederLogger) { + _logger = logger; _connection = new MockLdapPersistentConnectionAdapter(); + + var _seeder = new MockLdapDataSeeder(seederLogger); + _seeder.SeedAllData(); + _seeder.PrintAllData(); } public Task CreateConnectionAsync( diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionFactoryAdapter.cs index b0789a9..0bf76ce 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.Novell/NovellLdapConnectionFactoryAdapter.cs @@ -14,28 +14,34 @@ public async Task CreateConnectionAsync( IConnectionInfo connectionInfo, string userAccount, string password, - bool bindRequired = true) { - var ldapConnection = new LdapConnection { + bool bindRequired = true) + { + var ldapConnection = new LdapConnection + { ConnectionTimeout = connectionInfo.ConnectionTimeout * 1000 }; var adapter = new NovellLdapConnectionAdapter(ldapConnection); - if (connectionInfo.UseSSL) { + if (connectionInfo.UseSSL) + { adapter.SecureSocketLayer = true; adapter.ServerCertificateValidationByPass(); } await adapter.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); - try { + try + { await adapter.BindAsync(userAccount, password); } - catch (LdapException) { + catch (LdapException) + { if (bindRequired) throw; } - catch (Exception) { + catch (Exception) + { throw; } diff --git a/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj b/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj index 380d235..24284ff 100644 --- a/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj +++ b/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj @@ -2,9 +2,9 @@ net10.0 - 10.0.1 - 10.0.1 - 10.0.1 + 10.1.0 + 10.1.0 + 10.1.0 Bitai.LDAPHelper.DTO true Viko Bastidas (BITAI) diff --git a/src/Bitai.LDAPHelper.DTO/LDAPMsADUserAccount.cs b/src/Bitai.LDAPHelper.DTO/LDAPMsADUserAccount.cs index e53ab83..a4e9d7a 100644 --- a/src/Bitai.LDAPHelper.DTO/LDAPMsADUserAccount.cs +++ b/src/Bitai.LDAPHelper.DTO/LDAPMsADUserAccount.cs @@ -6,202 +6,360 @@ namespace Bitai.LDAPHelper.DTO { - /// - /// User account for Microsoft Active Directory - /// - /// Resources of interest: - /// - https://www.rlmueller.net/Name_Attributes.htm - /// - public class LDAPMsADUserAccount : ISecureCloningCredential - { - private string distinguishedNameOfContainer; - private string givenName; - private string sn; - private string cn; - private string name; - private string displayName; - private string description; - private string distinguishedName; - private string[] memberOf; - private string[] objectClass; - private string samAccountName; - private string userPrincipalName; - private string userAccountControl; - private UserAccountControlFlagsForMsAD? userAccountControlFlags; - private string department; - private string telephoneNumber; - private string mail; - private string password; - - - - /// - /// Default constructor - /// - public LDAPMsADUserAccount() - { - UserAccountControl = $"{UserAccountControlFlagsForMsAD.NORMAL_ACCOUNT.ToString()},{UserAccountControlFlagsForMsAD.DONT_EXPIRE_PASSWORD}"; - } - - /// - /// Constructor - /// - /// Container distinguished name of user account. - public LDAPMsADUserAccount(string distinguishedNmaeOfContainer) : this() - { - if (string.IsNullOrEmpty(distinguishedNmaeOfContainer)) - throw new ArgumentNullException(nameof(distinguishedNmaeOfContainer)); - - DistinguishedNameOfContainer = distinguishedNmaeOfContainer; - } - - - - /// - /// Gets or sets the distinguished name of the container where the user account should be created. - /// - public string DistinguishedNameOfContainer { get => distinguishedNameOfContainer; set => distinguishedNameOfContainer = value; } - - /// - /// Gets or sets the user's given name. - /// - public string GivenName { get => givenName; set => givenName = value; } - - /// - /// Gets or sets the user's surname. - /// - public string Sn { get => sn; set => sn = value; } - - /// - /// Gets or sets the user common name (CN). - /// - public string Cn { get => cn; set => cn = value; } - - /// - /// Gets or sets the LDAP name attribute. - /// - public string Name { get => name; set => name = value; } - - /// - /// Gets or sets the display name. - /// - public string DisplayName { get => displayName; set => displayName = value; } - - /// - /// Gets or sets the account description. - /// - public string Description { get => description; set => description = value; } - - /// - /// Gets or sets the full distinguished name of the user account. - /// - public string DistinguishedName { get => distinguishedName; set => distinguishedName = value; } - - /// - /// Gets or sets parent groups (distinguished names) this account belongs to. - /// - public string[] MemberOf { get => memberOf; set => memberOf = value; } - - /// - /// Gets or sets LDAP object classes for this account. - /// - public string[] ObjectClass { get => objectClass; set => objectClass = value; } - - /// - /// Gets or sets the sAMAccountName value. - /// - public string SAMAccountName { get => samAccountName; set => samAccountName = value; } - - /// - /// Gets or sets the user principal name (UPN). - /// - public string UserPrincipalName { get => userPrincipalName; set => userPrincipalName = value; } - /// - /// Gets or sets user-account-control flags as a comma-separated list of names. - /// - public string UserAccountControl - { - get => userAccountControl; - set - { - var tempValue = value; - - if (!string.IsNullOrEmpty(tempValue)) - { - var flagNames = tempValue.Split(','); - int totalFlagValue = 0; - foreach (var flagName in flagNames) - { - UserAccountControlFlagsForMsAD parsedFlag; - if (!Enum.TryParse(flagName, out parsedFlag)) - throw new InvalidCastException($"Unable to assign property {nameof(UserAccountControl)}. Can not parse {flagName} to {nameof(UserAccountControlFlagsForMsAD)}"); - - totalFlagValue += (int)parsedFlag; - } - - userAccountControlFlags = (UserAccountControlFlagsForMsAD)Enum.ToObject(typeof(UserAccountControlFlagsForMsAD), totalFlagValue); - - userAccountControl = tempValue; - } - else - { - userAccountControlFlags = null; - userAccountControl = value; - } - } - } - /// - /// Gets or sets the department. - /// - public string Department { get => department; set => department = value; } - - /// - /// Gets or sets the phone number. - /// - public string TelephoneNumber { get => telephoneNumber; set => telephoneNumber = value; } - - /// - /// Gets or sets the email address. - /// - public string Mail { get => mail; set => mail = value; } - - /// - /// Gets or sets the account password. - /// - public string Password { get => password; set => password = value; } - - /// - /// Gets parsed account-control flags derived from . - /// - public UserAccountControlFlagsForMsAD? UserAccountControlFlags { get => userAccountControlFlags; } - - - - /// - /// Creates a secure clone with password masked. - /// - /// A cloned account suitable for logging/transport. - public LDAPMsADUserAccount SecureClone() - { - return new LDAPMsADUserAccount - { - Cn = Cn, - Department = Department, - Description = Description, - DisplayName = DisplayName, - DistinguishedName = DistinguishedName, - DistinguishedNameOfContainer = DistinguishedNameOfContainer, - GivenName = GivenName, - Mail = Mail, - MemberOf = (string[])MemberOf?.Clone(), - Name = Name, - ObjectClass = (string[])ObjectClass?.Clone(), - Password = "*****", - SAMAccountName = SAMAccountName, - TelephoneNumber = TelephoneNumber, - UserAccountControl = UserAccountControl, - UserPrincipalName = UserPrincipalName - }; - } - } + /// + /// User account for Microsoft Active Directory (immutable record version) + /// + /// Resources of interest: + /// - https://www.rlmueller.net/Name_Attributes.htm + /// + public record LDAPMsADUserAccount : ISecureCloningCredential + { + /// + /// Default constructor + /// + public LDAPMsADUserAccount() + { + UserAccountControl = + $"{UserAccountControlFlagsForMsAD.NORMAL_ACCOUNT},{UserAccountControlFlagsForMsAD.DONT_EXPIRE_PASSWORD}"; + } + + /// + /// Constructor + /// + /// Container distinguished name of user account. + public LDAPMsADUserAccount(string distinguishedNameOfContainer) : this() + { + DistinguishedNameOfContainer = distinguishedNameOfContainer + ?? throw new ArgumentNullException(nameof(distinguishedNameOfContainer)); + } + + /// + /// Constructor that initializes all properties at once. Any parameter left at its + /// default keeps the same behavior as the parameterless/single-arg constructors above + /// (e.g. still defaults to NORMAL_ACCOUNT + DONT_EXPIRE_PASSWORD). + /// Calls with just still resolve to the + /// simpler constructor above, so existing call sites are unaffected. + /// + public LDAPMsADUserAccount( + string distinguishedNameOfContainer, + string? givenName = null, + string? sn = null, + string? cn = null, + string? name = null, + string? displayName = null, + string? description = null, + string? distinguishedName = null, + string[]? objectClass = null, + string? samAccountName = null, + string? userPrincipalName = null, + string? userAccountControl = null, + string? department = null, + string? telephoneNumber = null, + string? mail = null, + string? password = null) : this(distinguishedNameOfContainer) + { + GivenName = givenName; + Sn = sn; + Cn = cn; + Name = name; + DisplayName = displayName; + Description = description; + DistinguishedName = distinguishedName; + ObjectClass = objectClass; + SAMAccountName = samAccountName; + UserPrincipalName = userPrincipalName; + Department = department; + TelephoneNumber = telephoneNumber; + Mail = mail; + Password = password; + + // Only overrides the default UAC set by the parameterless constructor + // when the caller explicitly passed one — preserves current default behavior. + if (userAccountControl != null) + UserAccountControl = userAccountControl; + } + + /// + /// Gets the distinguished name of the container where the user account should be created. + /// + public string? DistinguishedNameOfContainer { get; init; } + + /// + /// Gets the user's given name. + /// + public string? GivenName { get; init; } + + /// + /// Gets the user's surname. + /// + public string? Sn { get; init; } + + /// + /// Gets the user common name (CN). + /// + public string? Cn { get; init; } + + /// + /// Gets the LDAP name attribute. + /// + public string? Name { get; init; } + + /// + /// Gets the display name. + /// + public string? DisplayName { get; init; } + + /// + /// Gets the account description. + /// + public string? Description { get; init; } + + /// + /// Gets the full distinguished name of the user account. + /// + public string? DistinguishedName { get; init; } + + /// + /// Gets LDAP object classes for this account. + /// + public string[]? ObjectClass { get; init; } + + /// + /// Gets the sAMAccountName value. + /// + public string? SAMAccountName { get; init; } + + /// + /// Gets the user principal name (UPN). + /// + public string? UserPrincipalName { get; init; } + + /// + /// Gets user-account-control flags as a comma-separated list of + /// names. + /// Validated eagerly via the init accessor so an invalid value still fails fast, + /// the same way the original setter did. + /// + public string? UserAccountControl + { + get => userAccountControl; + init + { + userAccountControl = value; + userAccountControlFlags = ParseFlags(value); + } + } + private readonly string? userAccountControl; + + /// + /// Gets the department. + /// + public string? Department { get; init; } + + /// + /// Gets the phone number. + /// + public string? TelephoneNumber { get; init; } + + /// + /// Gets the email address. + /// + public string? Mail { get; init; } + + /// + /// Gets the account password. + /// + public string? Password { get; init; } + + /// + /// Gets parsed account-control flags derived from . + /// + public UserAccountControlFlagsForMsAD? UserAccountControlFlags => userAccountControlFlags; + private readonly UserAccountControlFlagsForMsAD? userAccountControlFlags; + + private static UserAccountControlFlagsForMsAD? ParseFlags(string? value) + { + if (string.IsNullOrEmpty(value)) + return null; + + int totalFlagValue = 0; + foreach (var flagName in value.Split(',')) + { + if (!Enum.TryParse(flagName.Trim(), out var parsedFlag)) + throw new InvalidCastException( + $"Unable to assign property {nameof(UserAccountControl)}. Can not parse {flagName} to {nameof(UserAccountControlFlagsForMsAD)}"); + + totalFlagValue += (int)parsedFlag; + } + + return (UserAccountControlFlagsForMsAD)Enum.ToObject(typeof(UserAccountControlFlagsForMsAD), totalFlagValue); + } + + /// + /// Creates a secure clone with password masked. + /// + /// A cloned account suitable for logging/transport. + public LDAPMsADUserAccount SecureClone() => + this with + { + Password = "*****", + ObjectClass = (string[]?)ObjectClass?.Clone() + }; + } + + + ///// + ///// User account for Microsoft Active Directory (immutable record version) + ///// + ///// Resources of interest: + ///// - https://www.rlmueller.net/Name_Attributes.htm + ///// + //public record LDAPMsADUserAccount : ISecureCloningCredential + //{ + // /// + // /// Default constructor + // /// + // public LDAPMsADUserAccount() + // { + // UserAccountControl = + // $"{UserAccountControlFlagsForMsAD.NORMAL_ACCOUNT},{UserAccountControlFlagsForMsAD.DONT_EXPIRE_PASSWORD}"; + // } + + // /// + // /// Constructor + // /// + // /// Container distinguished name of user account. + // public LDAPMsADUserAccount(string distinguishedNameOfContainer) : this() + // { + // DistinguishedNameOfContainer = distinguishedNameOfContainer + // ?? throw new ArgumentNullException(nameof(distinguishedNameOfContainer)); + // } + + // /// + // /// Gets the distinguished name of the container where the user account should be created. + // /// + // public string? DistinguishedNameOfContainer { get; init; } + + // /// + // /// Gets the user's given name. + // /// + // public string? GivenName { get; init; } + + // /// + // /// Gets the user's surname. + // /// + // public string? Sn { get; init; } + + // /// + // /// Gets the user common name (CN). + // /// + // public string? Cn { get; init; } + + // /// + // /// Gets the LDAP name attribute. + // /// + // public string? Name { get; init; } + + // /// + // /// Gets the display name. + // /// + // public string? DisplayName { get; init; } + + // /// + // /// Gets the account description. + // /// + // public string? Description { get; init; } + + // /// + // /// Gets the full distinguished name of the user account. + // /// + // public string? DistinguishedName { get; init; } + + // /// + // /// Gets LDAP object classes for this account. + // /// + // public string[]? ObjectClass { get; init; } + + // /// + // /// Gets the sAMAccountName value. + // /// + // public string? SAMAccountName { get; init; } + + // /// + // /// Gets the user principal name (UPN). + // /// + // public string? UserPrincipalName { get; init; } + + // /// + // /// Gets user-account-control flags as a comma-separated list of + // /// names. + // /// Validated eagerly via the init accessor so an invalid value still fails fast, + // /// the same way the original setter did. + // /// + // public string? UserAccountControl + // { + // get => userAccountControl; + // init + // { + // userAccountControl = value; + // userAccountControlFlags = ParseFlags(value); + // } + // } + // private readonly string? userAccountControl; + + // /// + // /// Gets the department. + // /// + // public string? Department { get; init; } + + // /// + // /// Gets the phone number. + // /// + // public string? TelephoneNumber { get; init; } + + // /// + // /// Gets the email address. + // /// + // public string? Mail { get; init; } + + // /// + // /// Gets the account password. + // /// + // public string? Password { get; init; } + + // /// + // /// Gets parsed account-control flags derived from . + // /// + // public UserAccountControlFlagsForMsAD? UserAccountControlFlags => userAccountControlFlags; + // private readonly UserAccountControlFlagsForMsAD? userAccountControlFlags; + + // private static UserAccountControlFlagsForMsAD? ParseFlags(string? value) + // { + // if (string.IsNullOrEmpty(value)) + // return null; + + // int totalFlagValue = 0; + // foreach (var flagName in value.Split(',')) + // { + // if (!Enum.TryParse(flagName.Trim(), out var parsedFlag)) + // throw new InvalidCastException( + // $"Unable to assign property {nameof(UserAccountControl)}. Can not parse {flagName} to {nameof(UserAccountControlFlagsForMsAD)}"); + + // totalFlagValue += (int)parsedFlag; + // } + + // return (UserAccountControlFlagsForMsAD)Enum.ToObject(typeof(UserAccountControlFlagsForMsAD), totalFlagValue); + // } + + // /// + // /// Creates a secure clone with password masked. + // /// + // /// A cloned account suitable for logging/transport. + // public LDAPMsADUserAccount SecureClone() => + // this with + // { + // Password = "*****", + // ObjectClass = (string[]?)ObjectClass?.Clone() + // }; + //} } diff --git a/src/Bitai.LDAPHelper/AccountManager.cs b/src/Bitai.LDAPHelper/AccountManager.cs index e14515a..cb1f83f 100644 --- a/src/Bitai.LDAPHelper/AccountManager.cs +++ b/src/Bitai.LDAPHelper/AccountManager.cs @@ -14,7 +14,7 @@ namespace Bitai.LDAPHelper /// Provides account-management operations for LDAP/Active Directory entries. /// public class AccountManager : BaseHelper - { + { #region Constructors /// /// Initializes a new instance of the class. @@ -22,7 +22,8 @@ public class AccountManager : BaseHelper /// Client configuration containing connection, credential, and search settings. /// LDAP connection factory abstraction. public AccountManager(ClientConfiguration clientConfiguration, ILdapConnectionFactoryAdapter connectionFactory) - : base(clientConfiguration, connectionFactory) { + : base(clientConfiguration, connectionFactory) + { } /// @@ -33,20 +34,11 @@ public AccountManager(ClientConfiguration clientConfiguration, ILdapConnectionFa /// Credential used for management operations. /// LDAP connection factory abstraction. public AccountManager(ConnectionInfo connectionInfo, SearchLimits searchLimits, DTO.LDAPDomainAccountCredential domainAccountCredential, ILdapConnectionFactoryAdapter connectionFactory) - : base(connectionInfo, searchLimits, domainAccountCredential, connectionFactory) { + : base(connectionInfo, searchLimits, domainAccountCredential, connectionFactory) + { } #endregion - /// - /// Initializes the account distinguished name when it is missing. - /// - /// User-account model to normalize. - public void InitializeMissingMsADUserAccountDN(LDAPMsADUserAccount userAccount) - { - if (string.IsNullOrEmpty(userAccount.DistinguishedName)) - userAccount.DistinguishedName = $"CN={userAccount.Cn},{userAccount.DistinguishedNameOfContainer}"; - } - /// /// Create a username in MS Active Directory service /// https://www.rlmueller.net/Name_Attributes.htm @@ -62,6 +54,9 @@ public async Task CreateUserAccountForMsAD(LDAP if (string.IsNullOrEmpty(newUserAccount.DistinguishedNameOfContainer)) throw new DataValidationException($"{nameof(newUserAccount.DistinguishedNameOfContainer)} is required."); + if (string.IsNullOrEmpty(newUserAccount.DistinguishedName)) + throw new DataValidationException($"{nameof(newUserAccount.DistinguishedName)} is required. Set the value: CN={newUserAccount.Cn},{newUserAccount.DistinguishedNameOfContainer}"); + if (string.IsNullOrEmpty(newUserAccount.Cn)) throw new DataValidationException($"{nameof(newUserAccount.Cn)} is required."); @@ -75,9 +70,6 @@ public async Task CreateUserAccountForMsAD(LDAP throw new DataValidationException($"{nameof(newUserAccount.ObjectClass)} is required."); #endregion - //Generate username DistinguishedName LDAP attribute - InitializeMissingMsADUserAccountDN(newUserAccount); - LDAPEntry checkUserAccount = null; // Check whether an entry with the same distinguished name already exists in the directory before creating a new user. Since the distinguished name must be unique, a duplicate entry cannot be created. If one already exists, return an error or ask for a different name. try @@ -108,7 +100,8 @@ public async Task CreateUserAccountForMsAD(LDAP throw new DuplicateNameException($"The {EntryAttribute.sAMAccountName}: {newUserAccount.SAMAccountName} already exists in the directory."); } - using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) { + using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) + { #region Initialize and populate LDAP attribute set var attributeSet = ldapConnection.CreateAttributeSet(); @@ -130,9 +123,6 @@ public async Task CreateUserAccountForMsAD(LDAP if (!string.IsNullOrEmpty(newUserAccount.Description)) attributeSet.AddAttribute(EntryAttribute.description.ToString(), newUserAccount.Description); - if (newUserAccount.MemberOf != null && newUserAccount.MemberOf.Length > 0) - attributeSet.AddAttribute(EntryAttribute.memberOf.ToString(), newUserAccount.MemberOf); - if (newUserAccount.ObjectClass != null && newUserAccount.ObjectClass.Length > 0) attributeSet.AddAttribute(EntryAttribute.objectClass.ToString(), newUserAccount.ObjectClass); @@ -154,7 +144,8 @@ public async Task CreateUserAccountForMsAD(LDAP if (!string.IsNullOrEmpty(newUserAccount.Mail)) attributeSet.AddAttribute(EntryAttribute.mail.ToString(), newUserAccount.Mail); - if (!string.IsNullOrEmpty(newUserAccount.Password)) { + if (!string.IsNullOrEmpty(newUserAccount.Password)) + { byte[] encodedNewPasswordBytes = Encoding.Unicode.GetBytes($"\"{newUserAccount.Password}\""); attributeSet.AddAttribute(EntryAttribute.unicodePwd.ToString(), encodedNewPasswordBytes); } @@ -177,13 +168,13 @@ public async Task CreateUserAccountForMsAD(LDAP }; } catch (Exception ex) - { - return new LDAPCreateMsADUserAccountResult("Unexpected error while attempting to create user account.", ex, requestLabel) - { - UserAccount = newUserAccount.SecureClone() - }; - } - } + { + return new LDAPCreateMsADUserAccountResult("Unexpected error while attempting to create user account.", ex, requestLabel) + { + UserAccount = newUserAccount.SecureClone() + }; + } + } /// /// Set a password for a username in MS Active Directory service. This method will verify the authenticity of the username by its distinguished name before trying to set the password. If the username is not valid, the operation will not be attempted and an error will be returned. @@ -195,29 +186,29 @@ public async Task CreateUserAccountForMsAD(LDAP /// True if the MS AD user account will be tested to verify authentication with the new password. False if the password will simply be assigned and authentication will not be tested. /// A task with the password-update operation result. public async Task SetMsADUserAccountPassword(EntryAttribute identifierAttribute, string identifierValue, string password, string requestLabel = null, bool postUpdateTestAuthentication = true) - { - try - { + { + try + { if (identifierAttribute != EntryAttribute.sAMAccountName && identifierAttribute != EntryAttribute.distinguishedName) throw new ArgumentException($"The identifier attribute must be {EntryAttribute.sAMAccountName} or {EntryAttribute.distinguishedName} for setting a user account password."); if (string.IsNullOrEmpty(identifierValue)) - throw new DataValidationException("The user account identifier is required."); + throw new DataValidationException("The user account identifier is required."); if (string.IsNullOrEmpty(password)) - throw new DataValidationException("The user account password is required."); + throw new DataValidationException("The user account password is required."); - var entry = await verifyMsADEntryAccountAuthenticity(identifierAttribute, identifierValue, true, requestLabel); + var entry = await verifyMsADEntryAccountAuthenticity(identifierAttribute, identifierValue, true, requestLabel); - //Create password modification request - string newPassword = $"\"{password}\""; - byte[] encodedNewPasswordBytes = Encoding.Unicode.GetBytes(newPassword); - //string newPasswordEncodedString = Convert.ToBase64String(encodedNewPasswordBytes); - //var pwdAttribute = new LdapAttribute(DTO.EntryAttribute.unicodePwd.ToString(), encodedNewPasswordBytes); - //var pwdModification = new LdapModification(LdapModification.Replace, pwdAttribute); + //Create password modification request + string newPassword = $"\"{password}\""; + byte[] encodedNewPasswordBytes = Encoding.Unicode.GetBytes(newPassword); + //string newPasswordEncodedString = Convert.ToBase64String(encodedNewPasswordBytes); + //var pwdAttribute = new LdapAttribute(DTO.EntryAttribute.unicodePwd.ToString(), encodedNewPasswordBytes); + //var pwdModification = new LdapModification(LdapModification.Replace, pwdAttribute); - using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) - { + using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) + { var modification = ldapConnection.CreateModification(LdapModificationType.Replace, EntryAttribute.unicodePwd.ToString(), encodedNewPasswordBytes); //Send modification request to the directory @@ -225,31 +216,32 @@ public async Task SetMsADUserAccountPassword(EntryAttr //await ldapConnection.ModifyAsync(entry.identifierValue, pwdModification); if (postUpdateTestAuthentication) - { + { var postValidationCredential = new LDAPDistinguishedNameCredential(entry.distinguishedName, password); - var authenticator = new Authenticator(ConnectionInfo, ConnectionFactory); - var authenticationResult = await authenticator.AuthenticateAsync(postValidationCredential, requestLabel); - - if (authenticationResult.IsSuccessfulOperation) - { - if (authenticationResult.IsAuthenticated) - return createSuccessfulResult(requestLabel, entry.distinguishedName); - else - return new DTO.LDAPPasswordUpdateResult(requestLabel, $"Could not set password for {entry.distinguishedName} distinguished name.", false); - } - else - { - if (authenticationResult.HasErrorObject) - throw new Exception(authenticationResult.OperationMessage, authenticationResult.ErrorObject); - else - throw new Exception(authenticationResult.OperationMessage); - } - } - else + var authenticator = new Authenticator(ConnectionInfo, ConnectionFactory); + var authenticationResult = await authenticator.AuthenticateAsync(postValidationCredential, requestLabel); + + if (authenticationResult.IsSuccessfulOperation) + { + if (authenticationResult.IsAuthenticated) + return createSuccessfulResult(requestLabel, entry.distinguishedName); + else + return new DTO.LDAPPasswordUpdateResult(requestLabel, $"Could not set password for {entry.distinguishedName} distinguished name.", false); + } + else + { + if (authenticationResult.HasErrorObject) + throw new Exception(authenticationResult.OperationMessage, authenticationResult.ErrorObject); + else + throw new Exception(authenticationResult.OperationMessage); + } + } + else return createSuccessfulResult(requestLabel, entry.distinguishedName); } - } - catch (EntryNotFoundException ex) { + } + catch (EntryNotFoundException ex) + { return new LDAPPasswordUpdateResult("User account not found.", ex, requestLabel); } catch (DataValidationException ex) @@ -257,11 +249,12 @@ public async Task SetMsADUserAccountPassword(EntryAttr return new LDAPPasswordUpdateResult("Invalid data found.", ex, requestLabel); } catch (Exception ex) - { - return new LDAPPasswordUpdateResult("Unexpected error while attempting to replace password.", ex, requestLabel); - } + { + return new LDAPPasswordUpdateResult("Unexpected error while attempting to replace password.", ex, requestLabel); + } - LDAPPasswordUpdateResult createSuccessfulResult(string label, string name) { + LDAPPasswordUpdateResult createSuccessfulResult(string label, string name) + { return new LDAPPasswordUpdateResult(label, $"Password set successfully for {name}"); } } @@ -274,18 +267,19 @@ LDAPPasswordUpdateResult createSuccessfulResult(string label, string name) { /// Optional tag to mark the request and/or response. /// public async Task DisableMsADUserAccount(EntryAttribute identifierAttribute, string identifierValue, string requestLabel) - { - try - { + { + try + { if (EntryAttribute.sAMAccountName != identifierAttribute && EntryAttribute.distinguishedName != identifierAttribute) throw new ArgumentException($"The identifier attribute must be {EntryAttribute.sAMAccountName} or {EntryAttribute.distinguishedName} for disabling a user account."); if (string.IsNullOrEmpty(identifierValue)) - throw new ArgumentNullException($"The user account's {identifierAttribute} value is required."); + throw new ArgumentNullException($"The user account's {identifierAttribute} value is required."); var entry = await verifyMsADEntryAccountAuthenticity(identifierAttribute, identifierValue, true, requestLabel); - using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) { + using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) + { //To disable a MS AD user account, the userAccountControl attribute needs to be set with the appropriate flags. The flag for disabling an account is ACCOUNTDISABLE (0x0002). However, when setting the userAccountControl attribute, it is important to preserve the existing flags that are set for the account, and only add the ACCOUNTDISABLE flag without removing any of the existing flags. This is because other flags may be set for the account that are necessary for its proper functioning, and removing them could cause unintended consequences. Therefore, when disabling a username, you should retrieve the current value of the userAccountControl attribute, add the ACCOUNTDISABLE flag to it, and then update the attribute with the new value that includes both the existing flags and the ACCOUNTDISABLE flag. UserAccountControlFlagsForMsAD userAccountControlFlags = UserAccountControlFlagsForMsAD.NORMAL_ACCOUNT | UserAccountControlFlagsForMsAD.ACCOUNTDISABLE; //var userAccountControlAttribute = new LdapAttribute(DTO.EntryAttribute.userAccountControl.ToString(), ((int)userAccountControlFlags).ToString()); @@ -300,11 +294,11 @@ public async Task DisableMsADUserAccount( //await ldapConnection.ModifyAsync(entry.identifierValue, userAccountControlModification); } - return new DTO.LDAPDisableUserAccountOperationResult(requestLabel) - { - OperationMessage = $"Username {entry.samAccountName} has been disabled." - }; - } + return new DTO.LDAPDisableUserAccountOperationResult(requestLabel) + { + OperationMessage = $"Username {entry.samAccountName} has been disabled." + }; + } catch (EntryNotFoundException ex) { return new LDAPDisableUserAccountOperationResult("User account not found.", ex, requestLabel); @@ -314,10 +308,10 @@ public async Task DisableMsADUserAccount( return new LDAPDisableUserAccountOperationResult("Invalid data found.", ex, requestLabel); } catch (Exception ex) - { - return new LDAPDisableUserAccountOperationResult($"Error trying to disable user account with {identifierAttribute}: {identifierValue}", ex, requestLabel); - } - } + { + return new LDAPDisableUserAccountOperationResult($"Error trying to disable user account with {identifierAttribute}: {identifierValue}", ex, requestLabel); + } + } /// /// Remove a username in MS Active Directory service. This operation will permanently delete the username entry from the directory, so it should be used with caution. @@ -327,9 +321,9 @@ public async Task DisableMsADUserAccount( /// Optional tag to mark the request and/or response. /// public async Task RemoveMsADUserAccount(EntryAttribute identifierAttribute, string identifierValue, string requestLabel = null) - { - try - { + { + try + { if (identifierAttribute != EntryAttribute.sAMAccountName && identifierAttribute != EntryAttribute.distinguishedName) throw new ArgumentException($"The identifier attribute must be {EntryAttribute.sAMAccountName} or {EntryAttribute.distinguishedName} for removing a user account."); @@ -338,18 +332,18 @@ public async Task RemoveMsADUserAccount(EntryAt var entry = await verifyMsADEntryAccountAuthenticity(identifierAttribute, identifierValue, true, requestLabel); - using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) - { + using (var ldapConnection = await GetLdapConnection(this.ConnectionInfo, this.DomainAccountCredential)) + { //To remove a username from MS AD, the username entry needs to be deleted from the directory. This operation will permanently delete the username entry, so it should be used with caution. await ldapConnection.DeleteEntryAsync(entry.distinguishedName); //await ldapConnection.DeleteAsync(entry.identifierValue); } - return new LDAPRemoveMsADUserAccountResult(requestLabel) - { - OperationMessage = $"The username {entry.samAccountName} has been successfully removed." - }; - } + return new LDAPRemoveMsADUserAccountResult(requestLabel) + { + OperationMessage = $"The username {entry.samAccountName} has been successfully removed." + }; + } catch (EntryNotFoundException ex) { return new LDAPRemoveMsADUserAccountResult("User account not found.", ex, requestLabel); @@ -359,29 +353,31 @@ public async Task RemoveMsADUserAccount(EntryAt return new LDAPRemoveMsADUserAccountResult("Invalid data found.", ex, requestLabel); } catch (Exception ex) - { - return new LDAPRemoveMsADUserAccountResult($"Error trying to remove username with {identifierAttribute}: {identifierValue}", ex, requestLabel); - } - } + { + return new LDAPRemoveMsADUserAccountResult($"Error trying to remove username with {identifierAttribute}: {identifierValue}", ex, requestLabel); + } + } - private async Task verifyMsADEntryAccountAuthenticity(EntryAttribute identifierAttribute, string identifierValue, bool validateObjectClass, string requestLabel = null) - { + private async Task verifyMsADEntryAccountAuthenticity(EntryAttribute identifierAttribute, string identifierValue, bool validateObjectClass, string requestLabel = null) + { if (identifierAttribute != EntryAttribute.sAMAccountName && identifierAttribute != EntryAttribute.distinguishedName) throw new ArgumentException($"The identifier attribute must be {EntryAttribute.sAMAccountName} or {EntryAttribute.distinguishedName} for verifying the authenticity of a user account."); var onlyUsersFilterCombiner = QueryFilters.AttributeFilterCombiner.CreateOnlyUsersFilterCombiner(); - var attributeFilter = new QueryFilters.AttributeFilter(identifierAttribute, new QueryFilters.FilterValue(identifierValue)); - var searchFilterCombiner = new QueryFilters.AttributeFilterCombiner(false, true, new List { onlyUsersFilterCombiner, attributeFilter }); - - var searcher = new Searcher(this.ConnectionInfo, this.SearchLimits, this.DomainAccountCredential, ConnectionFactory); - var searchResult = await searcher.SearchEntriesAsync(searchFilterCombiner, RequiredEntryAttributes.Few, requestLabel); - if (!searchResult.IsSuccessfulOperation) - { - if (searchResult.HasErrorObject) { - if (searchResult.ErrorObject is LdapException) { + var attributeFilter = new QueryFilters.AttributeFilter(identifierAttribute, new QueryFilters.FilterValue(identifierValue)); + var searchFilterCombiner = new QueryFilters.AttributeFilterCombiner(false, true, new List { onlyUsersFilterCombiner, attributeFilter }); + + var searcher = new Searcher(this.ConnectionInfo, this.SearchLimits, this.DomainAccountCredential, ConnectionFactory); + var searchResult = await searcher.SearchEntriesAsync(searchFilterCombiner, RequiredEntryAttributes.Few, requestLabel); + if (!searchResult.IsSuccessfulOperation) + { + if (searchResult.HasErrorObject) + { + if (searchResult.ErrorObject is LdapException) + { var unwrappedLdapException = (LdapException)searchResult.ErrorObject; throw new LdapException(searchResult.OperationMessage, unwrappedLdapException.ResultCode, unwrappedLdapException.LdapErrorMessage, unwrappedLdapException); @@ -391,17 +387,17 @@ private async Task verifyMsADEntryAccountAuthenticity(EntryAttribute } else throw new Exception(searchResult.OperationMessage); - } + } - if (searchResult.Entries.Count() == 0) - throw new EntryNotFoundException($"{identifierAttribute} {identifierValue} does not exist."); + if (searchResult.Entries.Count() == 0) + throw new EntryNotFoundException($"{identifierAttribute} {identifierValue} does not exist."); - var entry = searchResult.Entries.Single(); + var entry = searchResult.Entries.Single(); - if (validateObjectClass && !entry.objectClass.Contains("user")) - throw new DataValidationException($"{identifierAttribute} {identifierValue} is not a user entry."); + if (validateObjectClass && !entry.objectClass.Contains("user")) + throw new DataValidationException($"{identifierAttribute} {identifierValue} is not a user entry."); - return entry; - } - } + return entry; + } + } } diff --git a/src/Bitai.LDAPHelper/Authenticator.cs b/src/Bitai.LDAPHelper/Authenticator.cs index 7791b76..7dba90f 100644 --- a/src/Bitai.LDAPHelper/Authenticator.cs +++ b/src/Bitai.LDAPHelper/Authenticator.cs @@ -47,13 +47,15 @@ public async Task AuthenticateAsync(LDAPD var searchResult = await searcher.SearchEntriesAsync(searchFilter, RequiredEntryAttributes.OnlyObjectSid, requestLabel); if (!searchResult.IsSuccessfulOperation) { + string errorMessage = $"Failed to authenticate account '{credential.DomainAccountName}'."; + if (searchResult.HasErrorObject) { - return new LDAPDomainAccountAuthenticationResult(credential, searchResult.OperationMessage, searchResult.ErrorObject, requestLabel); + return new LDAPDomainAccountAuthenticationResult(credential, $"{errorMessage} {searchResult.OperationMessage}", searchResult.ErrorObject, requestLabel); } else { authenticationResult = new LDAPDomainAccountAuthenticationResult(credential, false, requestLabel, false) { - OperationMessage = searchResult.OperationMessage + OperationMessage = $"{errorMessage} {searchResult.OperationMessage}" }; return authenticationResult; diff --git a/src/Bitai.LDAPHelper/SearchLimits.cs b/src/Bitai.LDAPHelper/SearchLimits.cs index f91f121..f663271 100644 --- a/src/Bitai.LDAPHelper/SearchLimits.cs +++ b/src/Bitai.LDAPHelper/SearchLimits.cs @@ -31,6 +31,8 @@ public class SearchLimits : ISearchLimits /// public int MaxSearchTimeout { get; set; } = 60; + + /// /// Initializes a new instance of the class. /// @@ -39,5 +41,17 @@ public SearchLimits(string baseDN) { this.BaseDN = baseDN; } + + + + /// + /// Returns a string representation of the current search limits. + /// + /// + public override string ToString() + { + return $"BaseDN={BaseDN}, Scope={LdapSearchScope}, MaxResults={MaxSearchResults}, MaxTimeout={MaxSearchTimeout}"; + } + } } diff --git a/src/Bitai.LDAPHelper/Searcher.cs b/src/Bitai.LDAPHelper/Searcher.cs index cc3e7f3..469fbf8 100644 --- a/src/Bitai.LDAPHelper/Searcher.cs +++ b/src/Bitai.LDAPHelper/Searcher.cs @@ -12,16 +12,16 @@ namespace Bitai.LDAPHelper /// Performs LDAP search operations and maps results into DTO models. /// public class Searcher : BaseHelper - { - #region Constructor + { + #region Constructor /// /// Initializes a new instance of the class. /// /// Client configuration containing connection, credential, and search settings. /// LDAP connection factory abstraction. - public Searcher(ClientConfiguration clientConfiguration, ILdapConnectionFactoryAdapter connectionFactory) : base(clientConfiguration, connectionFactory) - { - } + public Searcher(ClientConfiguration clientConfiguration, ILdapConnectionFactoryAdapter connectionFactory) : base(clientConfiguration, connectionFactory) + { + } /// /// Initializes a new instance of the class. @@ -31,36 +31,36 @@ public Searcher(ClientConfiguration clientConfiguration, ILdapConnectionFactoryA /// Credential used to execute LDAP searches. /// LDAP connection factory abstraction. public Searcher(ConnectionInfo connectionInfo, SearchLimits searchLimits, DTO.LDAPDomainAccountCredential domainAccountCredential, ILdapConnectionFactoryAdapter connectionFactory) : base(connectionInfo, searchLimits, domainAccountCredential, connectionFactory) - { - } - #endregion + { + } + #endregion - #region Public methods - /// - /// Searches for entries matching the provided LDAP filter and loads the requested attributes. - /// + #region Public methods + /// + /// Searches for entries matching the provided LDAP filter and loads the requested attributes. + /// /// - /// A combinable LDAP filter that identifies the entries to search for. This filter will be converted - /// to its string representation and used directly in the LDAP search operation. - /// - /// - /// The set of attributes to load for each entry returned in the search result. Use this to limit - /// attributes loaded for performance (for example, OnlyMemberOf, Few, All, etc.). - /// - /// - /// Optional label/tag that will be set on the returned LDAPSearchResult and on created LDAPEntry - /// instances to help callers correlate operations and results. - /// - /// - /// A task that resolves to an containing the entries found and - /// any operation message. If an error occurs, the returned LDAPSearchResult will have - /// IsSuccessfulOperation == false and contain error details. - /// - public async Task SearchEntriesAsync(QueryFilters.ICombinableFilter searchFilterObject, DTO.RequiredEntryAttributes requiredEntryAttributes, string requestLabel) - { + /// A combinable LDAP filter that identifies the entries to search for. This filter will be converted + /// to its string representation and used directly in the LDAP search operation. + /// + /// + /// The set of attributes to load for each entry returned in the search result. Use this to limit + /// attributes loaded for performance (for example, OnlyMemberOf, Few, All, etc.). + /// + /// + /// Optional label/tag that will be set on the returned LDAPSearchResult and on created LDAPEntry + /// instances to help callers correlate operations and results. + /// + /// + /// A task that resolves to an containing the entries found and + /// any operation message. If an error occurs, the returned LDAPSearchResult will have + /// IsSuccessfulOperation == false and contain error details. + /// + public async Task SearchEntriesAsync(QueryFilters.ICombinableFilter searchFilterObject, DTO.RequiredEntryAttributes requiredEntryAttributes, string requestLabel) + { try { string searchFilter = searchFilterObject.ToString(); @@ -90,9 +90,7 @@ public Searcher(ConnectionInfo connectionInfo, SearchLimits searchLimits, DTO.LD } catch (LdapException ex) { - string msg = string.IsNullOrEmpty(ex.LdapErrorMessage) ? ex.Message : (string.IsNullOrEmpty(ex.Message) ? ex.LdapErrorMessage : $"{ex.Message} ({ex.LdapErrorMessage})"); - - var searchResult = new DTO.LDAPSearchResult(msg, ex, requestLabel); + var searchResult = new DTO.LDAPSearchResult($"LDAP error encountered while performing search.", ex, requestLabel); return searchResult; } @@ -141,26 +139,83 @@ public Searcher(ConnectionInfo connectionInfo, SearchLimits searchLimits, DTO.LD /// each discovered parent. Any LDAP or general exception is captured and returned as an unsuccessful /// LDAPSearchResult rather than being thrown. /// - public async Task SearchParentEntriesAsync(QueryFilters.ICombinableFilter searchFilter, DTO.RequiredEntryAttributes requiredEntryAttributes, string requestLabel) { - try { - // First, perform a partial search to get the memberOf attributes of the entries matching the provided filter. This is necessary to discover the parent entries (groups/containers) that we need to load with the requested attributes. + public async Task SearchParentEntriesAsync(QueryFilters.ICombinableFilter searchFilter, DTO.RequiredEntryAttributes requiredEntryAttributes, string requestLabel) + { + try + { + // First, perform a partial search to get the memberOf attributes of the entry or entries matching the provided filter. This is necessary to discover the parent entries (groups/containers) that we need to load with the requested attributes. var partialSearchResult = await this.SearchEntriesAsync(searchFilter, DTO.RequiredEntryAttributes.OnlyMemberOf, requestLabel); - if (!partialSearchResult.IsSuccessfulOperation) { + if (!partialSearchResult.IsSuccessfulOperation) + { return partialSearchResult; } - else if (!partialSearchResult.Entries.Any()) { + else if (!partialSearchResult.Entries.Any()) + { throw new EntryNotFoundException("Unable to evaluate without an entry."); } var collectedEntries = partialSearchResult.Entries.SelectAllMemberOfEntriesRecursively(); var resultEntries = new List(); - foreach (var entry in collectedEntries) { + foreach (var entry in collectedEntries) + { var distinguishedNameFilter = new QueryFilters.AttributeFilter(DTO.EntryAttribute.distinguishedName, new QueryFilters.FilterValue(entry.distinguishedName.ReplaceSpecialCharsToScapedChars())); partialSearchResult = await SearchEntriesAsync(distinguishedNameFilter, requiredEntryAttributes, requestLabel); - if (!partialSearchResult.IsSuccessfulOperation) { + if (!partialSearchResult.IsSuccessfulOperation) + { + return partialSearchResult; + } + + resultEntries.AddRange(partialSearchResult.Entries); + } + + return new DTO.LDAPSearchResult(requestLabel, resultEntries); + } + catch (EntryNotFoundException ex) + { + var searchResult = new DTO.LDAPSearchResult("Nonexistent entry.", ex, requestLabel); + + return searchResult; + } + catch (LdapException ex) + { + string msg = string.IsNullOrEmpty(ex.LdapErrorMessage) ? ex.Message : (string.IsNullOrEmpty(ex.Message) ? ex.LdapErrorMessage : $"{ex.Message} ({ex.LdapErrorMessage})"); + var searchResult = new DTO.LDAPSearchResult(msg, ex, requestLabel); + + return searchResult; + } + //// BITAI: Remain for future reference if we want to avoid direct dependency on Novell.Directory.Ldap in this class. The LdapException type is specific to the Novell library, so if we want to keep this class decoupled from that library, we can catch general Exception and check the type name as done in other parts of the code. However, if we are okay with referencing Novell.Directory.Ldap directly, catching LdapException is more straightforward and type-safe. + //catch (Exception ex) when (ex.GetType().Name == "LdapException") { + // var ldapErrorMessageProp = ex.GetType().GetProperty("LdapErrorMessage"); + // string ldapErrorMessage = ldapErrorMessageProp?.GetValue(ex) as string ?? ""; + // string msg = string.IsNullOrEmpty(ldapErrorMessage) ? ex.Message : $"{ex.Message} ({ldapErrorMessage})"; + // var searchResult = new DTO.LDAPSearchResult(msg, ex, requestLabel); + // return searchResult; + //} + catch (Exception ex) + { + var searchResult = new DTO.LDAPSearchResult($"Unexpected error performing search. {ex.Message}", ex, requestLabel); + + return searchResult; + } + } + + public async Task SearchParentEntriesAsync(IEnumerable entries, DTO.RequiredEntryAttributes requiredEntryAttributes, string requestLabel) + { + try + { + var collectedEntries = entries.SelectAllMemberOfEntriesRecursively(); + + var resultEntries = new List(); + foreach (var entry in collectedEntries) + { + var distinguishedNameFilter = new QueryFilters.AttributeFilter(DTO.EntryAttribute.distinguishedName, new QueryFilters.FilterValue(entry.distinguishedName.ReplaceSpecialCharsToScapedChars())); + + var partialSearchResult = await SearchEntriesAsync(distinguishedNameFilter, requiredEntryAttributes, requestLabel); + if (!partialSearchResult.IsSuccessfulOperation) + { return partialSearchResult; } @@ -190,11 +245,12 @@ public Searcher(ConnectionInfo connectionInfo, SearchLimits searchLimits, DTO.LD // var searchResult = new DTO.LDAPSearchResult(msg, ex, requestLabel); // return searchResult; //} - catch (Exception ex) { - var searchResult = new DTO.LDAPSearchResult($"Unexpected error performing search. {ex.Message}", ex, requestLabel); + catch (Exception ex) + { + var searchResult = new DTO.LDAPSearchResult($"Unexpected error performing search. {ex.Message}", ex, requestLabel); - return searchResult; - } + return searchResult; + } } #endregion diff --git a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs index fdf04dd..3a85508 100644 --- a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs @@ -21,7 +21,7 @@ public MockLdapEntryAdapter CreateMockUserEntry(string firstName, string lastNam mockLdapEntry.AddAttribute("objectSid", new byte[] { 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 134, 161, 247, 215, 208, 13, 248, 19, 35, 76, 31, 226, 79, 4, 0, 0 }); mockLdapEntry.AddAttribute("objectGUID", new byte[] { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 }); mockLdapEntry.AddAttribute("sAMAccountName", $"{firstName.ToLower()}.{lastName.ToLower()}"); - mockLdapEntry.AddAttribute("cn", $"{firstName} {lastName}"); + mockLdapEntry.AddAttribute("Name", $"{firstName} {lastName}"); mockLdapEntry.AddAttribute("sn", $"{lastName}"); mockLdapEntry.AddAttribute("givenName", $"{firstName}"); mockLdapEntry.AddAttribute("mail", $"{firstName.ToLower()}.{lastName.ToLower()}@bitaitec.com"); @@ -70,7 +70,7 @@ public MockLdapEntryAdapter CreateMockGroupEntry(string groupName, string organi // GUID: f8e9d7c6-b5a4-4321-8765-43210fedcba9 mockLdapEntry.AddAttribute("objectGUID", new byte[] { 0xC6, 0xD7, 0xE9, 0xF8, 0xA4, 0xB5, 0x21, 0x43, 0x87, 0x65, 0x43, 0x21, 0x0F, 0xED, 0xCB, 0xA9 }); mockLdapEntry.AddAttribute("sAMAccountName", $"{groupName}"); - mockLdapEntry.AddAttribute("cn", $"{groupName}"); + mockLdapEntry.AddAttribute("Name", $"{groupName}"); mockLdapEntry.AddAttribute("objectClass", new string[] { "top", "group" }); mockLdapEntry.AddAttribute("sAMAccountType", "268435456"); mockLdapEntry.AddAttribute("groupType", "-2147483640"); // Represents a Universal Security Group @@ -97,7 +97,7 @@ public ConnectionInfo CreateInvalidConnectionInfo(bool ssl) { } public SearchLimits CreateValidSearchLimits() { - return new SearchLimits("DC=domain,DC=com") { + return new SearchLimits("DC=va,DC=bitai,DC=com") { MaxSearchResults = 1000, MaxSearchTimeout = 60 }; From 782351011950cce90cb11d27f136c05bac9cb850 Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Tue, 29 Sep 2026 16:46:32 -0400 Subject: [PATCH 2/7] File version updated --- .../Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj | 6 +++--- src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj | 6 +++--- src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj | 6 +++--- tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj index f8c69ff..e5897b6 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj @@ -5,9 +5,9 @@ BITAI LDAP Services Wrappers © 2026 BITAI. All rights reserved. - 10.0.0 - 10.0.0 - 10.0.0 + 10.1.0 + 10.1.0 + 10.1.0 hierarchy_32.png true Bitai.LDAPHelper.LdapAdapters.LdapHelperMock diff --git a/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj b/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj index 24284ff..ea0ebf0 100644 --- a/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj +++ b/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj @@ -2,9 +2,9 @@ net10.0 - 10.1.0 - 10.1.0 - 10.1.0 + 10.2.0 + 10.2.0 + 10.2.0 Bitai.LDAPHelper.DTO true Viko Bastidas (BITAI) diff --git a/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj b/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj index 82feacd..ed0975c 100644 --- a/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj +++ b/src/Bitai.LDAPHelper/Bitai.LDAPHelper.csproj @@ -7,9 +7,9 @@ LDAP Services Wrappers Library to wrap Novell.Directory.Ldap.NETStandard functionality to make LDAP common queries to search accounts and objects in a Directory Service. © 2026 BITAI. All rights reserved. - 10.2.0 - 10.2.0 - 10.2.0 + 10.2.1 + 10.2.1 + 10.2.1 hierarchy_32.png true Bitai.LDAPHelper diff --git a/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj b/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj index 3d8cd38..fbe3570 100644 --- a/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj +++ b/tests/Bitai.LDAPHelper.Tests/Bitai.LDAPHelper.Tests.csproj @@ -7,9 +7,9 @@ false true - 10.1.3 - 10.1.3 - 10.1.3 + 10.2.0 + 10.2.0 + 10.2.0 5981b6a0-6b9e-439d-8324-a0ef8bfd0f11 From baead2aebc818e35735e486e810c31497c081eeb Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Wed, 30 Sep 2026 14:53:25 -0400 Subject: [PATCH 3/7] Fix tests --- .../MockLdapConnectionFactoryAdapter.cs | 24 +++++++++---------- .../AccountManagerAdapterTests.cs | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs index fbf0305..a1c7e70 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs @@ -1,13 +1,16 @@ namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// -/// Mock implementation of that returns a provided mock connection. +/// Mock implementation of that returns a provided mock _connection. /// public class MockLdapConnectionFactoryAdapter : ILdapConnectionFactoryAdapter { - //public MockLdapConnectionFactoryAdapter(MockLdapConnectionAdapter connection) { - // connection = connection; - //} + private readonly MockLdapConnectionAdapter _connection; + + public MockLdapConnectionFactoryAdapter(MockLdapConnectionAdapter connection) + { + _connection = connection; + } public async Task CreateConnectionAsync( IConnectionInfo connectionInfo, @@ -15,16 +18,13 @@ public async Task CreateConnectionAsync( string password, bool bindRequired = true) { - var connection = new MockLdapConnectionAdapter() - { - ConnectionTimeout = connectionInfo.ConnectionTimeout, - SecureSocketLayer = connectionInfo.UseSSL - }; + _connection.ConnectionTimeout = connectionInfo.ConnectionTimeout; + _connection.SecureSocketLayer = connectionInfo.UseSSL; - await connection.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); + await _connection.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); try { - await connection.BindAsync(userAccount, password); + await _connection.BindAsync(userAccount, password); } catch (LdapOperationException) { if (bindRequired) @@ -34,6 +34,6 @@ public async Task CreateConnectionAsync( throw; } - return (ILdapConnectionAdapter)connection; + return (ILdapConnectionAdapter)_connection; } } diff --git a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs index d259e9d..f006f8d 100644 --- a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs @@ -24,6 +24,7 @@ public async Task CreateUserAccountForMsAD_ReturnsSuccess() { var newUser = new LDAPMsADUserAccount { DistinguishedNameOfContainer = $"CN=Software Developers;OU=IT,{searchLimits.BaseDN}", + DistinguishedName = $"CN=John Doe,CN=Software Developers;OU=IT,{searchLimits.BaseDN}", Cn = "John Doe", DisplayName = "John Doe (Fullstack)", SAMAccountName = "john.doe", From fc5b912e2d89befd87af3c74f1d414fa3f2141e0 Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Wed, 30 Sep 2026 18:50:31 -0400 Subject: [PATCH 4/7] Test with Persistence Data --- .../MockLdapConnectionAdapter.cs | 5 +- .../IEnumerableLDAPEntryExtensions.cs | 2 +- .../GroupMembershipValidator.cs | 2 +- .../AccountManagerAdapterTests.cs | 139 ++-- .../AuthenticatorAdapterTests.cs | 110 ++-- tests/Bitai.LDAPHelper.Tests/BaseTests.cs | 112 +--- .../GroupMembershipValidatorTests.cs | 621 ++++-------------- .../SearcherAdapterTests.cs | 66 +- 8 files changed, 281 insertions(+), 776 deletions(-) diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs index 34297d6..368c9b9 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs @@ -38,7 +38,10 @@ public Task BindAsync(string userDN, string password) { if (string.IsNullOrEmpty(userDN) || string.IsNullOrEmpty(password) || userDN.Contains("hacker") || password.Contains("123456")) throw new LdapOperationException($"{nameof(MockLdapConnectionAdapter)}.{nameof(MockLdapConnectionAdapter.BindAsync)}: Invalid credentials!"); - _isBound = !string.IsNullOrEmpty(userDN) && !string.IsNullOrEmpty(password); + if (password.Equals("wrongpassword", StringComparison.OrdinalIgnoreCase)) + _isBound = false; + else + _isBound = !string.IsNullOrEmpty(userDN) && !string.IsNullOrEmpty(password); return Task.CompletedTask; } diff --git a/src/Bitai.LDAPHelper/Extensions/IEnumerableLDAPEntryExtensions.cs b/src/Bitai.LDAPHelper/Extensions/IEnumerableLDAPEntryExtensions.cs index 8c939d7..27f0e9e 100644 --- a/src/Bitai.LDAPHelper/Extensions/IEnumerableLDAPEntryExtensions.cs +++ b/src/Bitai.LDAPHelper/Extensions/IEnumerableLDAPEntryExtensions.cs @@ -22,7 +22,7 @@ public static class IEnumerableLDAPEntryExtensions partialList.AddRange(entry.GetMemberOfEntriesRecursively()); } - return partialList.Distinct(); + return partialList.DistinctBy(e => e.distinguishedName); } } } diff --git a/src/Bitai.LDAPHelper/GroupMembershipValidator.cs b/src/Bitai.LDAPHelper/GroupMembershipValidator.cs index 2c3e222..281e5ad 100644 --- a/src/Bitai.LDAPHelper/GroupMembershipValidator.cs +++ b/src/Bitai.LDAPHelper/GroupMembershipValidator.cs @@ -121,7 +121,7 @@ public async Task GetAllGroupMembershipsAsync(string sAMAccountName) { if (string.IsNullOrEmpty(sAMAccountName)) throw new ArgumentNullException(nameof(sAMAccountName)); - if (sAMAccountName.Contains("*")) + if (sAMAccountName.Contains('*')) throw new ArgumentException($"{nameof(sAMAccountName)} cannot contain the character *."); var attributeFilter = new QueryFilters.AttributeFilter(DTO.EntryAttribute.sAMAccountName, new QueryFilters.FilterValue(sAMAccountName)); diff --git a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs index f006f8d..746abfb 100644 --- a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs @@ -1,5 +1,7 @@ using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging.Abstractions; namespace Bitai.LDAPHelper.Tests { @@ -10,9 +12,9 @@ public class AccountManagerAdapterTests: BaseTests { [Fact] public async Task CreateUserAccountForMsAD_ReturnsSuccess() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -44,9 +46,9 @@ public async Task CreateUserAccountForMsAD_ReturnsSuccess() { [Fact] public async Task CreateUserAccountForMsAD_MissingRequiredAttr_ReturnsError() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -77,9 +79,9 @@ public async Task CreateUserAccountForMsAD_MissingRequiredAttr_ReturnsError() { [Fact] public async Task SetUserAccountPasswordForMsAD_ValidAccount_ReturnsSuccess() { // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -89,18 +91,10 @@ public async Task SetUserAccountPasswordForMsAD_ValidAccount_ReturnsSuccess() { var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - var groupName = "Accountants"; - var groupContainerName = "Finance"; - var mockGroupEntry1 = CreateMockGroupEntry(groupName, groupContainerName, searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Auditors", "Trusted", searchLimits, out var groupSearchFilter2); - - var mockUserEntry = CreateMockUserEntry("John", "Doe", searchLimits, out var _, out var userSearchFilter, new string[] { mockGroupEntry2.DistinguishedName }, groupName, groupContainerName); + // Use data from the seeder - james.dockers user + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); - - var result = await accountManager.SetMsADUserAccountPassword(EntryAttribute.distinguishedName, mockUserEntry.DistinguishedName, "TestPassword", postUpdateTestAuthentication: true); + var result = await accountManager.SetMsADUserAccountPassword(EntryAttribute.distinguishedName, userDistinguishedName, "TestPassword", postUpdateTestAuthentication: true); // Assert Assert.True(result.IsSuccessfulOperation); @@ -109,9 +103,9 @@ public async Task SetUserAccountPasswordForMsAD_ValidAccount_ReturnsSuccess() { [Fact] public async Task SetUserAccountPasswordForMsAD_AccountNotFound_ReturnsFailed() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -121,21 +115,10 @@ public async Task SetUserAccountPasswordForMsAD_AccountNotFound_ReturnsFailed() var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - var groupName = "Accountants"; - var groupContainerName = "Finance"; - var mockGroupEntry1 = CreateMockGroupEntry(groupName, groupContainerName, searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Auditors", "Trusted", searchLimits, out var groupSearchFilter2); + // Use a DN that doesn't exist in the seeder + var nonExistentUserDN = "CN=Non Existent User,OU=IT,DC=va,DC=bitai,DC=com"; - var mockUserEntry = CreateMockUserEntry("John", "Doe", searchLimits, out var _, out var userSearchFilter, new string[] { mockGroupEntry2.DistinguishedName }, groupName, groupContainerName); - - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - //Do not add to trigger account verification error! - //mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); - - var userCredential = new LDAPDistinguishedNameCredential(mockUserEntry.DistinguishedName, "NewP@ssw0rd"); - - var result = await accountManager.SetMsADUserAccountPassword(EntryAttribute.distinguishedName, mockUserEntry.DistinguishedName, "TestPassword", postUpdateTestAuthentication: true); + var result = await accountManager.SetMsADUserAccountPassword(EntryAttribute.distinguishedName, nonExistentUserDN, "TestPassword", postUpdateTestAuthentication: true); Assert.False(result.IsSuccessfulOperation); Assert.StartsWith("user account not found", result.OperationMessage, StringComparison.OrdinalIgnoreCase); @@ -143,9 +126,9 @@ public async Task SetUserAccountPasswordForMsAD_AccountNotFound_ReturnsFailed() [Fact] public async Task DisableUserAccountForMsAD_ValidAccount_ReturnsSuccess() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -155,28 +138,21 @@ public async Task DisableUserAccountForMsAD_ValidAccount_ReturnsSuccess() { var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - var groupName = "Interviewers"; - var groupContainerName = "Human Resources"; - var mockGroupEntry1 = CreateMockGroupEntry(groupName, groupContainerName, searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Auditors", "Trusted", searchLimits, out var groupSearchFilter2); - var mockUserEntry = CreateMockUserEntry("Francis", "Peralta", searchLimits, out var _, out var userSearchFilter, new string[] { mockGroupEntry2.DistinguishedName }, groupName, groupContainerName); + // Use data from the seeder - james.dockers user + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); - - var result = await accountManager.DisableMsADUserAccount(EntryAttribute.distinguishedName, mockUserEntry.DistinguishedName, "TestDisable"); + var result = await accountManager.DisableMsADUserAccount(EntryAttribute.distinguishedName, userDistinguishedName, "TestDisable"); // Assert - Assert.True(result.IsSuccessfulOperation); + Assert.True(result.IsSuccessfulOperation); Assert.Contains("has been disabled", result.OperationMessage.ToLower()); } [Fact] public async Task DisableUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -186,18 +162,10 @@ public async Task DisableUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - var groupName = "Interviewers"; - var groupContainerName = "Human Resources"; - var mockGroupEntry1 = CreateMockGroupEntry(groupName, groupContainerName, searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Auditors", "Trusted", searchLimits, out var groupSearchFilter2); - var mockUserEntry = CreateMockUserEntry("Francis", "Peralta", searchLimits, out var _, out var userSearchFilter, new string[] { mockGroupEntry2.DistinguishedName }, groupName, groupContainerName); + // Use a DN that doesn't exist in the seeder + var nonExistentUserDN = "CN=Non Existent User,OU=IT,DC=va,DC=bitai,DC=com"; - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - //Do not add user account in order to trigger user not found validation. - //mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); - - var result = await accountManager.DisableMsADUserAccount(EntryAttribute.distinguishedName, mockUserEntry.DistinguishedName, "TestDisable"); + var result = await accountManager.DisableMsADUserAccount(EntryAttribute.distinguishedName, nonExistentUserDN, "TestDisable"); // Assert Assert.False(result.IsSuccessfulOperation); @@ -206,9 +174,9 @@ public async Task DisableUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { [Fact] public async Task RemoveUserAccountForMsAD_ValidAccount_ReturnsSuccess() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -216,19 +184,12 @@ public async Task RemoveUserAccountForMsAD_ValidAccount_ReturnsSuccess() { var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); - var groupName = "Accountants"; - var groupContainerName = "Finance"; - var mockGroupEntry1 = CreateMockGroupEntry(groupName, groupContainerName, searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Auditors", "Trusted", searchLimits, out var groupSearchFilter2); - var mockUserEntry = CreateMockUserEntry("John", "Doe", searchLimits, out var _, out var userSearchFilter, new string[] { mockGroupEntry2.DistinguishedName }, groupName, groupContainerName); - - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); + // Use data from the seeder - james.dockers user + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - - var result = await accountManager.RemoveMsADUserAccount(EntryAttribute.distinguishedName, mockUserEntry.DistinguishedName, "TestDelete"); + + var result = await accountManager.RemoveMsADUserAccount(EntryAttribute.distinguishedName, userDistinguishedName, "TestDelete"); Assert.True(result.IsSuccessfulOperation); Assert.Contains("successfully removed", result.OperationMessage.ToLower()); @@ -236,9 +197,9 @@ public async Task RemoveUserAccountForMsAD_ValidAccount_ReturnsSuccess() { [Fact] public async Task RemoveUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -246,20 +207,12 @@ public async Task RemoveUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); - var groupName = "Accountants"; - var groupContainerName = "Finance"; - var mockGroupEntry1 = CreateMockGroupEntry(groupName, groupContainerName, searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Auditors", "Trusted", searchLimits, out var groupSearchFilter2); - var mockUserEntry = CreateMockUserEntry("John", "Doe", searchLimits, out var _, out var userSearchFilter, new string[] { mockGroupEntry2.DistinguishedName }, groupName, groupContainerName); - - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - //Do not add user entry to trigger account not found validation. - //mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); + // Use a DN that doesn't exist in the seeder + var nonExistentUserDN = "CN=Non Existent User,OU=IT,DC=va,DC=bitai,DC=com"; var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - var result = await accountManager.RemoveMsADUserAccount(EntryAttribute.distinguishedName, mockUserEntry.DistinguishedName, "TestDelete"); + var result = await accountManager.RemoveMsADUserAccount(EntryAttribute.distinguishedName, nonExistentUserDN, "TestDelete"); Assert.False(result.IsSuccessfulOperation); Assert.Contains("user account not found", result.OperationMessage, StringComparison.OrdinalIgnoreCase); diff --git a/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs index 03aaafe..f7c3ec2 100644 --- a/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs @@ -1,5 +1,7 @@ using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging.Abstractions; namespace Bitai.LDAPHelper.Tests { @@ -10,11 +12,10 @@ public class AuthenticatorAdapterTests : BaseTests { [Fact] public async Task AuthenticateUser_ReturnsSuccess() { - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -36,23 +37,20 @@ public async Task AuthenticateUser_WithVerification_ReturnsSuccess() { //Search limits var searchLimits = CreateValidSearchLimits(); - //Mock LDAP entry for the user - var userMockEntry = CreateMockUserEntry("Victor", "Bastidas", searchLimits, out var userSearchFilter, out var _); - - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock the search for the user - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { userMockEntry }); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); var authenticator = new Authenticator(connectionInfo, mockConnectionFactory); - var credential = new LDAPDomainAccountCredential("domain", userSearchFilter.FilterValue.Value, "p@55w0rd"); + // Use data from the seeder - james.dockers user + var userSearchFilter = CreateSearchFilter("sAMAccountName", "james.dockers"); + + var credential = new LDAPDomainAccountCredential("domain", "james.dockers", "p@55w0rd"); var credentialForSearching = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); //Execute authentication @@ -65,21 +63,20 @@ public async Task AuthenticateUser_WithVerification_ReturnsSuccess() { [Fact] public async Task AuthenticateDN_ReturnsSuccess() { - //Mock LDAP entry for the user - var userMockEntry = CreateMockUserEntry("Victor", "Bastidas", null, out var _, out var _); - - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); var authenticator = new Authenticator(connectionInfo, mockConnectionFactory); - var credential = new LDAPDistinguishedNameCredential(userMockEntry.DistinguishedName, "p@55w0rd"); + // Use data from the seeder - james.dockers user + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + + var credential = new LDAPDistinguishedNameCredential(userDistinguishedName, "p@55w0rd"); //Execute authentication var result = await authenticator.AuthenticateAsync(credential, "TestAuth"); @@ -94,23 +91,20 @@ public async Task AuthenticateDN_WithVerification_ReturnsSuccess() { //Search limits var searchLimits = CreateValidSearchLimits(); - //Mock LDAP entry for the user - var userMockEntry = CreateMockUserEntry("Victor", "Bastidas", searchLimits, out var _, out var userSearchFilter); - - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock the search for the user - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { userMockEntry }); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); var authenticator = new Authenticator(connectionInfo, mockConnectionFactory); - var credential = new LDAPDistinguishedNameCredential(userMockEntry.DistinguishedName, "p@55w0rd"); + // Use data from the seeder - james.dockers user + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + + var credential = new LDAPDistinguishedNameCredential(userDistinguishedName, "p@55w0rd"); var credentialForSearching = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); //Execute authentication @@ -121,23 +115,19 @@ public async Task AuthenticateDN_WithVerification_ReturnsSuccess() { Assert.True(result.IsSuccessfulOperation); } - - - [Fact] public async Task AuthenticateUser_ReturnsFailed() { - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); var authenticator = new Authenticator(connectionInfo, mockConnectionFactory); - var credential = new LDAPDomainAccountCredential("domain", "dummy", "123456"); + var credential = new LDAPDomainAccountCredential("domain", "dummy", "wrongpassword"); //Execute authentication var result = await authenticator.AuthenticateAsync(credential, "TestAuth"); @@ -146,7 +136,6 @@ public async Task AuthenticateUser_ReturnsFailed() { Assert.False(result.IsAuthenticated); Assert.True(result.IsSuccessfulOperation); Assert.True(string.IsNullOrEmpty(result.ErrorType)); - //Assert.Contains("could not be found", result.OperationMessage); } [Fact] @@ -154,16 +143,10 @@ public async Task AuthenticateUser_WithVerification_ReturnsFailed() { //Search limits var searchLimits = CreateValidSearchLimits(); - //Mock LDAP entry for the user - var userMockEntry = CreateMockUserEntry("Victor", "Bastidas", searchLimits, out var userSearchFilter, out var _); - - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock the search for the user - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { userMockEntry }); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -184,21 +167,20 @@ public async Task AuthenticateUser_WithVerification_ReturnsFailed() { [Fact] public async Task AuthenticateDN_ReturnsFailed() { - //Mock LDAP entry for the user - var userMockEntry = CreateMockUserEntry("Victor", "Bastidas", null, out var _, out var userSearchFilter); - - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); var authenticator = new Authenticator(connectionInfo, mockConnectionFactory); - var credential = new LDAPDistinguishedNameCredential(userMockEntry.DistinguishedName, "123456"); + // Use data from the seeder - james.dockers user + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + + var credential = new LDAPDistinguishedNameCredential(userDistinguishedName, "wrongpassword"); //Execute authentication var result = await authenticator.AuthenticateAsync(credential, "TestAuth"); @@ -214,16 +196,10 @@ public async Task AuthenticateDN_WithVerification_ReturnsFailed() { //Search limits var searchLimits = CreateValidSearchLimits(); - //Mock LDAP entry for the user - var userMockEntry = CreateMockUserEntry("Victor", "Bastidas", searchLimits, out var _, out var userSearchFilter); - - //Mock connection - var mockConnection = new MockLdapConnectionAdapter(); - //Mock the search for the user - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { userMockEntry }); - //Mock connection factory - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); //Connection information var connectionInfo = CreateValidConnectionInfo(true); diff --git a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs index 3a85508..33b6dd1 100644 --- a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs @@ -1,4 +1,5 @@ using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; namespace Bitai.LDAPHelper.Tests { @@ -7,85 +8,24 @@ namespace Bitai.LDAPHelper.Tests /// public class BaseTests { - public MockLdapEntryAdapter CreateMockUserEntry(string firstName, string lastName, SearchLimits? searchLimits, out QueryFilters.AttributeFilter searchFilterSAMAccountName, out QueryFilters.AttributeFilter searchFilterDistinguishedName, string[] memberOfDistinguishedNames = null, string groupName = null, string groupContainerName = null) { - if (string.IsNullOrEmpty(firstName) || string.IsNullOrEmpty(lastName)) - throw new ArgumentException("First name and last name cannot be null or empty."); - - //Mock LDAP entry for the user - var mockLdapEntry = new MockLdapEntryAdapter( - $"CN={firstName} {lastName}" + - (string.IsNullOrEmpty(groupName) ? string.Empty : $",CN={groupName}" ) + - (string.IsNullOrEmpty(groupContainerName) ? string.Empty : $",OU={groupContainerName}") + - (searchLimits != null ? string.Concat(",", searchLimits.BaseDN) : ",DC=domain,DC=com")); - //Add more attributes - mockLdapEntry.AddAttribute("objectSid", new byte[] { 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 134, 161, 247, 215, 208, 13, 248, 19, 35, 76, 31, 226, 79, 4, 0, 0 }); - mockLdapEntry.AddAttribute("objectGUID", new byte[] { 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 }); - mockLdapEntry.AddAttribute("sAMAccountName", $"{firstName.ToLower()}.{lastName.ToLower()}"); - mockLdapEntry.AddAttribute("Name", $"{firstName} {lastName}"); - mockLdapEntry.AddAttribute("sn", $"{lastName}"); - mockLdapEntry.AddAttribute("givenName", $"{firstName}"); - mockLdapEntry.AddAttribute("mail", $"{firstName.ToLower()}.{lastName.ToLower()}@bitaitec.com"); - mockLdapEntry.AddAttribute("title", "Software Engineer"); - mockLdapEntry.AddAttribute("department", "IT"); - mockLdapEntry.AddAttribute("mail", $"{firstName.ToLower()}.{lastName.ToLower()}@bitaitec.com"); - mockLdapEntry.AddAttribute("userPrincipalName", $"{firstName.ToLower()}.{lastName.ToLower()}@domain"); - mockLdapEntry.AddAttribute("userAccountControl", "512"); - mockLdapEntry.AddAttribute("objectClass", new string[] { "top", "person", "organizationalPerson", "user" }); - mockLdapEntry.AddAttribute("sAMAccountType", "805306368"); - mockLdapEntry.AddAttribute("whenCreated", "20220101000000.0Z"); - if (memberOfDistinguishedNames != null && memberOfDistinguishedNames.Length > 0) { - mockLdapEntry.AddAttribute("memberOf", memberOfDistinguishedNames); - } - else { - mockLdapEntry.AddAttribute("memberOf", new string[] { "CN=Devs,OU=IT,DC=domain,DC=com", "CN=Admins,OU=Trusted,DC=domain,DC=com" }); - } - mockLdapEntry.AddAttribute("logonCount", "10"); - mockLdapEntry.AddAttribute("badPwdCount", "2"); - //mockLdapEntry.AddAttribute("whenChanged", "20220102000000.0Z"); - //mockLdapEntry.AddAttribute("lastLogon", "20220103000000.0Z"); - //mockLdapEntry.AddAttribute("lastLogonTimestamp", "20220104000000.0Z"); - //mockLdapEntry.AddAttribute("lastLogoff", "20220105000000.0Z"); - //mockLdapEntry.AddAttribute("pwdLastSet", "20220106000000.0Z"); - //mockLdapEntry.AddAttribute("accountExpires", "20220107000000.0Z"); - //mockLdapEntry.AddAttribute("badPasswordTime", "20220108000000.0Z"); - //mockLdapEntry.AddAttribute("lastBadPasswordAttempt", "20220109000000.0Z"); - //mockLdapEntry.AddAttribute("lastBadPasswordAttemptTimestamp", "20220110000000.0Z"); - - GenerateSearchFilter(mockLdapEntry.GetAttributeSet().GetAttribute(DTO.EntryAttribute.sAMAccountName.ToString()).StringValue, DTO.EntryAttribute.sAMAccountName, out searchFilterSAMAccountName); - GenerateSearchFilter(mockLdapEntry.GetAttributeSet().GetAttribute(DTO.EntryAttribute.distinguishedName.ToString()).StringValue, DTO.EntryAttribute.distinguishedName, out searchFilterDistinguishedName); - - return mockLdapEntry; - } - - public MockLdapEntryAdapter CreateMockGroupEntry(string groupName, string organitationalUnitName, SearchLimits? searchLimits, out QueryFilters.AttributeFilter searchFilterDistinguishedName) { - if (string.IsNullOrEmpty(groupName) || string.IsNullOrEmpty(organitationalUnitName)) - throw new ArgumentException("First name and last name cannot be null or empty."); - - //Mock LDAP entry for the user - var mockLdapEntry = new MockLdapEntryAdapter($"CN={groupName},OU={organitationalUnitName},{(searchLimits != null ? searchLimits.BaseDN : "DC=domain,DC=com")}"); - // SID for a typical Security Group (RID 1105) - // S-1-5-21-3623811974-335183920-3791444003-1105 - mockLdapEntry.AddAttribute("objectSid", new byte[] { 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 134, 161, 247, 215, 208, 13, 248, 19, 35, 76, 31, 226, 79, 5, 0, 0 }); - // A unique 16-byte array for the Group's GUID - // GUID: f8e9d7c6-b5a4-4321-8765-43210fedcba9 - mockLdapEntry.AddAttribute("objectGUID", new byte[] { 0xC6, 0xD7, 0xE9, 0xF8, 0xA4, 0xB5, 0x21, 0x43, 0x87, 0x65, 0x43, 0x21, 0x0F, 0xED, 0xCB, 0xA9 }); - mockLdapEntry.AddAttribute("sAMAccountName", $"{groupName}"); - mockLdapEntry.AddAttribute("Name", $"{groupName}"); - mockLdapEntry.AddAttribute("objectClass", new string[] { "top", "group" }); - mockLdapEntry.AddAttribute("sAMAccountType", "268435456"); - mockLdapEntry.AddAttribute("groupType", "-2147483640"); // Represents a Universal Security Group - mockLdapEntry.AddAttribute("whenCreated", "20220101000000.0Z"); - //mockLdapEntry.AddAttribute("memberOf", new string[] { "CN=IT,DC=domain,DC=com", "CN=Trusted,DC=domain,DC=com" }); - //mockLdapEntry.AddAttribute("whenChanged", "20220102000000.0Z"); - //mockLdapEntry.AddAttribute("lastLogon", "20220103000000.0Z"); - //mockLdapEntry.AddAttribute("lastLogonTimestamp", "20220104000000.0Z"); - //mockLdapEntry.AddAttribute("lastLogoff", "20220105000000.0Z"); - //mockLdapEntry.AddAttribute("pwdLastSet", "20220106000000.0Z"); - //mockLdapEntry.AddAttribute("accountExpires", "20220107000000.0Z"); - - GenerateCommonGroupSearchFilter(groupName, organitationalUnitName, out searchFilterDistinguishedName); - - return mockLdapEntry; + /// + /// Finds an entry in the persistent mock data store by its distinguished name. + /// + protected MockLdapEntryAdapter FindEntryInStore(string distinguishedName) + { + var entry = MockLdapDataStore.Instance.GetEntry(distinguishedName); + if (entry == null) + throw new InvalidOperationException($"Entry '{distinguishedName}' not found in the persistent mock data store. Make sure the seeder has been run."); + return entry; + } + + /// + /// Creates a search filter for a given attribute and value. + /// + protected QueryFilters.AttributeFilter CreateSearchFilter(string attributeName, string value) + { + var attribute = (DTO.EntryAttribute)Enum.Parse(typeof(DTO.EntryAttribute), attributeName); + return new QueryFilters.AttributeFilter(attribute, new QueryFilters.FilterValue(value)); } public ConnectionInfo CreateValidConnectionInfo(bool ssl) { @@ -102,19 +42,5 @@ public SearchLimits CreateValidSearchLimits() { MaxSearchTimeout = 60 }; } - - public void GenerateCommonUserSearchFilter(string firstName, string lastName, SearchLimits searchLimits, out QueryFilters.AttributeFilter searchFilterSAMAccountName, out QueryFilters.AttributeFilter searchFilterDistinguishedName) { - searchFilterSAMAccountName = new QueryFilters.AttributeFilter(DTO.EntryAttribute.sAMAccountName, new QueryFilters.FilterValue($"{firstName.ToLower()}.{lastName.ToLower()}")); - - searchFilterDistinguishedName = new QueryFilters.AttributeFilter(DTO.EntryAttribute.distinguishedName, new QueryFilters.FilterValue($"CN={firstName} {lastName},{searchLimits.BaseDN}")); - } - - public void GenerateSearchFilter(string filterValue, DTO.EntryAttribute attribute, out QueryFilters.AttributeFilter searchFilter) { - searchFilter = new QueryFilters.AttributeFilter(attribute, new QueryFilters.FilterValue(filterValue)); - } - - public void GenerateCommonGroupSearchFilter(string groupName, string organitationalUnitName, out QueryFilters.AttributeFilter searchFilterDistinguishedName) { - searchFilterDistinguishedName = new QueryFilters.AttributeFilter(DTO.EntryAttribute.distinguishedName, new QueryFilters.FilterValue($"CN={groupName},OU={organitationalUnitName},DC=domain,DC=com")); - } } } diff --git a/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs b/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs index 29f479e..53a0fda 100644 --- a/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs @@ -1,5 +1,7 @@ using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging.Abstractions; namespace Bitai.LDAPHelper.Tests { @@ -22,75 +24,29 @@ public GroupMembershipValidatorTests() { [Fact] public async Task CheckGroupMembershipAsync_UserIsDirectMember_ReturnsTrue() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var userEntry = CreateMockUserEntry( - firstName: "John", - lastName: "Doe", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { "CN=Devs,OU=IT,DC=domain,DC=com" } - ); - - var groupEntry = CreateMockGroupEntry( - groupName: "Devs", - organitationalUnitName: "IT", - searchLimits: _validSearchLimits, - searchFilterDistinguishedName: out var groupFilter - ); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(groupFilter.ToString(), new List { groupEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - james.dockers is direct member of DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - var result = await validator.CheckGroupMembershipAsync("john.doe", "Devs"); + var result = await validator.CheckGroupMembershipAsync("james.dockers", "Domain Admins"); Assert.True(result); } [Fact] public async Task CheckGroupMembershipAsync_UserIsIndirectMember_ReturnsTrue() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - // User -> DevTeam (direct) -> Devs (indirect) - var userEntry = CreateMockUserEntry( - firstName: "Jane", - lastName: "Smith", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { "CN=DevTeam,OU=IT,DC=domain,DC=com" } - ); - - var devTeamGroup = CreateMockGroupEntry( - groupName: "DevTeam", - organitationalUnitName: "IT", - searchLimits: _validSearchLimits, - searchFilterDistinguishedName: out var devTeamFilter - ); - devTeamGroup.AddAttribute("memberOf", new[] { "CN=Devs,OU=IT,DC=domain,DC=com" }); - - var devsGroup = CreateMockGroupEntry( - groupName: "Devs", - organitationalUnitName: "IT", - searchLimits: _validSearchLimits, - searchFilterDistinguishedName: out var devsFilter - ); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(devTeamFilter.ToString(), new List { devTeamGroup }); - mockConnection.AddSearchResult(devsFilter.ToString(), new List { devsGroup }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - sara.pikes -> JuniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var result = await validator.CheckGroupMembershipAsync("jane.smith", "Devs"); + var result = await validator.CheckGroupMembershipAsync("sara.pikes", "Domain Admins"); // Assert Assert.True(result); @@ -98,41 +54,15 @@ public async Task CheckGroupMembershipAsync_UserIsIndirectMember_ReturnsTrue() { [Fact] public async Task CheckGroupMembershipAsync_UserIsMemberThroughMultipleLevels_ReturnsTrue() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - // User -> Level1 -> Level2 -> Level3 -> TargetGroup - var userEntry = CreateMockUserEntry( - firstName: "Deep", - lastName: "Nested", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { "CN=Level1,OU=IT,DC=domain,DC=com" } - ); - - var level1 = CreateMockGroupEntry("Level1", "IT", _validSearchLimits, out var level1Filter); - level1.AddAttribute("memberOf", new[] { "CN=Level2,OU=IT,DC=domain,DC=com" }); + // Arrange - james.dockers -> SeniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var level2 = CreateMockGroupEntry("Level2", "IT", _validSearchLimits, out var level2Filter); - level2.AddAttribute("memberOf", new[] { "CN=Level3,OU=IT,DC=domain,DC=com" }); - - var level3 = CreateMockGroupEntry("Level3", "IT", _validSearchLimits, out var level3Filter); - level3.AddAttribute("memberOf", new[] { "CN=TargetGroup,OU=IT,DC=domain,DC=com" }); - - var targetGroup = CreateMockGroupEntry("TargetGroup", "IT", _validSearchLimits, out var targetFilter); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(level1Filter.ToString(), new List { level1 }); - mockConnection.AddSearchResult(level2Filter.ToString(), new List { level2 }); - mockConnection.AddSearchResult(level3Filter.ToString(), new List { level3 }); - mockConnection.AddSearchResult(targetFilter.ToString(), new List { targetGroup }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var result = await validator.CheckGroupMembershipAsync("deep.nested", "TargetGroup"); + var result = await validator.CheckGroupMembershipAsync("james.dockers", "Administrators"); // Assert Assert.True(result); @@ -140,28 +70,15 @@ public async Task CheckGroupMembershipAsync_UserIsMemberThroughMultipleLevels_Re [Fact] public async Task CheckGroupMembershipAsync_UserIsNotMember_ReturnsFalse() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var userEntry = CreateMockUserEntry( - firstName: "Bob", - lastName: "Johnson", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { "CN=DevTeam,OU=IT,DC=domain,DC=com" } - ); + // Arrange - sara.pikes is not a member of DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var devTeamGroup = CreateMockGroupEntry("DevTeam", "IT", _validSearchLimits, out var devTeamFilter); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(devTeamFilter.ToString(), new List { devTeamGroup }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var result = await validator.CheckGroupMembershipAsync("bob.johnson", "Devs"); + var result = await validator.CheckGroupMembershipAsync("sara.pikes", "DomainAdmins"); // Assert Assert.False(result); @@ -169,74 +86,26 @@ public async Task CheckGroupMembershipAsync_UserIsNotMember_ReturnsFalse() { [Fact] public async Task CheckGroupMembershipAsync_UserNotFound_ThrowsException() { - var mockConnection = new MockLdapConnectionAdapter(); - // No user entry added - will not be found + // Arrange + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - await Assert.ThrowsAsync(() => validator.CheckGroupMembershipAsync("nonexistent.user", "Devs")); + await Assert.ThrowsAsync(() => validator.CheckGroupMembershipAsync("nonexistent.user", "DomainAdmins")); } - //[Fact] - //public async Task CheckGroupMembershipAsync_WithCircularReference_HandlesGracefullyAndReturnsTrue() { - // // Arrange - // var mockConnection = new MockLdapConnectionAdapter(); - - // var userEntry = CreateMockUserEntry( - // firstName: "Alice", - // lastName: "Brown", - // searchLimits: _validSearchLimits, - // searchFilterSAMAccountName: out var userFilter, - // searchFilterDistinguishedName: out _, - // memberOfDistinguishedNames: new[] { "CN=GroupA,OU=IT,DC=domain,DC=com" } - // ); - - // var groupA = CreateMockGroupEntry("GroupA", "IT", _validSearchLimits, out var groupAFilter); - // groupA.AddAttribute("memberOf", new[] { "CN=GroupB,OU=IT,DC=domain,DC=com" }); - - // var groupB = CreateMockGroupEntry("GroupB", "IT", _validSearchLimits, out var groupBFilter); - // groupB.AddAttribute("memberOf", new[] { "CN=GroupA,OU=IT,DC=domain,DC=com" }); // Circular reference - - // mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - // mockConnection.AddSearchResult(groupAFilter.ToString(), new List { groupA }); - // mockConnection.AddSearchResult(groupBFilter.ToString(), new List { groupB }); - - // var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); - - // var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - - // // Act - Should not throw StackOverflowException - // var result = await validator.CheckGroupMembershipAsync("alice.brown", "GroupA"); - - // // Assert - // Assert.True(result); - //} - [Fact] public async Task CheckGroupMembershipAsync_CaseInsensitiveComparison_ReturnsTrue() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var groupEntry = CreateMockGroupEntry("Devs", "IT", _validSearchLimits, out var groupFilter); - - var userEntry = CreateMockUserEntry( - firstName: "John", - lastName: "Cena", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { groupEntry.DistinguishedName } - ); - - mockConnection.AddSearchResult(groupFilter.ToString(), new List { groupEntry }); - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - james.dockers is member of DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - var result = await validator.CheckGroupMembershipAsync("JOHN.CENA", "DEVS"); + var result = await validator.CheckGroupMembershipAsync("JAMES.DOCKERS", "DOMAIN ADMINS"); Assert.True(result); } @@ -244,255 +113,155 @@ public async Task CheckGroupMembershipAsync_CaseInsensitiveComparison_ReturnsTru [Fact] public async Task CheckGroupMembershipAsync_NullSAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert await Assert.ThrowsAsync( - () => validator.CheckGroupMembershipAsync(null, "Devs")); + () => validator.CheckGroupMembershipAsync(null, "DomainAdmins")); } [Fact] public async Task CheckGroupMembershipAsync_EmptySAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert await Assert.ThrowsAsync( - () => validator.CheckGroupMembershipAsync(string.Empty, "Devs")); + () => validator.CheckGroupMembershipAsync(string.Empty, "DomainAdmins")); } [Fact] public async Task CheckGroupMembershipAsync_SAMAccountNameContainsWildcard_ThrowsArgumentException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert await Assert.ThrowsAsync( - () => validator.CheckGroupMembershipAsync("john.*", "Devs")); + () => validator.CheckGroupMembershipAsync("john.*", "DomainAdmins")); } [Fact] public async Task CheckGroupMembershipAsync_NullParentGroupCN_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert await Assert.ThrowsAsync( - () => validator.CheckGroupMembershipAsync("john.doe", null)); + () => validator.CheckGroupMembershipAsync("james.dockers", null)); } [Fact] public async Task CheckGroupMembershipAsync_EmptyParentGroupCN_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert await Assert.ThrowsAsync( - () => validator.CheckGroupMembershipAsync("john.doe", string.Empty)); + () => validator.CheckGroupMembershipAsync("james.dockers", string.Empty)); } [Fact] public async Task CheckGroupMembershipAsync_ParentGroupCNContainsWildcard_ThrowsArgumentException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert await Assert.ThrowsAsync( - () => validator.CheckGroupMembershipAsync("john.doe", "Dev*")); + () => validator.CheckGroupMembershipAsync("james.dockers", "Domain*")); } [Fact] public async Task CheckGroupMembershipAsync_UserInMultipleGroups_FindsCorrectGroup() { - var mockConnection = new MockLdapConnectionAdapter(); - - var userEntry = CreateMockUserEntry( - firstName: "Tom", - lastName: "Anderson", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] - { - "CN=Users,CN=Builtin,DC=domain,DC=com", - "CN=Devs,OU=IT,DC=domain,DC=com", - "CN=PowerUsers,CN=Builtin,DC=domain,DC=com" - } - ); - - var devsGroup = CreateMockGroupEntry("Devs", "IT", _validSearchLimits, out var devsGroupFilter); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(devsGroupFilter.ToString(), new List { devsGroup }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - james.dockers is member of multiple groups + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - var result = await validator.CheckGroupMembershipAsync("tom.anderson", "Devs"); + var result = await validator.CheckGroupMembershipAsync("james.dockers", "IT Admins"); Assert.True(result); } #endregion - #region GetAllGroupMembershipsAsync Tests - - [Fact] - public async Task GetAllGroupMembershipsAsync_UserWithDirectMemberships_ReturnsAllDirectGroups() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var groupEntry1 = CreateMockGroupEntry("Devs", "IT", _validSearchLimits, out var groupSearchFilter1); - var groupEntry2 = CreateMockGroupEntry("QA", "IT", _validSearchLimits, out var groupSearchFilter2); - var groupEntry3 = CreateMockGroupEntry("Security", "IT", _validSearchLimits, out var groupSearchFilter3); - - var userEntry = CreateMockUserEntry( - firstName: "David", - lastName: "Clark", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userSearchFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] - { - groupEntry1.DistinguishedName, - groupEntry2.DistinguishedName, - groupEntry3.DistinguishedName - } - ); - - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { groupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { groupEntry2 }); - mockConnection.AddSearchResult(groupSearchFilter3.ToString(), new List { groupEntry3 }); - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { userEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); - - var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - - var groups = await validator.GetAllGroupMembershipsAsync("david.clark"); - - Assert.NotNull(groups); - Assert.Contains("Devs", groups); - Assert.Contains("QA", groups); - Assert.Contains("Security", groups); - Assert.Equal(3, groups.Length); - } + #region GetAllGroupMembershipsAsync Tests [Fact] public async Task GetAllGroupMembershipsAsync_UserWithNestedMemberships_ReturnsAllGroupsIncludingIndirect() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var itDeptGroup = CreateMockGroupEntry("ITDepartment", "IT", _validSearchLimits, out var itDeptFilter); - - var devTeamGroup = CreateMockGroupEntry("DevTeam", "IT", _validSearchLimits, out var devTeamFilter); - devTeamGroup.AddAttribute("memberOf", new[] { itDeptGroup.DistinguishedName }); - - // User -> DevTeam (direct) -> ITDepartment (indirect) - var userEntry = CreateMockUserEntry( - firstName: "David", - lastName: "Miller", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { devTeamGroup.DistinguishedName } - ); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(devTeamFilter.ToString(), new List { devTeamGroup }); - mockConnection.AddSearchResult(itDeptFilter.ToString(), new List { itDeptGroup }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - sara.pikes -> JuniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - var groups = await validator.GetAllGroupMembershipsAsync("david.miller"); + var groups = await validator.GetAllGroupMembershipsAsync("sara.pikes"); - // Assert - Should include both direct (DevTeam) and indirect (ITDepartment) groups + // Assert - Should include both direct and indirect groups Assert.NotNull(groups); - Assert.Contains("DevTeam", groups); - Assert.Contains("ITDepartment", groups); + Assert.Contains("Administrators", groups); + Assert.Contains("Junior DevOps", groups); + Assert.Contains("DevOps Engineers", groups); + Assert.Contains("IT Admins", groups); + Assert.Contains("Domain Users", groups); + Assert.Contains("Domain Admins", groups); } [Fact] public async Task GetAllGroupMembershipsAsync_UserWithDeepNesting_ReturnsAllGroupsInHierarchy() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var userEntry = CreateMockUserEntry( - firstName: "Deep", - lastName: "Hierarchy", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { "CN=Level1,OU=IT,DC=domain,DC=com" } - ); - - var level1 = CreateMockGroupEntry("Level1", "IT", _validSearchLimits, out var level1Filter); - level1.AddAttribute("memberOf", new[] { "CN=Level2,OU=IT,DC=domain,DC=com" }); - - var level2 = CreateMockGroupEntry("Level2", "IT", _validSearchLimits, out var level2Filter); - level2.AddAttribute("memberOf", new[] { "CN=Level3,OU=IT,DC=domain,DC=com" }); + // Arrange - james.dockers -> SeniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins -> Administrators + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var level3 = CreateMockGroupEntry("Level3", "IT", _validSearchLimits, out var level3Filter); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(level1Filter.ToString(), new List { level1 }); - mockConnection.AddSearchResult(level2Filter.ToString(), new List { level2 }); - mockConnection.AddSearchResult(level3Filter.ToString(), new List { level3 }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var groups = await validator.GetAllGroupMembershipsAsync("deep.hierarchy"); + var groups = await validator.GetAllGroupMembershipsAsync("james.dockers"); // Assert Assert.NotNull(groups); - Assert.Contains("Level1", groups); - Assert.Contains("Level2", groups); - Assert.Contains("Level3", groups); - Assert.Equal(3, groups.Length); - } - - [Fact] - public async Task GetAllGroupMembershipsAsync_UserWithNoGroups_ReturnsEmptyArray() { - var mockConnection = new MockLdapConnectionAdapter(); - - var userEntry = CreateMockUserEntry( - firstName: "Emily", - lastName: "White", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: null - ); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); - var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - - var groups = await validator.GetAllGroupMembershipsAsync("emily.white"); - - // Assert - Assert.NotNull(groups); - Assert.Empty(groups); - } + Assert.Contains("Administrators", groups); + Assert.Contains("Senior DevOps", groups); + Assert.Contains("DevOps Engineers", groups); + Assert.Contains("IT Admins", groups); + Assert.Contains("Domain Admins", groups); + Assert.Contains("DevOps Leaders", groups); + } [Fact] public async Task GetAllGroupMembershipsAsync_UserNotFound_ThrowsException() { // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - // No user entry added + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act @@ -501,118 +270,45 @@ public async Task GetAllGroupMembershipsAsync_UserNotFound_ThrowsException() { [Fact] public async Task GetAllGroupMembershipsAsync_RemovesDuplicateGroups() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var groupEntry = CreateMockGroupEntry("Devs", "IT", _validSearchLimits, out var groupFilter); - - var userEntry = CreateMockUserEntry( - firstName: "Chris", - lastName: "Brown", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] - { - groupEntry.DistinguishedName, - groupEntry.DistinguishedName // Duplicate - } - ); - - mockConnection.AddSearchResult(groupFilter.ToString(), new List { groupEntry }); - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - james.dockers has multiple paths to same groups + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var groups = await validator.GetAllGroupMembershipsAsync("chris.brown"); + var groups = await validator.GetAllGroupMembershipsAsync("james.dockers"); - // Assert - Should only have one instance of Devs + // Assert - Should not have duplicates Assert.NotNull(groups); - Assert.Single(groups); - Assert.Equal("Devs", groups[0]); + Assert.Equal(groups.Distinct(StringComparer.OrdinalIgnoreCase).Count(), groups.Length); } [Fact] public async Task GetAllGroupMembershipsAsync_CaseInsensitiveDistinct_ReturnsUniqueGroups() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - var groupEntry = CreateMockGroupEntry("Devs", "IT", _validSearchLimits, out var groupFilter); - var groupEntryDuplicateCase = CreateMockGroupEntry("DEVS", "IT", _validSearchLimits, out var groupFilterDuplicateCase); - - var userEntry = CreateMockUserEntry( - firstName: "Case", - lastName: "Sensitive", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] - { - groupEntry.DistinguishedName, - groupEntryDuplicateCase.DistinguishedName // Same group, different case - } - ); - - mockConnection.AddSearchResult(groupFilter.ToString(), new List { groupEntry }); - mockConnection.AddSearchResult(groupFilterDuplicateCase.ToString(), new List { groupEntryDuplicateCase }); - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - james.dockers has multiple paths to same groups + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var groups = await validator.GetAllGroupMembershipsAsync("case.sensitive"); + var groups = await validator.GetAllGroupMembershipsAsync("james.dockers"); // Assert - Should treat as same group (case-insensitive) Assert.NotNull(groups); - Assert.Single(groups); - Assert.Equal("Devs", groups[0], StringComparer.OrdinalIgnoreCase); + Assert.Equal(groups.Distinct(StringComparer.OrdinalIgnoreCase).Count(), groups.Length); } - //[Fact] - //public async Task GetAllGroupMembershipsAsync_WithCircularReference_HandlesGracefullyAndReturnsUniqueGroups() { - // // Arrange - // var mockConnection = new MockLdapConnectionAdapter(); - - // var userEntry = CreateMockUserEntry( - // firstName: "Circular", - // lastName: "Reference", - // searchLimits: _validSearchLimits, - // searchFilterSAMAccountName: out var userFilter, - // searchFilterDistinguishedName: out _, - // memberOfDistinguishedNames: new[] { "CN=GroupA,OU=IT,DC=domain,DC=com" } - // ); - - // var groupA = CreateMockGroupEntry("GroupA", "IT", _validSearchLimits, out var groupAFilter); - // groupA.AddAttribute("memberOf", new[] { "CN=GroupB,OU=IT,DC=domain,DC=com" }); - - // var groupB = CreateMockGroupEntry("GroupB", "IT", _validSearchLimits, out var groupBFilter); - // groupB.AddAttribute("memberOf", new[] { "CN=GroupA,OU=IT,DC=domain,DC=com" }); // Circular - - // mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - // mockConnection.AddSearchResult(groupAFilter.ToString(), new List { groupA }); - // mockConnection.AddSearchResult(groupBFilter.ToString(), new List { groupB }); - - // var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); - // var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); - - // // Act - Should not throw StackOverflowException - // var groups = await validator.GetAllGroupMembershipsAsync("circular.reference"); - - // // Assert - Should have both groups but no duplicates from circular reference - // Assert.NotNull(groups); - // Assert.Contains("GroupA", groups); - // Assert.Contains("GroupB", groups); - // Assert.Equal(2, groups.Length); - //} - [Fact] public async Task GetAllGroupMembershipsAsync_NullSAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert @@ -623,7 +319,10 @@ await Assert.ThrowsAsync( [Fact] public async Task GetAllGroupMembershipsAsync_EmptySAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert @@ -634,7 +333,10 @@ await Assert.ThrowsAsync( [Fact] public async Task GetAllGroupMembershipsAsync_SAMAccountNameContainsWildcard_ThrowsArgumentException() { // Arrange - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(new MockLdapConnectionAdapter()); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert @@ -648,42 +350,15 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_WithMultipleNestedPaths_ReturnsTrueIfAnyPathLeadsToTarget() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - - // User has two paths: one leads to TargetGroup, one doesn't - var userEntry = CreateMockUserEntry( - firstName: "Multi", - lastName: "Path", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] - { - "CN=TeamA,OU=IT,DC=domain,DC=com", - "CN=TeamB,OU=IT,DC=domain,DC=com" - } - ); - - var teamA = CreateMockGroupEntry("TeamA", "IT", _validSearchLimits, out var teamAFilter); - teamA.AddAttribute("memberOf", new[] { "CN=OtherGroup,OU=IT,DC=domain,DC=com" }); - - var teamB = CreateMockGroupEntry("TeamB", "IT", _validSearchLimits, out var teamBFilter); - teamB.AddAttribute("memberOf", new[] { "CN=TargetGroup,OU=IT,DC=domain,DC=com" }); - - var otherGroup = CreateMockGroupEntry("OtherGroup", "IT", _validSearchLimits, out _); - var targetGroup = CreateMockGroupEntry("TargetGroup", "IT", _validSearchLimits, out var targetFilter); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(teamAFilter.ToString(), new List { teamA }); - mockConnection.AddSearchResult(teamBFilter.ToString(), new List { teamB }); - mockConnection.AddSearchResult(targetFilter.ToString(), new List { targetGroup }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + // Arrange - james.dockers has multiple paths to DomainAdmins + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); + var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var result = await validator.CheckGroupMembershipAsync("multi.path", "TargetGroup"); + var result = await validator.CheckGroupMembershipAsync("james.dockers", "Domain Admins"); // Assert Assert.True(result); @@ -691,48 +366,34 @@ public async Task CheckGroupMembershipAsync_WithMultipleNestedPaths_ReturnsTrueI [Fact] public async Task GetAllGroupMembershipsAsync_WhenSearchFails_ThrowsException() { - var mockConnection = new MockLdapConnectionAdapter(); - - // Don't add any search results - this will cause search to fail + // Arrange + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); - var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act & Assert var exception = await Assert.ThrowsAsync( - () => validator.GetAllGroupMembershipsAsync("john.doe")); + () => validator.GetAllGroupMembershipsAsync("nonexistent.user")); Assert.StartsWith("unable to evaluate without an entry", exception.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task CheckGroupMembershipAsync_WithWhitespaceInCN_HandlesCorrectly() { - // Arrange - var mockConnection = new MockLdapConnectionAdapter(); + // Arrange - No groups with whitespace in seeder, use existing group + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); - var userEntry = CreateMockUserEntry( - firstName: "White", - lastName: "Space", - searchLimits: _validSearchLimits, - searchFilterSAMAccountName: out var userFilter, - searchFilterDistinguishedName: out _, - memberOfDistinguishedNames: new[] { "CN=Dev Team,OU=IT,DC=domain,DC=com" } - ); - - var groupEntry = CreateMockGroupEntry("Dev Team", "IT", _validSearchLimits, out var groupFilter); - - mockConnection.AddSearchResult(userFilter.ToString(), new List { userEntry }); - mockConnection.AddSearchResult(groupFilter.ToString(), new List { groupEntry }); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); // Act - var result = await validator.CheckGroupMembershipAsync("white.space", "Dev Team"); + var result = await validator.CheckGroupMembershipAsync("james.dockers", "Domain Admins Z"); - // Assert - Assert.True(result); + // Assert - Should handle gracefully (group doesn't exist) + Assert.False(result); } #endregion diff --git a/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs index 236fd28..0c2ea21 100644 --- a/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs @@ -1,5 +1,7 @@ using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging.Abstractions; namespace Bitai.LDAPHelper.Tests { @@ -12,47 +14,41 @@ public class SearcherAdapterTests : BaseTests public async Task SearchEntries_ReturnsExpectedEntries() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var searchLimits = CreateValidSearchLimits(); - - var mockUserEntry = CreateMockUserEntry("Test", "User", searchLimits, out var userSearchFilter, out var _); - - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); var searcher = new Searcher(connectionInfo, searchLimits, credential, mockConnectionFactory); + var userSearchFilter = CreateSearchFilter("sAMAccountName", "james.dockers"); + var result = await searcher.SearchEntriesAsync(userSearchFilter, RequiredEntryAttributes.Minimun, "TestRequest"); Assert.True(result.IsSuccessfulOperation); Assert.Single(result.Entries); - Assert.Equal(userSearchFilter.FilterValue.Value, result.Entries.First().samAccountName); - Assert.Equal(mockUserEntry.DistinguishedName, result.Entries.First().distinguishedName); + Assert.Equal("james.dockers", result.Entries.First().samAccountName); } [Fact] public async Task SearchEntries_ReturnsEmptyList() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var searchLimits = CreateValidSearchLimits(); - var mockUserEntry = CreateMockUserEntry("Test", "User", searchLimits, out var userSearchFilter, out var _); - - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); - var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); var searcher = new Searcher(connectionInfo, searchLimits, credential, mockConnectionFactory); - GenerateCommonUserSearchFilter("Hacker", "User", searchLimits, out var unknownUserSearchFilter, out var _); + // Search for a user that doesn't exist in the seeder + var unknownUserSearchFilter = CreateSearchFilter("sAMAccountName", "nonexistent.user"); var result = await searcher.SearchEntriesAsync(unknownUserSearchFilter, RequiredEntryAttributes.Minimun, "TestRequest"); @@ -64,19 +60,16 @@ public async Task SearchEntries_ReturnsEmptyList() { public async Task SearchParentEntries_ReturnsExpectedEntries() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var searchLimits = CreateValidSearchLimits(); - var mockGroupEntry1 = CreateMockGroupEntry("Developers", "IT", searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Administrators", "Trusted", searchLimits, out var groupSearchFilter2); - var mockUserEntry = CreateMockUserEntry("John", "Doe", searchLimits, out var userSearchFilter, out var _, new string[] { mockGroupEntry1.DistinguishedName, mockGroupEntry2.DistinguishedName }); - - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); + // Use data from the seeder - james.dockers is member of DomainAdmins, ITAdmins, DevOpsEng, SeniorDevOps, DevOpsLeaders + var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + var mockUserEntry = FindEntryInStore(userDistinguishedName); + var userSearchFilter = CreateSearchFilter("sAMAccountName", "james.dockers"); var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); @@ -92,27 +85,20 @@ public async Task SearchParentEntries_ReturnsExpectedEntries() { public async Task SearchParentEntries_ReturnsEmptyList() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnection = new MockLdapConnectionAdapter(); - - var mockConnectionFactory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance); var searchLimits = CreateValidSearchLimits(); - var mockGroupEntry1 = CreateMockGroupEntry("Developers", "IT", searchLimits, out var groupSearchFilter1); - var mockGroupEntry2 = CreateMockGroupEntry("Administrators", "Trusted", searchLimits, out var groupSearchFilter2); - var mockUserEntry = CreateMockUserEntry("John", "Doe", searchLimits, out var userSearchFilter, out var _, new string[] { mockGroupEntry1.DistinguishedName, mockGroupEntry2.DistinguishedName }); - - mockConnection.AddSearchResult(groupSearchFilter1.ToString(), new List { mockGroupEntry1 }); - mockConnection.AddSearchResult(groupSearchFilter2.ToString(), new List { mockGroupEntry2 }); - mockConnection.AddSearchResult(userSearchFilter.ToString(), new List { mockUserEntry }); - var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); var searcher = new Searcher(connectionInfo, searchLimits, credential, mockConnectionFactory); - GenerateCommonUserSearchFilter("Dummiest", "User", searchLimits, out var expectedDummiestUserSearchFilter, out var _); + // Search for a user that doesn't exist in the seeder + var unknownUserSearchFilter = CreateSearchFilter("sAMAccountName", "nonexistent.user"); - var result = await searcher.SearchParentEntriesAsync(expectedDummiestUserSearchFilter, RequiredEntryAttributes.Minimun, "TestRequest"); + var result = await searcher.SearchParentEntriesAsync(unknownUserSearchFilter, RequiredEntryAttributes.Minimun, "TestRequest"); Assert.False(result.IsSuccessfulOperation); Assert.Null(result.Entries); From f54992a573503bf57a7a240873dafe7e6b89569d Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Wed, 30 Sep 2026 20:26:16 -0400 Subject: [PATCH 5/7] fix Tests --- .../LdapData/MockLdapDataSeeder.cs | 2 - ...kLdapPersistentConnectionFactoryAdapter.cs | 7 +- .../Program.DemoMethods.cs | 1 - demo/Bitai.LDAPHelper.Demo/Program.cs | 4 +- .../AccountManagerAdapterTests.cs | 46 +++------ .../AuthenticatorAdapterTests.cs | 34 ++----- tests/Bitai.LDAPHelper.Tests/BaseTests.cs | 26 +++++ .../GroupMembershipValidatorTests.cs | 98 +++++-------------- .../Bitai.LDAPHelper.Tests/LdapMockFixture.cs | 21 ++++ .../SearcherAdapterTests.cs | 18 +--- 10 files changed, 108 insertions(+), 149 deletions(-) create mode 100644 tests/Bitai.LDAPHelper.Tests/LdapMockFixture.cs diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs index f8d64eb..bfed717 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/LdapData/MockLdapDataSeeder.cs @@ -26,8 +26,6 @@ public MockLdapDataSeeder(ILogger logger) { } public void SeedAllData() { - _dataStore.Clear(); - _logger.LogInformation("=".PadRight(60, '=')); _logger.LogInformation("MOCK DATA SEEDING STARTED"); _logger.LogInformation("=".PadRight(60, '=')); diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs index d74a370..993a71d 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionFactoryAdapter.cs @@ -9,14 +9,13 @@ namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// public class MockLdapPersistentConnectionFactoryAdapter : ILdapConnectionFactoryAdapter { - private readonly MockLdapPersistentConnectionAdapter _connection; + //private readonly MockLdapPersistentConnectionAdapter _connection; private readonly ILogger _logger; public MockLdapPersistentConnectionFactoryAdapter(ILogger logger, ILogger seederLogger) { _logger = logger; - _connection = new MockLdapPersistentConnectionAdapter(); - + var _seeder = new MockLdapDataSeeder(seederLogger); _seeder.SeedAllData(); _seeder.PrintAllData(); @@ -29,7 +28,9 @@ public Task CreateConnectionAsync( bool bindRequired = true) { // Always succeed in mock mode + var _connection = new MockLdapPersistentConnectionAdapter(); _connection.BindAsync(userAccount, password); + return Task.FromResult(_connection); } } diff --git a/demo/Bitai.LDAPHelper.Demo/Program.DemoMethods.cs b/demo/Bitai.LDAPHelper.Demo/Program.DemoMethods.cs index 562bd9b..e93216c 100644 --- a/demo/Bitai.LDAPHelper.Demo/Program.DemoMethods.cs +++ b/demo/Bitai.LDAPHelper.Demo/Program.DemoMethods.cs @@ -107,7 +107,6 @@ public static async Task Demo_AccountManager_CreateUserAccount( Cn = fullName, Name = fullName, DisplayName = fullName, - MemberOf = memberOf, ObjectClass = objectClasses, Password = password, SAMAccountName = userAccountName, diff --git a/demo/Bitai.LDAPHelper.Demo/Program.cs b/demo/Bitai.LDAPHelper.Demo/Program.cs index 238faab..5b97cc7 100644 --- a/demo/Bitai.LDAPHelper.Demo/Program.cs +++ b/demo/Bitai.LDAPHelper.Demo/Program.cs @@ -117,7 +117,6 @@ public static async Task Main(string[] args) if (implementation == ImplementationType.Mock) { Log.Information("Initializing Mock Implementation..."); - _context.ConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter(); // Create logger for MockLdapDataSeeder using var loggerFactory = LoggerFactory.Create(builder => { @@ -125,7 +124,10 @@ public static async Task Main(string[] args) builder.AddSerilog(Log.Logger); }); var logger = loggerFactory.CreateLogger(); + var logger2 = loggerFactory.CreateLogger(); + _context.ConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter(logger2, logger); + // Seed mock data var seeder = new MockLdapDataSeeder(logger); seeder.SeedAllData(); diff --git a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs index 746abfb..9e5bd67 100644 --- a/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AccountManagerAdapterTests.cs @@ -1,4 +1,4 @@ -using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; using Microsoft.Extensions.Logging.Abstractions; @@ -12,9 +12,7 @@ public class AccountManagerAdapterTests: BaseTests { [Fact] public async Task CreateUserAccountForMsAD_ReturnsSuccess() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -46,9 +44,7 @@ public async Task CreateUserAccountForMsAD_ReturnsSuccess() { [Fact] public async Task CreateUserAccountForMsAD_MissingRequiredAttr_ReturnsError() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -79,9 +75,7 @@ public async Task CreateUserAccountForMsAD_MissingRequiredAttr_ReturnsError() { [Fact] public async Task SetUserAccountPasswordForMsAD_ValidAccount_ReturnsSuccess() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -91,8 +85,8 @@ public async Task SetUserAccountPasswordForMsAD_ValidAccount_ReturnsSuccess() { var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - // Use data from the seeder - james.dockers user - var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + // Disposable user, so the seeded data shared by all tests is never modified + var userDistinguishedName = CreateDisposableUser("setpassword"); var result = await accountManager.SetMsADUserAccountPassword(EntryAttribute.distinguishedName, userDistinguishedName, "TestPassword", postUpdateTestAuthentication: true); @@ -103,9 +97,7 @@ public async Task SetUserAccountPasswordForMsAD_ValidAccount_ReturnsSuccess() { [Fact] public async Task SetUserAccountPasswordForMsAD_AccountNotFound_ReturnsFailed() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -126,9 +118,7 @@ public async Task SetUserAccountPasswordForMsAD_AccountNotFound_ReturnsFailed() [Fact] public async Task DisableUserAccountForMsAD_ValidAccount_ReturnsSuccess() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -138,8 +128,8 @@ public async Task DisableUserAccountForMsAD_ValidAccount_ReturnsSuccess() { var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); - // Use data from the seeder - james.dockers user - var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + // Disposable user, so the seeded data shared by all tests is never modified + var userDistinguishedName = CreateDisposableUser("disable"); var result = await accountManager.DisableMsADUserAccount(EntryAttribute.distinguishedName, userDistinguishedName, "TestDisable"); @@ -150,9 +140,7 @@ public async Task DisableUserAccountForMsAD_ValidAccount_ReturnsSuccess() { [Fact] public async Task DisableUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -174,9 +162,7 @@ public async Task DisableUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { [Fact] public async Task RemoveUserAccountForMsAD_ValidAccount_ReturnsSuccess() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); @@ -184,8 +170,8 @@ public async Task RemoveUserAccountForMsAD_ValidAccount_ReturnsSuccess() { var credential = new LDAPDomainAccountCredential("domain", "admin", "p@55w0rd"); - // Use data from the seeder - james.dockers user - var userDistinguishedName = "CN=James Dockers,OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + // Disposable user, so the seeded data shared by all tests is never modified + var userDistinguishedName = CreateDisposableUser("remove"); var accountManager = new AccountManager(connectionInfo, searchLimits, credential, mockConnectionFactory); @@ -197,9 +183,7 @@ public async Task RemoveUserAccountForMsAD_ValidAccount_ReturnsSuccess() { [Fact] public async Task RemoveUserAccountForMsAD_AccountNotFound_ReturnsSuccess() { - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var connectionInfo = CreateValidConnectionInfo(ssl: true); diff --git a/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs index f7c3ec2..e3c8b75 100644 --- a/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/AuthenticatorAdapterTests.cs @@ -1,4 +1,4 @@ -using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; using Microsoft.Extensions.Logging.Abstractions; @@ -13,9 +13,7 @@ public class AuthenticatorAdapterTests : BaseTests [Fact] public async Task AuthenticateUser_ReturnsSuccess() { //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -38,9 +36,7 @@ public async Task AuthenticateUser_WithVerification_ReturnsSuccess() { var searchLimits = CreateValidSearchLimits(); //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -64,9 +60,7 @@ public async Task AuthenticateUser_WithVerification_ReturnsSuccess() { [Fact] public async Task AuthenticateDN_ReturnsSuccess() { //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -92,9 +86,7 @@ public async Task AuthenticateDN_WithVerification_ReturnsSuccess() { var searchLimits = CreateValidSearchLimits(); //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -118,9 +110,7 @@ public async Task AuthenticateDN_WithVerification_ReturnsSuccess() { [Fact] public async Task AuthenticateUser_ReturnsFailed() { //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -144,9 +134,7 @@ public async Task AuthenticateUser_WithVerification_ReturnsFailed() { var searchLimits = CreateValidSearchLimits(); //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -168,9 +156,7 @@ public async Task AuthenticateUser_WithVerification_ReturnsFailed() { [Fact] public async Task AuthenticateDN_ReturnsFailed() { //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); @@ -197,9 +183,7 @@ public async Task AuthenticateDN_WithVerification_ReturnsFailed() { var searchLimits = CreateValidSearchLimits(); //Mock connection factory - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; //Connection information var connectionInfo = CreateValidConnectionInfo(true); diff --git a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs index 33b6dd1..8120cb4 100644 --- a/tests/Bitai.LDAPHelper.Tests/BaseTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/BaseTests.cs @@ -19,6 +19,32 @@ protected MockLdapEntryAdapter FindEntryInStore(string distinguishedName) return entry; } + /// + /// Adds a disposable user with a unique DN to the shared mock data store, so tests that modify or + /// delete accounts never touch the users created by the seeder. Returns the DN of the new user. + /// + protected string CreateDisposableUser(string prefix) + { + var id = Guid.NewGuid().ToString("N"); + var samAccountName = $"{prefix}.{id}"; + var distinguishedName = $"CN={prefix} {id},OU=Seniors,OU=DevOps,OU=IT,DC=va,DC=bitai,DC=com"; + + var entry = new MockLdapEntryAdapter(distinguishedName); + entry.AddAttribute("objectGuid", Guid.NewGuid().ToByteArray()); + entry.AddAttribute("sAMAccountName", samAccountName); + entry.AddAttribute("sAMAccountType", "805306368"); + entry.AddAttribute("cn", $"{prefix} {id}"); + entry.AddAttribute("name", $"{prefix} {id}"); + entry.AddAttribute("displayName", $"{prefix} {id}"); + entry.AddAttribute("userPrincipalName", $"{samAccountName}@va.bitai.com"); + entry.AddAttribute("userAccountControl", "512"); + entry.AddAttribute("objectClass", new[] { "top", "person", "organizationalPerson", "user" }); + + MockLdapDataStore.Instance.AddOrUpdateEntry(entry); + + return distinguishedName; + } + /// /// Creates a search filter for a given attribute and value. /// diff --git a/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs b/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs index 53a0fda..5e2b2a3 100644 --- a/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/GroupMembershipValidatorTests.cs @@ -1,4 +1,4 @@ -using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; using Microsoft.Extensions.Logging.Abstractions; @@ -25,9 +25,7 @@ public GroupMembershipValidatorTests() { [Fact] public async Task CheckGroupMembershipAsync_UserIsDirectMember_ReturnsTrue() { // Arrange - james.dockers is direct member of DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -39,9 +37,7 @@ public async Task CheckGroupMembershipAsync_UserIsDirectMember_ReturnsTrue() { [Fact] public async Task CheckGroupMembershipAsync_UserIsIndirectMember_ReturnsTrue() { // Arrange - sara.pikes -> JuniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -55,9 +51,7 @@ public async Task CheckGroupMembershipAsync_UserIsIndirectMember_ReturnsTrue() { [Fact] public async Task CheckGroupMembershipAsync_UserIsMemberThroughMultipleLevels_ReturnsTrue() { // Arrange - james.dockers -> SeniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -71,9 +65,7 @@ public async Task CheckGroupMembershipAsync_UserIsMemberThroughMultipleLevels_Re [Fact] public async Task CheckGroupMembershipAsync_UserIsNotMember_ReturnsFalse() { // Arrange - sara.pikes is not a member of DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -87,9 +79,7 @@ public async Task CheckGroupMembershipAsync_UserIsNotMember_ReturnsFalse() { [Fact] public async Task CheckGroupMembershipAsync_UserNotFound_ThrowsException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -99,9 +89,7 @@ public async Task CheckGroupMembershipAsync_UserNotFound_ThrowsException() { [Fact] public async Task CheckGroupMembershipAsync_CaseInsensitiveComparison_ReturnsTrue() { // Arrange - james.dockers is member of DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -113,9 +101,7 @@ public async Task CheckGroupMembershipAsync_CaseInsensitiveComparison_ReturnsTru [Fact] public async Task CheckGroupMembershipAsync_NullSAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -127,9 +113,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_EmptySAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -141,9 +125,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_SAMAccountNameContainsWildcard_ThrowsArgumentException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -155,9 +137,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_NullParentGroupCN_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -169,9 +149,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_EmptyParentGroupCN_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -183,9 +161,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_ParentGroupCNContainsWildcard_ThrowsArgumentException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -197,9 +173,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_UserInMultipleGroups_FindsCorrectGroup() { // Arrange - james.dockers is member of multiple groups - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -215,9 +189,7 @@ public async Task CheckGroupMembershipAsync_UserInMultipleGroups_FindsCorrectGro [Fact] public async Task GetAllGroupMembershipsAsync_UserWithNestedMemberships_ReturnsAllGroupsIncludingIndirect() { // Arrange - sara.pikes -> JuniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -236,9 +208,7 @@ public async Task GetAllGroupMembershipsAsync_UserWithNestedMemberships_ReturnsA [Fact] public async Task GetAllGroupMembershipsAsync_UserWithDeepNesting_ReturnsAllGroupsInHierarchy() { // Arrange - james.dockers -> SeniorDevOps -> DevOpsEng -> ITAdmins -> DomainAdmins -> Administrators - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -258,9 +228,7 @@ public async Task GetAllGroupMembershipsAsync_UserWithDeepNesting_ReturnsAllGrou [Fact] public async Task GetAllGroupMembershipsAsync_UserNotFound_ThrowsException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -271,9 +239,7 @@ public async Task GetAllGroupMembershipsAsync_UserNotFound_ThrowsException() { [Fact] public async Task GetAllGroupMembershipsAsync_RemovesDuplicateGroups() { // Arrange - james.dockers has multiple paths to same groups - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -288,9 +254,7 @@ public async Task GetAllGroupMembershipsAsync_RemovesDuplicateGroups() { [Fact] public async Task GetAllGroupMembershipsAsync_CaseInsensitiveDistinct_ReturnsUniqueGroups() { // Arrange - james.dockers has multiple paths to same groups - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -305,9 +269,7 @@ public async Task GetAllGroupMembershipsAsync_CaseInsensitiveDistinct_ReturnsUni [Fact] public async Task GetAllGroupMembershipsAsync_NullSAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -319,9 +281,7 @@ await Assert.ThrowsAsync( [Fact] public async Task GetAllGroupMembershipsAsync_EmptySAMAccountName_ThrowsArgumentNullException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -333,9 +293,7 @@ await Assert.ThrowsAsync( [Fact] public async Task GetAllGroupMembershipsAsync_SAMAccountNameContainsWildcard_ThrowsArgumentException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -351,9 +309,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CheckGroupMembershipAsync_WithMultipleNestedPaths_ReturnsTrueIfAnyPathLeadsToTarget() { // Arrange - james.dockers has multiple paths to DomainAdmins - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -367,9 +323,7 @@ public async Task CheckGroupMembershipAsync_WithMultipleNestedPaths_ReturnsTrueI [Fact] public async Task GetAllGroupMembershipsAsync_WhenSearchFails_ThrowsException() { // Arrange - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); @@ -383,9 +337,7 @@ public async Task GetAllGroupMembershipsAsync_WhenSearchFails_ThrowsException() [Fact] public async Task CheckGroupMembershipAsync_WithWhitespaceInCN_HandlesCorrectly() { // Arrange - No groups with whitespace in seeder, use existing group - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var validator = new GroupMembershipValidator(_validConnectionInfo, _validSearchLimits, _validCredential, mockConnectionFactory); diff --git a/tests/Bitai.LDAPHelper.Tests/LdapMockFixture.cs b/tests/Bitai.LDAPHelper.Tests/LdapMockFixture.cs new file mode 100644 index 0000000..0f08c2f --- /dev/null +++ b/tests/Bitai.LDAPHelper.Tests/LdapMockFixture.cs @@ -0,0 +1,21 @@ +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; +using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Bitai.LDAPHelper.Tests +{ + /// + /// Provides a single shared by every test in the assembly, + /// so the mock data store is seeded exactly once. + /// + public static class LdapMockFixture + { + private static readonly Lazy _factory = new( + () => new MockLdapPersistentConnectionFactoryAdapter( + NullLogger.Instance, + NullLogger.Instance), + LazyThreadSafetyMode.ExecutionAndPublication); + + public static MockLdapPersistentConnectionFactoryAdapter Factory => _factory.Value; + } +} diff --git a/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs b/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs index 0c2ea21..60ed534 100644 --- a/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs +++ b/tests/Bitai.LDAPHelper.Tests/SearcherAdapterTests.cs @@ -1,4 +1,4 @@ -using Bitai.LDAPHelper.DTO; +using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; using Microsoft.Extensions.Logging.Abstractions; @@ -14,9 +14,7 @@ public class SearcherAdapterTests : BaseTests public async Task SearchEntries_ReturnsExpectedEntries() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var searchLimits = CreateValidSearchLimits(); @@ -37,9 +35,7 @@ public async Task SearchEntries_ReturnsExpectedEntries() { public async Task SearchEntries_ReturnsEmptyList() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var searchLimits = CreateValidSearchLimits(); @@ -60,9 +56,7 @@ public async Task SearchEntries_ReturnsEmptyList() { public async Task SearchParentEntries_ReturnsExpectedEntries() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var searchLimits = CreateValidSearchLimits(); @@ -85,9 +79,7 @@ public async Task SearchParentEntries_ReturnsExpectedEntries() { public async Task SearchParentEntries_ReturnsEmptyList() { var connectionInfo = CreateValidConnectionInfo(ssl: true); - var mockConnectionFactory = new MockLdapPersistentConnectionFactoryAdapter( - NullLogger.Instance, - NullLogger.Instance); + var mockConnectionFactory = LdapMockFixture.Factory; var searchLimits = CreateValidSearchLimits(); From 3fa6d67ba803074f2d388eb28b5a305f1bf70d9a Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Wed, 30 Sep 2026 21:20:06 -0400 Subject: [PATCH 6/7] Remove deprecated mocks --- README.md | 6 +- ...PHelper.LdapAdapters.LdapHelperMock.csproj | 6 +- .../MockLdapConnectionAdapter.cs | 123 ------------------ .../MockLdapConnectionFactoryAdapter.cs | 39 ------ .../MockLdapPersistentConnectionAdapter.cs | 78 +++++++++-- .../README.md | 27 ++-- 6 files changed, 87 insertions(+), 192 deletions(-) delete mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs delete mode 100644 adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs diff --git a/README.md b/README.md index cd2b9ad..2273fc4 100644 --- a/README.md +++ b/README.md @@ -365,8 +365,8 @@ public class SearcherTests [Fact] public async Task GetUser_UsingMockAdapter_ShouldReturnStubbedUser() { - // 1. Instantiate in-memory mock connection - var mockConnection = new MockLdapConnectionAdapter(); + // 1. Instantiate persistent mock connection + var mockConnection = new MockLdapPersistentConnectionAdapter(); // 2. Add stubbed search response var stubEntry = new MockLdapEntryAdapter("CN=Test User,OU=Users,DC=example,DC=com"); @@ -376,7 +376,7 @@ public class SearcherTests mockConnection.AddSearchResult("(sAMAccountName=testuser)", new List { stubEntry }); // 3. Pass mock factory to Searcher - var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + var factory = new MockLdapPersistentConnectionFactoryAdapter(); var searcher = new Searcher( new ConnectionInfo("localhost", 389, false, 5), new SearchLimits("DC=example,DC=com"), diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj index e5897b6..33da567 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj @@ -5,9 +5,9 @@ BITAI LDAP Services Wrappers © 2026 BITAI. All rights reserved. - 10.1.0 - 10.1.0 - 10.1.0 + 10.2.0 + 10.2.0 + 10.2.0 hierarchy_32.png true Bitai.LDAPHelper.LdapAdapters.LdapHelperMock diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs deleted file mode 100644 index 368c9b9..0000000 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionAdapter.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Bitai.LDAPHelper.LdapAdapters; -using Bitai.LDAPHelper.DTO; - -namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; - -/// -/// In-memory mock LDAP connection used by tests to simulate bind/search/add/modify/delete operations. -/// -public class MockLdapConnectionAdapter : ILdapConnectionAdapter -{ - private bool _disposed = false; - private Dictionary> _searchResults = new(); - private bool _isBound = false; - - public int ConnectionTimeout { get; set; } - public bool SecureSocketLayer { get; set; } - public bool IsBound => _isBound; - public List CreatedEntries { get; } = new List(); - public List Modifications { get; } = new List(); - public List DeletedEntries { get; } = new List(); - - public void AddSearchResult(string filterPattern, List entries) { - _searchResults[filterPattern] = entries; - } - - public void ServerCertificateValidationByPass() { - throw new InvalidOperationException($"{nameof(ServerCertificateValidationByPass)}: Not allowed in mocked classes!"); - } - - public Task ConnectAsync(string host, int port) { - if (string.IsNullOrEmpty(host) || host == "unknown" || host == "0.0.0.0" || port <= 0) - throw new Exception($"{nameof(MockLdapConnectionAdapter)}.{nameof(MockLdapConnectionAdapter.ConnectAsync)}: Invalid server connection!"); - - return Task.CompletedTask; - } - - public Task BindAsync(string userDN, string password) { - if (string.IsNullOrEmpty(userDN) || string.IsNullOrEmpty(password) || userDN.Contains("hacker") || password.Contains("123456")) - throw new LdapOperationException($"{nameof(MockLdapConnectionAdapter)}.{nameof(MockLdapConnectionAdapter.BindAsync)}: Invalid credentials!"); - - if (password.Equals("wrongpassword", StringComparison.OrdinalIgnoreCase)) - _isBound = false; - else - _isBound = !string.IsNullOrEmpty(userDN) && !string.IsNullOrEmpty(password); - - return Task.CompletedTask; - } - - public virtual Task SearchAsync(ISearchLimits searchLimits, string searchFilter, string[] attributeNames, bool typesOnly) { - var matchingEntries = new List(); - - // Find matching results based on filter - foreach (var kvp in _searchResults) { - if (searchFilter.Contains(kvp.Key, StringComparison.OrdinalIgnoreCase) || kvp.Key.Equals(searchFilter, StringComparison.OrdinalIgnoreCase)) { - matchingEntries.AddRange(kvp.Value); - } - } - - var mockQueue = new MockLdapSearchQueueAdapter(); - foreach (var entry in matchingEntries) { - mockQueue.AddSearchResult(new MockLdapMessageAdapter(entry)); - } - - return Task.FromResult(mockQueue); - } - - public ILdapAttributeSetAdapter CreateAttributeSet() { - return new MockLdapAttributeSetAdapter(); - } - - public virtual Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAdapter attributes) { - var mockAttributes = (MockLdapAttributeSetAdapter)attributes; - if (!mockAttributes.ContainsKey(EntryAttribute.distinguishedName.ToString())) - mockAttributes.AddAttribute(EntryAttribute.distinguishedName.ToString(), distinguishedName); - - var entry = new MockLdapEntryAdapter(distinguishedName, mockAttributes); - - CreatedEntries.Add(entry); - - return Task.CompletedTask; - } - - public ILdapModificationAdapter CreateModification(LdapModificationType type, string attributeName, object value) { - return new MockLdapModificationAdapter(type, attributeName, value); - } - - public virtual Task ModifyEntryAsync(string distinguishedName, IEnumerable modifications) { - foreach (var mod in modifications) { - var mockMod = (MockLdapModificationAdapter)mod; - - Modifications.Add(new MockModification { - DistinguishedName = distinguishedName, - ModificationType = mockMod.ModificationType, - AttributeName = mockMod.AttributeName, - Value = mockMod.Value - }); - } - return Task.CompletedTask; - } - - public virtual Task DeleteEntryAsync(string distinguishedName) { - DeletedEntries.Add(distinguishedName); - return Task.CompletedTask; - } - - public void Disconnect() { - _isBound = false; - } - - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) { - if (!_disposed) { - if (disposing) { - // Cleanup - } - _disposed = true; - } - } -} diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs deleted file mode 100644 index a1c7e70..0000000 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapConnectionFactoryAdapter.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; - -/// -/// Mock implementation of that returns a provided mock _connection. -/// -public class MockLdapConnectionFactoryAdapter : ILdapConnectionFactoryAdapter -{ - private readonly MockLdapConnectionAdapter _connection; - - public MockLdapConnectionFactoryAdapter(MockLdapConnectionAdapter connection) - { - _connection = connection; - } - - public async Task CreateConnectionAsync( - IConnectionInfo connectionInfo, - string userAccount, - string password, - bool bindRequired = true) { - - _connection.ConnectionTimeout = connectionInfo.ConnectionTimeout; - _connection.SecureSocketLayer = connectionInfo.UseSSL; - - await _connection.ConnectAsync(connectionInfo.Server, connectionInfo.ServerPort); - - try { - await _connection.BindAsync(userAccount, password); - } - catch (LdapOperationException) { - if (bindRequired) - throw; - } - catch (Exception) { - throw; - } - - return (ILdapConnectionAdapter)_connection; - } -} diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs index 684d7d9..f8c49b4 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/MockLdapPersistentConnectionAdapter.cs @@ -1,26 +1,88 @@ using System.Text; using Bitai.LDAPHelper.LdapAdapters; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.LdapData; +using Bitai.LDAPHelper.DTO; namespace Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; /// /// Persistent mock connection backed by a shared in-memory LDAP data store. /// -public class MockLdapPersistentConnectionAdapter : MockLdapConnectionAdapter +public class MockLdapPersistentConnectionAdapter : ILdapConnectionAdapter { private readonly MockLdapDataStore _dataStore; + private bool _disposed = false; + private Dictionary> _searchResults = new(); + private bool _isBound = false; + + public int ConnectionTimeout { get; set; } + public bool SecureSocketLayer { get; set; } + public bool IsBound => _isBound; + public List CreatedEntries { get; } = new List(); + public List Modifications { get; } = new List(); + public List DeletedEntries { get; } = new List(); + + public MockLdapPersistentConnectionAdapter() + { + _dataStore = MockLdapDataStore.Instance; + } + + public void AddSearchResult(string filterPattern, List entries) { + _searchResults[filterPattern] = entries; + } + public void ServerCertificateValidationByPass() { + throw new InvalidOperationException($"{nameof(ServerCertificateValidationByPass)}: Not allowed in mocked classes!"); + } + public Task ConnectAsync(string host, int port) { + if (string.IsNullOrEmpty(host) || host == "unknown" || host == "0.0.0.0" || port <= 0) + throw new Exception($"{nameof(MockLdapPersistentConnectionAdapter)}.{nameof(ConnectAsync)}: Invalid server connection!"); - public MockLdapPersistentConnectionAdapter() : base() - { - _dataStore = MockLdapDataStore.Instance; + return Task.CompletedTask; + } + + public Task BindAsync(string userDN, string password) { + if (string.IsNullOrEmpty(userDN) || string.IsNullOrEmpty(password) || userDN.Contains("hacker") || password.Contains("123456")) + throw new LdapOperationException($"{nameof(MockLdapPersistentConnectionAdapter)}.{nameof(BindAsync)}: Invalid credentials!"); + + if (password.Equals("wrongpassword", StringComparison.OrdinalIgnoreCase)) + _isBound = false; + else + _isBound = !string.IsNullOrEmpty(userDN) && !string.IsNullOrEmpty(password); + + return Task.CompletedTask; + } + + public ILdapAttributeSetAdapter CreateAttributeSet() { + return new MockLdapAttributeSetAdapter(); + } + + public ILdapModificationAdapter CreateModification(LdapModificationType type, string attributeName, object value) { + return new MockLdapModificationAdapter(type, attributeName, value); + } + + public void Disconnect() { + _isBound = false; + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { + if (!_disposed) { + if (disposing) { + // Cleanup + } + _disposed = true; + } } - public override Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAdapter attributes) + public Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAdapter attributes) { var mockAttributes = (MockLdapAttributeSetAdapter)attributes; var entry = new MockLdapEntryAdapter(distinguishedName); @@ -49,7 +111,7 @@ public override Task AddEntryAsync(string distinguishedName, ILdapAttributeSetAd return Task.CompletedTask; } - public override Task ModifyEntryAsync(string distinguishedName, IEnumerable modifications) + public Task ModifyEntryAsync(string distinguishedName, IEnumerable modifications) { var entry = _dataStore.GetEntry(distinguishedName); if (entry == null) @@ -137,7 +199,7 @@ public override Task ModifyEntryAsync(string distinguishedName, IEnumerable SearchAsync( + public Task SearchAsync( ISearchLimits searchLimits, string searchFilter, string[] attributeNames, diff --git a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md index 5faabd2..81bc43b 100644 --- a/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md +++ b/adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/README.md @@ -91,10 +91,9 @@ In the `Bitai.LDAPHelper` ecosystem, core services interact exclusively with LDA ## Key Features -1. **In-Memory Connection Simulation (`MockLdapConnectionAdapter`)** +1. **Persistent Shared State (`MockLdapPersistentConnectionAdapter`)** - Simulates `ConnectAsync`, `BindAsync`, `SearchAsync`, `AddEntryAsync`, `ModifyEntryAsync`, and `DeleteEntryAsync`. - Supports registerable search results via `AddSearchResult(filterPattern, entries)`. -2. **Persistent Shared State (`MockLdapPersistentConnectionAdapter`)** - Backed by `MockLdapDataStore` to persist entry creations, modifications, and deletions across multiple connections. - Evaluates search filters dynamically against stored records. 3. **Deterministic Directory Seeder (`MockLdapDataSeeder`)** @@ -116,8 +115,6 @@ adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/ ├── Bitai.LDAPHelper.LdapAdapters.LdapHelperMock.csproj ├── README.md ├── LICENSE.md -├── MockLdapConnectionAdapter.cs -├── MockLdapConnectionFactoryAdapter.cs ├── MockLdapPersistentConnectionAdapter.cs ├── MockLdapPersistentConnectionFactoryAdapter.cs ├── MockLdapEntryAdapter.cs @@ -135,9 +132,7 @@ adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/ | Class | Implemented Interface | Primary Responsibility | |---|---|---| -| `MockLdapConnectionAdapter` | `ILdapConnectionAdapter` | Standard in-memory mock connection with configurable search results & side-effect trackers. | -| `MockLdapConnectionFactoryAdapter` | `ILdapConnectionFactoryAdapter` | Factory returning an instance of `MockLdapConnectionAdapter`. | -| `MockLdapPersistentConnectionAdapter` | `ILdapConnectionAdapter` | Persistent mock connection connected to the central `MockLdapDataStore`. | +| `MockLdapPersistentConnectionAdapter` | `ILdapConnectionAdapter` | Persistent mock connection connected to the central `MockLdapDataStore`. Supports configurable search results & side-effect trackers. | | `MockLdapPersistentConnectionFactoryAdapter` | `ILdapConnectionFactoryAdapter` | Factory producing `MockLdapPersistentConnectionAdapter` instances. | | `MockLdapEntryAdapter` | `ILdapEntryAdapter` | Represents an LDAP entry with a DN and attribute set. | | `MockLdapAttributeSetAdapter` | `ILdapAttributeSetAdapter` | Dictionary-backed container for entry attributes. Includes verification helpers. | @@ -175,8 +170,8 @@ using Bitai.LDAPHelper; using Bitai.LDAPHelper.DTO; using Bitai.LDAPHelper.LdapAdapters.LdapHelperMock; -// 1. Create a mock connection and configure stubbed search results -var mockConnection = new MockLdapConnectionAdapter(); +// 1. Create a persistent mock connection +var mockConnection = new MockLdapPersistentConnectionAdapter(); var userEntry = new MockLdapEntryAdapter("CN=John Doe,OU=Users,DC=example,DC=com"); userEntry.AddAttribute("cn", "John Doe"); @@ -186,7 +181,7 @@ userEntry.AddAttribute("userPrincipalName", "jdoe@example.com"); mockConnection.AddSearchResult("(sAMAccountName=jdoe)", new List { userEntry }); // 2. Wrap in a factory -var factory = new MockLdapConnectionFactoryAdapter(mockConnection); +var factory = new MockLdapPersistentConnectionFactoryAdapter(); // 3. Initialize LDAPHelper services using the mock factory var connectionInfo = new ConnectionInfo("localhost", 389, useSSL: false, connectionTimeout: 15); @@ -220,7 +215,7 @@ public class UserServiceTests public async Task GetUser_ShouldReturnMatchingLdapEntry() { // Arrange - var mockConnection = new MockLdapConnectionAdapter(); + var mockConnection = new MockLdapPersistentConnectionAdapter(); var expectedDn = "CN=Alice Smith,OU=Engineering,DC=corp,DC=local"; var entry = new MockLdapEntryAdapter(expectedDn); @@ -231,7 +226,7 @@ public class UserServiceTests mockConnection.AddSearchResult("asmith", new List { entry }); - var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + var factory = new MockLdapPersistentConnectionFactoryAdapter(); var searcher = new Searcher( new ConnectionInfo("ldap.corp.local", 389, false, 10), new SearchLimits("DC=corp,DC=local"), @@ -300,7 +295,7 @@ public class AuthenticationIntegrationTests ### Scenario 3: Verifying Account Management Side Effects -When testing routines that create, modify, or delete directory objects (e.g. `AccountManager`), use `MockLdapConnectionAdapter` to verify generated modifications: +When testing routines that create, modify, or delete directory objects (e.g. `AccountManager`), use `MockLdapPersistentConnectionAdapter` to verify generated modifications: ```csharp using Xunit; @@ -315,8 +310,8 @@ public class AccountManagerTests public async Task ModifyUserAttribute_ShouldRecordModification() { // Arrange - var mockConnection = new MockLdapConnectionAdapter(); - var factory = new MockLdapConnectionFactoryAdapter(mockConnection); + var mockConnection = new MockLdapPersistentConnectionAdapter(); + var factory = new MockLdapPersistentConnectionFactoryAdapter(); var connectionInfo = new ConnectionInfo("localhost", 389, false, 10); var searchLimits = new SearchLimits("DC=example,DC=com"); @@ -413,7 +408,7 @@ dotnet pack adapters/Bitai.LDAPHelper.LdapAdapters.LdapHelperMock/Bitai.LDAPHelp ## Observability & Diagnostic Assertions -When debugging unit or integration tests, `MockLdapConnectionAdapter` provides diagnostic tracking properties: +When debugging unit or integration tests, `MockLdapPersistentConnectionAdapter` provides diagnostic tracking properties: ```csharp // Inspect entries created during the test run From e88ea5101a340732ad119fe87a68505a80c33737 Mon Sep 17 00:00:00 2001 From: Viko Bastidas Date: Wed, 30 Sep 2026 21:33:00 -0400 Subject: [PATCH 7/7] Fix version --- src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj b/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj index ea0ebf0..24284ff 100644 --- a/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj +++ b/src/Bitai.LDAPHelper.DTO/Bitai.LDAPHelper.DTO.csproj @@ -2,9 +2,9 @@ net10.0 - 10.2.0 - 10.2.0 - 10.2.0 + 10.1.0 + 10.1.0 + 10.1.0 Bitai.LDAPHelper.DTO true Viko Bastidas (BITAI)