diff --git a/.github/workflows/verify-prs-and-commits.yml b/.github/workflows/verify-prs-and-commits.yml index 7eeec5c..6a223cd 100644 --- a/.github/workflows/verify-prs-and-commits.yml +++ b/.github/workflows/verify-prs-and-commits.yml @@ -9,9 +9,10 @@ on: jobs: call-build: - uses: Yvand/AzureCP/.github/workflows/reusable-build.yml@master + uses: Yvand/EntraCP/.github/workflows/reusable-build.yml@master with: project-name: ${{ vars.PROJECT_NAME }} version-major-minor: ${{ vars.VERSION_MAJOR_MINOR }} + is-production-release: false secrets: base64-encoded-signingkey: ${{ secrets.BASE64_ENCODED_SIGNINGKEY }} diff --git a/LDAPCP.Tests/AugmentationTests.cs b/LDAPCP.Tests/AugmentationTests.cs deleted file mode 100644 index 5fb8142..0000000 --- a/LDAPCP.Tests/AugmentationTests.cs +++ /dev/null @@ -1,176 +0,0 @@ -using ldapcp; -using Newtonsoft.Json; -using NUnit.Framework; -using System.Collections.Generic; -using System.DirectoryServices; -using System.IO; - -namespace LDAPCP.Tests -{ - [TestFixture] - public class AugmentWithCustomLDAPConnectionsAsADDomainTests : EntityTestsBase - { - public override bool TestSearch => false; - public override bool TestValidation => false; - public override bool TestAugmentation => true; - - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - Config.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - - string json = File.ReadAllText(UnitTestsHelper.CustomLDAPConnections); - List ldapConnections = JsonConvert.DeserializeObject>(json); - Config.LDAPConnectionsProp = ldapConnections; - foreach (LDAPConnection coco in Config.LDAPConnectionsProp) - { - coco.UseSPServerConnectionToAD = false; - coco.EnableAugmentation = true; - coco.GetGroupMembershipUsingDotNetHelpers = true; - coco.AuthenticationSettings = AuthenticationTypes.Secure | AuthenticationTypes.Signing | AuthenticationTypes.Sealing; - } - Config.Update(); - } - } - - public class AugmentWithCustomLDAPConnectionsAsLDAPServerTests : EntityTestsBase - { - public override bool TestSearch => false; - public override bool TestValidation => false; - public override bool TestAugmentation => true; - - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - Config.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - - string json = File.ReadAllText(UnitTestsHelper.CustomLDAPConnections); - List ldapConnections = JsonConvert.DeserializeObject>(json); - Config.LDAPConnectionsProp = ldapConnections; - foreach (LDAPConnection coco in Config.LDAPConnectionsProp) - { - coco.UseSPServerConnectionToAD = false; - coco.EnableAugmentation = true; - coco.GetGroupMembershipUsingDotNetHelpers = false; - coco.AuthenticationSettings = AuthenticationTypes.Secure | AuthenticationTypes.Signing | AuthenticationTypes.Sealing; - } - Config.Update(); - } - } - - [TestFixture] - public class AugmentatAsADDomainOnBaseConfigTests : EntityTestsBase - { - public override bool TestSearch => false; - public override bool TestValidation => false; - public override bool TestAugmentation => true; - - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - Config.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - foreach (LDAPConnection ldapConn in Config.LDAPConnectionsProp) - { - ldapConn.EnableAugmentation = true; - ldapConn.GetGroupMembershipUsingDotNetHelpers = true; - } - Config.Update(); - } - } - - [TestFixture] - [Parallelizable(ParallelScope.Children)] - public class AugmentAsADDomaOnCustomConfigTests : CustomConfigTests - { - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - Config.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - foreach (LDAPConnection ldapConn in Config.LDAPConnectionsProp) - { - ldapConn.EnableAugmentation = true; - ldapConn.GetGroupMembershipUsingDotNetHelpers = true; - } - Config.Update(); - } - - /// - /// Test augmentation from the data file. Assumes augmentation is enabled and configured in the claims provider (handled in the constructor) - /// - /// - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData")] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public void TestAugmentation(ValidateEntityData registrationData) - { - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup); - } - } - - [TestFixture] - [Parallelizable(ParallelScope.Children)] - public class AugmentAsLDAPServersOnBaseConfigTests : EntityTestsBase - { - public override bool TestSearch => false; - public override bool TestValidation => false; - public override bool TestAugmentation => true; - - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - Config.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - foreach (LDAPConnection ldapConn in Config.LDAPConnectionsProp) - { - ldapConn.EnableAugmentation = true; - ldapConn.GetGroupMembershipUsingDotNetHelpers = false; - } - Config.Update(); - } - -#if DEBUG - [TestCase("yvand@contoso.local", true)] - [TestCase("zzzyvand@contoso.local", false)] - public override void DEBUG_AugmentEntity(string claimValue, bool isMemberOfTrustedGroup) - { - //LDAPConnection coco = new LDAPConnection(); - //coco.AugmentationEnabled = true; - //coco.GetGroupMembershipAsADDomain = false; - //coco.UseSPServerConnectionToAD = false; - //coco.Path = "LDAP://test"; - //coco.Username = "userTest"; - //Config.LDAPConnectionsProp.Add(coco); - //Config.Update(); - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, claimValue, isMemberOfTrustedGroup); - } -#endif - } - - [TestFixture] - public class AugmentAsLDAPServersOnCustomConfigTests : CustomConfigTests - { - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - Config.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - foreach (LDAPConnection ldapConn in Config.LDAPConnectionsProp) - { - ldapConn.EnableAugmentation = true; - ldapConn.GetGroupMembershipUsingDotNetHelpers = false; - ldapConn.GroupMembershipLDAPAttributes = new string[] { "memberOf", "uniquememberof" }; - } - Config.Update(); - } - - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData")] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public void TestAugmentation(ValidateEntityData registrationData) - { - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup); - } - } -} diff --git a/LDAPCP.Tests/BackupCurrentConfig.cs b/LDAPCP.Tests/BackupCurrentConfig.cs deleted file mode 100644 index 29d933e..0000000 --- a/LDAPCP.Tests/BackupCurrentConfig.cs +++ /dev/null @@ -1,46 +0,0 @@ -using ldapcp; -using NUnit.Framework; -using System; -using System.Diagnostics; - -namespace LDAPCP.Tests -{ - /// - /// This class creates a backup of current configuration and provides one that can be modified as needed. At the end of the test, initial configuration will be restored. - /// - public class BackupCurrentConfig - { - protected LDAPCPConfig Config; - private LDAPCPConfig BackupConfig; - - [OneTimeSetUp] - public void Init() - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} Start backup of current LDAPCP configuration"); - Config = LDAPCPConfig.GetConfiguration(UnitTestsHelper.ClaimsProviderConfigName, UnitTestsHelper.SPTrust.Name); - if (Config == null) - { - Trace.TraceWarning($"{DateTime.Now.ToString("s")} Configuration {UnitTestsHelper.ClaimsProviderConfigName} does not exist, create it with default settings..."); - Config = LDAPCPConfig.CreateConfiguration(ClaimsProviderConstants.CONFIG_ID, ClaimsProviderConstants.CONFIG_NAME, UnitTestsHelper.SPTrust.Name); - } - BackupConfig = Config.CopyConfiguration(); - InitializeConfiguration(); - } - - /// - /// Initialize configuration - /// - public virtual void InitializeConfiguration() - { - UnitTestsHelper.InitializeConfiguration(Config); - } - - [OneTimeTearDown] - public void Cleanup() - { - Config.ApplyConfiguration(BackupConfig); - Config.Update(); - Trace.WriteLine($"{DateTime.Now.ToString("s")} Restored original settings of LDAPCP configuration"); - } - } -} diff --git a/LDAPCP.Tests/CustomConfigTests.cs b/LDAPCP.Tests/CustomConfigTests.cs deleted file mode 100644 index 3549854..0000000 --- a/LDAPCP.Tests/CustomConfigTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -using ldapcp; -using Microsoft.SharePoint.Administration.Claims; -using NUnit.Framework; -using System; -using System.Linq; -using System.Security.Claims; - -namespace LDAPCP.Tests -{ - /// - /// Apply following configuration: - /// - Enable augmentation on all LDAPConnectionsProp - /// - Set property ClaimTypeConfig.PrefixToBypassLookup to "bypass-user:" on identity claim type - /// - Set property ClaimTypeConfig.PrefixToBypassLookup to "bypass-group:" and ClaimTypeConfig.ClaimValuePrefix to "{fqdn}\" on group claim type - /// - [TestFixture] - public class CustomConfigTests : BackupCurrentConfig - { - public static string GroupsClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - private readonly object LockUpdateDynamicTokensConfig = new object(); - - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - - // Extra initialization for current test class - Config.EnableAugmentation = true; - Config.LDAPConnectionsProp.ForEach(x => x.EnableAugmentation = true); - Config.ClaimTypes.GetByClaimType(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType).PrefixToBypassLookup = "bypass-user:"; - Config.ClaimTypes.GetByClaimType(UnitTestsHelper.TrustedGroupToAdd_ClaimType).PrefixToBypassLookup = "bypass-group:"; - Config.ClaimTypes.GetByClaimType(UnitTestsHelper.TrustedGroupToAdd_ClaimType).ClaimValuePrefix = @"{fqdn}\"; - Config.Update(); - } - - [TestCase("bypass-user:externalUser@contoso.com", 1, "externalUser@contoso.com")] - [TestCase("nonExistingUser@contoso.com", 0, "")] - [TestCase("bypass-user:", 0, "")] - public void BypassLookupOnIdentityClaimTest(string inputValue, int expectedCount, string expectedClaimValue) - { - UnitTestsHelper.TestSearchOperation(inputValue, expectedCount, expectedClaimValue); - - if (expectedCount > 0) - { - SPClaim inputClaim = new SPClaim(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, expectedClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, true, expectedClaimValue); - } - } - - [TestCase(@"bypass-group:domain\groupValue", 1, @"domain\groupValue")] - [TestCase(@"domain\groupValue", 0, "")] - [TestCase("bypass-group:", 0, "")] - public void BypassLookupOnGroupClaimTest(string inputValue, int expectedCount, string expectedClaimValue) - { - UnitTestsHelper.TestSearchOperation(inputValue, expectedCount, expectedClaimValue); - - if (expectedCount > 0) - { - SPClaim inputClaim = new SPClaim(UnitTestsHelper.TrustedGroupToAdd_ClaimType, expectedClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, true, expectedClaimValue); - } - } - - [TestCase("Domain Users")] - [TestCase("Domain Admins")] - [NonParallelizable] - public void TestDynamicTokens(string inputValue) - { - // With NonParallelizable set, this method is not called in parallel with other methods, but it seems it can still be called multiple times simultaneously, so lock is required due to the multiple config changes - lock (LockUpdateDynamicTokensConfig) - { - string domainNetbios = "contoso"; - string domainFQDN = "contoso.local"; - ClaimTypeConfig ctConfig = Config.ClaimTypes.FirstOrDefault(x => String.Equals(GroupsClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); - - string expectedValue = inputValue; - ctConfig.ClaimValuePrefix = String.Empty; - Config.Update(); - UnitTestsHelper.TestSearchOperation(inputValue, 1, inputValue); - SPClaim inputClaim = new SPClaim(GroupsClaimType, expectedValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, true, expectedValue); - - expectedValue = $@"{domainNetbios}\{inputValue}"; - ctConfig.ClaimValuePrefix = @"{domain}\"; - Config.Update(); - UnitTestsHelper.TestSearchOperation(inputValue, 1, expectedValue); - inputClaim = new SPClaim(GroupsClaimType, expectedValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, true, expectedValue); - - expectedValue = $@"{domainFQDN}\{inputValue}"; - ctConfig.ClaimValuePrefix = @"{fqdn}\"; // This is the default value, set at last step to restore it before releasing lock - Config.Update(); - UnitTestsHelper.TestSearchOperation(inputValue, 1, expectedValue); - inputClaim = new SPClaim(GroupsClaimType, expectedValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, true, expectedValue); - } - } - - [Test] - [NonParallelizable] - public void BypassServer() - { - Config.BypassLDAPLookup = true; - Config.Update(); - - try - { - UnitTestsHelper.TestSearchOperation(UnitTestsHelper.RandomClaimValue, Int32.MaxValue, UnitTestsHelper.RandomClaimValue); - - SPClaim inputClaim = new SPClaim(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, UnitTestsHelper.RandomClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, true, UnitTestsHelper.RandomClaimValue); - } - catch { } - finally - { - Config.BypassLDAPLookup = false; - Config.Update(); - } - } - } -} diff --git a/LDAPCP.Tests/EntityTestsBase.cs b/LDAPCP.Tests/EntityTestsBase.cs deleted file mode 100644 index 0014499..0000000 --- a/LDAPCP.Tests/EntityTestsBase.cs +++ /dev/null @@ -1,116 +0,0 @@ -using ldapcp; -using Microsoft.SharePoint.Administration.Claims; -using NUnit.Framework; -using System; -using System.DirectoryServices; -using System.Security.Claims; - -namespace LDAPCP.Tests -{ - [TestFixture] - [Parallelizable(ParallelScope.Children)] - public class EntityTestsBase : BackupCurrentConfig - { - /// - /// Configure whether to run entity search tests. - /// - public virtual bool TestSearch => true; - - /// - /// Configure whether to run entity validation tests. - /// - public virtual bool TestValidation => true; - - /// - /// Configure whether to run entity augmentation tests. By default, augmentation is disabled on LDAPCP. - /// - public virtual bool TestAugmentation => false; - - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - } - - [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData")] - [MaxTime(UnitTestsHelper.MaxTime)] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public virtual void SearchEntities(SearchEntityData registrationData) - { - if (!TestSearch) return; - - UnitTestsHelper.TestSearchOperation(registrationData.Input, registrationData.ExpectedResultCount, registrationData.ExpectedEntityClaimValue); - } - - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData")] - [MaxTime(UnitTestsHelper.MaxTime)] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public virtual void ValidateClaim(ValidateEntityData registrationData) - { - if (!TestValidation) return; - - SPClaim inputClaim = new SPClaim(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, registrationData.ShouldValidate, registrationData.ClaimValue); - } - - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData")] - [MaxTime(UnitTestsHelper.MaxTime)] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public virtual void AugmentEntity(ValidateEntityData registrationData) - { - if (!TestAugmentation) return; - - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup); - } - -#if DEBUG - //[TestCaseSource(typeof(SearchEntityDataSourceCollection))] - public virtual void DEBUG_SearchEntitiesFromCollection(string inputValue, string expectedCount, string expectedClaimValue) - { - UnitTestsHelper.TestSearchOperation(inputValue, Convert.ToInt32(expectedCount), expectedClaimValue); - } - - //[TestCase(@"group\ch", 1, @"contoso.local\group\chartest")] - //[TestCase(@"test)", 2, @"test)char@contoso.local")] - //[TestCase(@"group\ch", 1, @"group\chartest")] - [TestCase(@"user1", 1, @"user1@contoso.local")] - public virtual void DEBUG_SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) - { - if (!TestSearch) return; - - LDAPConnection coco = new LDAPConnection(); - coco.EnableAugmentation = true; - coco.GetGroupMembershipUsingDotNetHelpers = false; - coco.UseSPServerConnectionToAD = false; - coco.LDAPPath = "LDAP://test"; - coco.LDAPUsername = "userTest"; - coco.AuthenticationSettings = AuthenticationTypes.Secure | AuthenticationTypes.Signing | AuthenticationTypes.Sealing; - Config.LDAPConnectionsProp.Add(coco); - Config.Update(); - - UnitTestsHelper.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); - } - - //[TestCase("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", @"contoso.local\group\chartest", true)] - //[TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", @"test)char@contoso.local", true)] - //[TestCase("http://yvand.com/customType1", @"group\chartest", true)] - [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", @"yvan", false)] - public virtual void DEBUG_ValidateClaim(string claimType, string claimValue, bool shouldValidate) - { - if (!TestValidation) return; - - SPClaim inputClaim = new SPClaim(claimType, claimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, shouldValidate, claimValue); - } - - [TestCase("FakeAccount", false)] - [TestCase("yvand@contoso.local", true)] - public virtual void DEBUG_AugmentEntity(string claimValue, bool shouldHavePermissions) - { - if (!TestAugmentation) return; - - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, claimValue, shouldHavePermissions); - } -#endif - } -} diff --git a/LDAPCP.Tests/ModifyConfigTests.cs b/LDAPCP.Tests/ModifyConfigTests.cs deleted file mode 100644 index e2a5429..0000000 --- a/LDAPCP.Tests/ModifyConfigTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using ldapcp; -using NUnit.Framework; -using System; -using System.Linq; - -namespace LDAPCP.Tests -{ - [TestFixture] - public class ModifyConfigTests : BackupCurrentConfig - { - const string ConfigUpdateErrorMessage = "Some changes made to list ClaimTypes are invalid and cannot be committed to configuration database. Inspect inner exception for more details about the error."; - - [Test] - public void AddClaimTypeConfig() - { - ClaimTypeConfig ctConfig = new ClaimTypeConfig(); - - // Add a ClaimTypeConfig with a claim type already set should throw exception InvalidOperationException - ctConfig.ClaimType = UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType; - ctConfig.LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute; - ctConfig.LDAPClass = UnitTestsHelper.RandomLDAPClass; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with a claim type already set should throw exception InvalidOperationException with this message: \"Claim type '{UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType}' already exists in the collection\""); - - // Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = false (default value) and LDAPAttribute / LDAPClass not set should throw exception InvalidOperationException - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.LDAPAttribute = String.Empty; - ctConfig.LDAPClass = String.Empty; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = false (default value) and LDAPAttribute / LDAPClass not set should throw exception InvalidOperationException with this message: \"Property LDAPAttribute and LDAPClass are required\""); - - // Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = true and ClaimType set should throw exception InvalidOperationException - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute; - ctConfig.LDAPClass = UnitTestsHelper.RandomLDAPClass; - ctConfig.UseMainClaimTypeOfDirectoryObject = true; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = true and ClaimType set should throw exception InvalidOperationException with this message: \"No claim type should be set if UseMainClaimTypeOfDirectoryObject is set to true\""); - - // Add a ClaimTypeConfig with EntityType 'Group' should succeed - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute; - ctConfig.LDAPClass = UnitTestsHelper.RandomLDAPClass; - ctConfig.EntityType = DirectoryObjectType.Group; - ctConfig.UseMainClaimTypeOfDirectoryObject = false; - Assert.DoesNotThrow(() => Config.ClaimTypes.Add(ctConfig), "Add a ClaimTypeConfig with EntityType 'Group' should succeed"); - Assert.IsTrue(Config.ClaimTypes.Remove(UnitTestsHelper.RandomClaimType)); - - // Add a valid ClaimTypeConfig should succeed - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute; - ctConfig.LDAPClass = UnitTestsHelper.RandomLDAPClass; - ctConfig.EntityType = DirectoryObjectType.User; - ctConfig.UseMainClaimTypeOfDirectoryObject = false; - Assert.DoesNotThrow(() => Config.ClaimTypes.Add(ctConfig), $"Add a valid ClaimTypeConfig should succeed"); - - // Add a ClaimTypeConfig twice should throw exception InvalidOperationException - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with a claim type already set should throw exception InvalidOperationException with this message: \"Claim type '{UnitTestsHelper.RandomClaimType}' already exists in the collection\""); - - // Delete the ClaimTypeConfig by calling method ClaimTypeConfigCollection.Remove(ClaimTypeConfig) should succeed - Assert.IsTrue(Config.ClaimTypes.Remove(ctConfig), $"Delete the ClaimTypeConfig by calling method ClaimTypeConfigCollection.Remove(ClaimTypeConfig) should succeed"); - } - - [Test] - public void ModifyOrDeleteIdentityClaimTypeConfig() - { - // Delete identity claim type from ClaimTypes list based on its claim type should throw exception InvalidOperationException - string identityClaimType = UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType; - Assert.Throws(() => Config.ClaimTypes.Remove(identityClaimType), $"Delete identity claim type from ClaimTypes list should throw exception InvalidOperationException with this message: \"Cannot delete claim type \"{UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType}\" because it is the identity claim type of \"{UnitTestsHelper.SPTrust.Name}\"\""); - - // Delete identity claim type from ClaimTypes list based on its ClaimTypeConfig should throw exception InvalidOperationException - ClaimTypeConfig identityCTConfig = Config.ClaimTypes.FirstOrDefault(x => String.Equals(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); - Assert.Throws(() => Config.ClaimTypes.Remove(identityClaimType), $"Delete identity claim type from ClaimTypes list should throw exception InvalidOperationException with this message: \"Cannot delete claim type \"{UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType}\" because it is the identity claim type of \"{UnitTestsHelper.SPTrust.Name}\"\""); - - // Modify identity ClaimTypeConfig to set its EntityType to Group should throw exception InvalidOperationException - identityCTConfig.EntityType = DirectoryObjectType.Group; - Assert.Throws(() => Config.Update(), $"Modify identity claim type to set its EntityType to Group should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); - } - - [Test] - public void DuplicateClaimType() - { - var firstCTConfig = Config.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType)); - - // Add a ClaimTypeConfig with property ClaimType already defined in another ClaimTypeConfig should throw exception InvalidOperationException - ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = firstCTConfig.ClaimType, LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute, LDAPClass = UnitTestsHelper.RandomLDAPClass }; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with property ClaimType already defined in another ClaimTypeConfig should throw exception InvalidOperationException with this message: \"Claim type '{firstCTConfig.ClaimType}' already exists in the collection\""); - - // Modify an existing claim type to set a claim type already defined should throw exception InvalidOperationException - var anotherCTConfig = Config.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType) && !String.Equals(firstCTConfig.ClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); - string backupClaimType = anotherCTConfig.ClaimType; - anotherCTConfig.ClaimType = firstCTConfig.ClaimType; - Assert.Throws(() => Config.Update(), $"Modify an existing claim type to set a claim type already defined should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); - anotherCTConfig.ClaimType = backupClaimType; // Revert the change to continue with a clean config - } - - [Test] - public void DuplicatePrefixToBypassLookup() - { - string prefixToBypassLookup = "test:"; - - // Set a duplicate PrefixToBypassLookup on 2 items already existing in the list should throw exception InvalidOperationException - Config.ClaimTypes.Where(x => !String.IsNullOrEmpty(x.ClaimType)).Take(2).Select(x => x.PrefixToBypassLookup = prefixToBypassLookup).ToList(); - Assert.Throws(() => Config.Update(), $"Set a duplicate PrefixToBypassLookup on 2 items already existing in the list should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); - - // Set a PrefixToBypassLookup on an existing item and add a new item with the same PrefixToBypassLookup should throw exception InvalidOperationException - var firstCTConfig = Config.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType)); - firstCTConfig.PrefixToBypassLookup = prefixToBypassLookup; - ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = UnitTestsHelper.RandomClaimType, PrefixToBypassLookup = prefixToBypassLookup, LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute, LDAPClass = UnitTestsHelper.RandomLDAPClass }; - Assert.Throws(() => Config.Update(), $"Set a duplicate PrefixToBypassLookup on an existing item and add a new item with the same PrefixToBypassLookup should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); - } - - [Test] - public void DuplicateEntityDataKey() - { - string entityDataKey = "test"; - - // Duplicate EntityDataKey on 2 items already existing in the list should throw exception InvalidOperationException - Config.ClaimTypes.Where(x => !String.IsNullOrEmpty(x.ClaimType)).Take(2).Select(x => x.EntityDataKey = entityDataKey).ToList(); - Assert.Throws(() => Config.Update(), $"Duplicate EntityDataKey on 2 items already existing in the list should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); - - // Remove one of the duplicated EntityDataKey - Config.ClaimTypes.FirstOrDefault(x => x.EntityDataKey == entityDataKey).EntityDataKey = String.Empty; - // Set an EntityDataKey on an existing item and add a new item with the same EntityDataKey should throw exception InvalidOperationException - ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = UnitTestsHelper.RandomClaimType, EntityDataKey = entityDataKey, LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute, LDAPClass = UnitTestsHelper.RandomLDAPClass }; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Set an EntityDataKey on an existing item and add a new item with the same EntityDataKey should throw exception InvalidOperationException with this message: \"Entity metadata '{entityDataKey}' already exists in the collection for the directory object User\""); - } - - [Test] - public void DuplicateLDAPAttributeAndClass() - { - ClaimTypeConfig existingCTConfig = Config.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType) && x.EntityType == DirectoryObjectType.User); - - // Create a new ClaimTypeConfig with a LDAPAttribute / LDAPClass already set should throw exception InvalidOperationException - ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = UnitTestsHelper.RandomClaimType, EntityType = DirectoryObjectType.User, LDAPAttribute = existingCTConfig.LDAPAttribute, LDAPClass = existingCTConfig.LDAPClass }; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Create a new ClaimTypeConfig with a LDAPAttribute / LDAPClass already set should throw exception InvalidOperationException with this message: \"An item with LDAP attribute '{existingCTConfig.LDAPAttribute}' and LDAP class '{existingCTConfig.LDAPClass}' already exists for the object type 'User'\""); - - // Add a valid ClaimTypeConfig should succeed (done for next test) - ctConfig.LDAPAttribute = UnitTestsHelper.RandomLDAPAttribute; - ctConfig.LDAPClass = UnitTestsHelper.RandomLDAPClass; - Assert.DoesNotThrow(() => Config.ClaimTypes.Add(ctConfig), $"Add a valid ClaimTypeConfig should succeed"); - - // Update an existing ClaimTypeConfig with a LDAPAttribute / LDAPClass already set should throw exception InvalidOperationException - ctConfig.LDAPAttribute = existingCTConfig.LDAPAttribute; - ctConfig.LDAPClass = existingCTConfig.LDAPClass; - Assert.Throws(() => Config.Update(), $"Update an existing ClaimTypeConfig with a LDAPAttribute / LDAPClass already set should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); - - // Delete the ClaimTypeConfig should succeed - Assert.IsTrue(Config.ClaimTypes.Remove(ctConfig), "Delete the ClaimTypeConfig should succeed"); - } - - [Test] - public void ModifyUserIdentifier() - { - ClaimTypeConfig backupIdentityCTConfig = Config.ClaimTypes.GetByClaimType(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType).CopyConfiguration(); - - Assert.Throws(() => Config.ClaimTypes.UpdateUserIdentifier(String.Empty, String.Empty), $"Update user identifier with either LDAPAttribute / LDAPClass null or empty should throw exception ArgumentNullException"); - - bool configUpdated = Config.ClaimTypes.UpdateUserIdentifier(UnitTestsHelper.RandomLDAPClass, UnitTestsHelper.RandomLDAPAttribute); - Assert.IsTrue(configUpdated, $"Update user identifier with any LDAPAttribute / LDAPClass should succeed and return true"); - - configUpdated = Config.ClaimTypes.UpdateUserIdentifier(backupIdentityCTConfig.LDAPClass, backupIdentityCTConfig.LDAPAttribute); - Assert.IsTrue(configUpdated, $"Update user identifier with any LDAPAttribute / LDAPClass should succeed and return true"); - - configUpdated = Config.ClaimTypes.UpdateUserIdentifier(backupIdentityCTConfig.LDAPClass, backupIdentityCTConfig.LDAPAttribute); - Assert.IsFalse(configUpdated, $"Update user identifier with the same LDAPAttribute / LDAPClass should not change anything and return false"); - } - } -} diff --git a/LDAPCP.Tests/Properties/AssemblyInfo.cs b/LDAPCP.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 34fa94a..0000000 --- a/LDAPCP.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("LDAPCP.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft Corporation")] -[assembly: AssemblyProduct("LDAPCP.Tests")] -[assembly: AssemblyCopyright("Copyright © Microsoft Corporation 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -[assembly: ComVisible(false)] - -[assembly: Guid("4fa40f51-787e-4287-9120-b5f4fe8c3579")] - -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LDAPCP.Tests/RequireExactMatchTests.cs b/LDAPCP.Tests/RequireExactMatchTests.cs deleted file mode 100644 index 9653142..0000000 --- a/LDAPCP.Tests/RequireExactMatchTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using NUnit.Framework; - -namespace LDAPCP.Tests -{ - [TestFixture] - public class RequireExactMatchOnBaseConfigTests : BackupCurrentConfig - { - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - - // Extra initialization for current test class - Config.FilterExactMatchOnlyProp = true; - Config.Update(); - } - - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData")] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public void RequireExactMatchDuringSearch(ValidateEntityData registrationData) - { - int expectedCount = registrationData.ShouldValidate ? 1 : 0; - UnitTestsHelper.TestSearchOperation(registrationData.ClaimValue, expectedCount, registrationData.ClaimValue); - } - } - - [TestFixture] - public class RequireExactMatchOnCustomConfigTests : CustomConfigTests - { - public override void InitializeConfiguration() - { - base.InitializeConfiguration(); - - // Extra initialization for current test class - Config.FilterExactMatchOnlyProp = true; - Config.Update(); - } - - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData")] - [Repeat(UnitTestsHelper.TestRepeatCount)] - public void RequireExactMatchDuringSearch(ValidateEntityData registrationData) - { - int expectedCount = registrationData.ShouldValidate ? 1 : 0; - UnitTestsHelper.TestSearchOperation(registrationData.ClaimValue, expectedCount, registrationData.ClaimValue); - } - } -} diff --git a/LDAPCP.Tests/UnitTestsHelper.cs b/LDAPCP.Tests/UnitTestsHelper.cs deleted file mode 100644 index e84cdc7..0000000 --- a/LDAPCP.Tests/UnitTestsHelper.cs +++ /dev/null @@ -1,344 +0,0 @@ -using DataAccess; -using ldapcp; -using Microsoft.SharePoint; -using Microsoft.SharePoint.Administration; -using Microsoft.SharePoint.Administration.Claims; -using Microsoft.SharePoint.WebControls; -using NUnit.Framework; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Security.Claims; -using System.Text; - -[SetUpFixture] -public class UnitTestsHelper -{ - public static string ClaimsProviderName => "LDAPCP"; - public static readonly ldapcp.LDAPCP ClaimsProvider = new ldapcp.LDAPCP(UnitTestsHelper.ClaimsProviderName); - public static readonly string ClaimsProviderConfigName = TestContext.Parameters["ClaimsProviderConfigName"]; - public static Uri TestSiteCollUri; - public static readonly string TestSiteRelativePath = $"/sites/{TestContext.Parameters["TestSiteCollectionName"]}"; - public const int MaxTime = 500000; - public static readonly string FarmAdmin = TestContext.Parameters["FarmAdmin"]; -#if DEBUG - public const int TestRepeatCount = 5; -#else - public const int TestRepeatCount = 20; -#endif - - public static string RandomClaimType => "http://schemas.yvand.com/ws/claims/random"; - public static string RandomClaimValue => "IDoNotExist"; - public static string RandomLDAPAttribute => "randomAttribute"; - public static string RandomLDAPClass => "randomClass"; - - public static readonly string TrustedGroupToAdd_ClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - public static readonly string TrustedGroupToAdd_ClaimValue = TestContext.Parameters["TrustedGroupToAdd_ClaimValue"]; - public static readonly SPClaim TrustedGroup = new SPClaim(TrustedGroupToAdd_ClaimType, TrustedGroupToAdd_ClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, SPTrust.Name)); - - public static readonly string CustomLDAPConnections = TestContext.Parameters["CustomLDAPConnections"]; - public static readonly string DataFile_AllAccounts_Search = TestContext.Parameters["DataFile_AllAccounts_Search"]; - public static readonly string DataFile_AllAccounts_Validate = TestContext.Parameters["DataFile_AllAccounts_Validate"]; - - public static SPTrustedLoginProvider SPTrust => SPSecurityTokenServiceManager.Local.TrustedLoginProviders.FirstOrDefault(x => String.Equals(x.ClaimProviderName, UnitTestsHelper.ClaimsProviderName, StringComparison.InvariantCultureIgnoreCase)); - - static TextWriterTraceListener logFileListener; - - [OneTimeSetUp] - public static void InitializeSiteCollection() - { - logFileListener = new TextWriterTraceListener(TestContext.Parameters["TestLogFileName"]); - Trace.Listeners.Add(logFileListener); - Trace.AutoFlush = true; - Trace.TraceInformation($"{DateTime.Now.ToString("s")} Start integration tests of {ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(ldapcp.LDAPCP)).Location).FileVersion}."); - Trace.WriteLine($"{DateTime.Now.ToString("s")} DataFile_AllAccounts_Search: {DataFile_AllAccounts_Search}"); - Trace.WriteLine($"{DateTime.Now.ToString("s")} DataFile_AllAccounts_Validate: {DataFile_AllAccounts_Validate}"); - Trace.WriteLine($"{DateTime.Now.ToString("s")} TestSiteCollectionName: {TestContext.Parameters["TestSiteCollectionName"]}"); - -#if DEBUG - TestSiteCollUri = new Uri("http://spsites/sites/" + TestContext.Parameters["TestSiteCollectionName"]); - //return; // Uncommented when debugging LDAPCP code from unit tests -#endif - - if (SPTrust == null) - { - Trace.TraceError($"{DateTime.Now.ToString("s")} SPTrust: is null"); - } - else - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} SPTrust: {SPTrust.Name}"); - } - - LDAPCPConfig config = LDAPCPConfig.GetConfiguration(UnitTestsHelper.ClaimsProviderConfigName, UnitTestsHelper.SPTrust.Name); - if (config == null) - { - LDAPCPConfig.CreateConfiguration(ClaimsProviderConstants.CONFIG_ID, ClaimsProviderConstants.CONFIG_NAME, SPTrust.Name); - } - - var service = SPFarm.Local.Services.GetValue(String.Empty); - SPWebApplication wa = service.WebApplications.FirstOrDefault(); - if (wa != null) - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} Web application {wa.Name} found."); - SPClaimProviderManager claimMgr = SPClaimProviderManager.Local; - string encodedClaim = claimMgr.EncodeClaim(TrustedGroup); - SPUserInfo userInfo = new SPUserInfo { LoginName = encodedClaim, Name = TrustedGroupToAdd_ClaimValue }; - - // The root site may not exist, but it must be present for tests to run - Uri rootWebAppUri = wa.GetResponseUri(0); - if (!SPSite.Exists(rootWebAppUri)) - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} Creating root site collection {rootWebAppUri.AbsoluteUri}..."); - SPSite spSite = wa.Sites.Add(rootWebAppUri.AbsoluteUri, "root", "root", 1033, "STS#1", FarmAdmin, String.Empty, String.Empty); - spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); - - SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; - membersGroup.AddUser(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes); - spSite.Dispose(); - } - - if (!Uri.TryCreate(rootWebAppUri, TestSiteRelativePath, out TestSiteCollUri)) - { - Trace.TraceError($"{DateTime.Now.ToString("s")} Unable to generate Uri of test site collection from Web application Uri {rootWebAppUri.AbsolutePath} and relative path {TestSiteRelativePath}."); - } - - if (!SPSite.Exists(TestSiteCollUri)) - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} Creating site collection {TestSiteCollUri.AbsoluteUri}..."); - SPSite spSite = wa.Sites.Add(TestSiteCollUri.AbsoluteUri, ClaimsProviderName, ClaimsProviderName, 1033, "STS#1", FarmAdmin, String.Empty, String.Empty); - spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); - - SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; - membersGroup.AddUser(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes); - spSite.Dispose(); - } - else - { - using (SPSite spSite = new SPSite(TestSiteCollUri.AbsoluteUri)) - { - SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; - membersGroup.AddUser(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes); - } - } - } - else - { - Trace.TraceError($"{DateTime.Now.ToString("s")} Web application was NOT found."); - } - } - - [OneTimeTearDown] - public static void Cleanup() - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} Integration tests of {ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(ldapcp.LDAPCP)).Location).FileVersion} finished."); - Trace.Flush(); - if (logFileListener != null) - { - logFileListener.Dispose(); - } - } - - public static void InitializeConfiguration(LDAPCPConfig config) - { - config.ResetCurrentConfiguration(); - -#if DEBUG - config.LDAPQueryTimeout = 99999; -#endif - - config.Update(); - } - - /// - /// Start search operation on a specific claims provider - /// - /// - /// How many entities are expected to be returned. Set to Int32.MaxValue if exact number is unknown but greater than 0 - /// - public static void TestSearchOperation(string inputValue, int expectedCount, string expectedClaimValue) - { - try - { - Stopwatch timer = new Stopwatch(); - timer.Start(); - string[] entityTypes = new string[] { "User", "SecGroup", "SharePointGroup", "System", "FormsRole" }; - - SPProviderHierarchyTree providerResults = ClaimsProvider.Search(TestSiteCollUri, entityTypes, inputValue, null, 30); - List entities = new List(); - foreach (var children in providerResults.Children) - { - entities.AddRange(children.EntityData); - } - VerifySearchTest(entities, inputValue, expectedCount, expectedClaimValue); - - entities = ClaimsProvider.Resolve(TestSiteCollUri, entityTypes, inputValue).ToList(); - VerifySearchTest(entities, inputValue, expectedCount, expectedClaimValue); - timer.Stop(); - Trace.WriteLine($"{DateTime.Now.ToString("s")} TestSearchOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: inputValue: '{inputValue}', expectedCount: '{expectedCount}', expectedClaimValue: '{expectedClaimValue}'."); - } - catch (Exception ex) - { - Trace.TraceError($"{DateTime.Now.ToString("s")} TestSearchOperation failed with exception '{ex.GetType()}', message '{ex.Message}'. Parameters: inputValue: '{inputValue}', expectedCount: '{expectedCount}', expectedClaimValue: '{expectedClaimValue}'."); - } - } - - public static void VerifySearchTest(List entities, string input, int expectedCount, string expectedClaimValue) - { - bool entityValueFound = false; - StringBuilder detailedLog = new StringBuilder($"It returned {entities.Count.ToString()} entities: "); - string entityLogPattern = "entity \"{0}\", claim type: \"{1}\"; "; - foreach (PickerEntity entity in entities) - { - detailedLog.AppendLine(String.Format(entityLogPattern, entity.Claim.Value, entity.Claim.ClaimType)); - if (String.Equals(expectedClaimValue, entity.Claim.Value, StringComparison.InvariantCultureIgnoreCase)) - { - entityValueFound = true; - } - } - - if (!entityValueFound && expectedCount > 0) - { - Assert.Fail($"Input \"{input}\" returned no entity with claim value \"{expectedClaimValue}\". {detailedLog.ToString()}"); - } - - if (expectedCount == Int32.MaxValue) - { - expectedCount = entities.Count; - } - - Assert.AreEqual(expectedCount, entities.Count, $"Input \"{input}\" should have returned {expectedCount} entities, but it returned {entities.Count} instead. {detailedLog.ToString()}"); - } - - public static void TestValidationOperation(SPClaim inputClaim, bool shouldValidate, string expectedClaimValue) - { - try - { - Stopwatch timer = new Stopwatch(); - timer.Start(); - var entityTypes = new[] { "User" }; - - PickerEntity[] entities = ClaimsProvider.Resolve(TestSiteCollUri, entityTypes, inputClaim); - - int expectedCount = shouldValidate ? 1 : 0; - Assert.AreEqual(expectedCount, entities.Length, $"Validation of entity \"{inputClaim.Value}\" should have returned {expectedCount} entity, but it returned {entities.Length} instead."); - if (shouldValidate) - { - StringAssert.AreEqualIgnoringCase(expectedClaimValue, entities[0].Claim.Value, $"Validation of entity \"{inputClaim.Value}\" should have returned value \"{expectedClaimValue}\", but it returned \"{entities[0].Claim.Value}\" instead."); - } - timer.Stop(); - Trace.WriteLine($"{DateTime.Now.ToString("s")} TestValidationOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: inputClaim.Value: '{inputClaim.Value}', shouldValidate: '{shouldValidate}', expectedClaimValue: '{expectedClaimValue}'."); - } - catch (Exception ex) - { - Trace.TraceError($"{DateTime.Now.ToString("s")} TestValidationOperation failed with exception '{ex.GetType()}', message '{ex.Message}'. Parameters: inputClaim.Value: '{inputClaim.Value}', shouldValidate: '{shouldValidate}', expectedClaimValue: '{expectedClaimValue}'."); - } - } - - public static void TestAugmentationOperation(string claimType, string claimValue, bool isMemberOfTrustedGroup) - { - try - { - Stopwatch timer = new Stopwatch(); - timer.Start(); - SPClaim inputClaim = new SPClaim(claimType, claimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - Uri context = new Uri(UnitTestsHelper.TestSiteCollUri.AbsoluteUri); - - SPClaim[] groups = ClaimsProvider.GetClaimsForEntity(context, inputClaim); - - bool groupFound = false; - if (groups != null && groups.Contains(TrustedGroup)) - { - groupFound = true; - } - - if (isMemberOfTrustedGroup) - { - Assert.IsTrue(groupFound, $"Entity \"{claimValue}\" should be member of group \"{TrustedGroupToAdd_ClaimValue}\", but this group was not found in the claims returned by the claims provider."); - } - else - { - Assert.IsFalse(groupFound, $"Entity \"{claimValue}\" should NOT be member of group \"{TrustedGroupToAdd_ClaimValue}\", but this group was found in the claims returned by the claims provider."); - } - timer.Stop(); - Trace.WriteLine($"{DateTime.Now.ToString("s")} TestAugmentationOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: claimType: '{claimType}', claimValue: '{claimValue}', isMemberOfTrustedGroup: '{isMemberOfTrustedGroup}'."); - } - catch (Exception ex) - { - Trace.TraceError($"{DateTime.Now.ToString("s")} TestAugmentationOperation failed with exception '{ex.GetType()}', message '{ex.Message}'. Parameters: claimType: '{claimType}', claimValue: '{claimValue}', isMemberOfTrustedGroup: '{isMemberOfTrustedGroup}'."); - } - } -} - -public class SearchEntityDataSourceCollection : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new[] { "yvand", "2", "yvand@contoso.local" }; - yield return new[] { "IDoNotExist", "0", "" }; - yield return new[] { "group1", "1", @"contoso.local\group1" }; - } -} - -public class SearchEntityDataSource -{ - public static IEnumerable GetTestData() - { - DataTable dt = DataTable.New.ReadCsv(UnitTestsHelper.DataFile_AllAccounts_Search); - foreach (Row row in dt.Rows) - { - var registrationData = new SearchEntityData(); - registrationData.Input = row["Input"]; - registrationData.ExpectedResultCount = Convert.ToInt32(row["ExpectedResultCount"]); - registrationData.ExpectedEntityClaimValue = row["ExpectedEntityClaimValue"]; - yield return new TestCaseData(new object[] { registrationData }); - } - } - - //public class ReadCSV - //{ - // public void GetValue() - // { - // TextReader tr1 = new StreamReader(@"c:\pathtofile\filename", true); - - // var Data = tr1.ReadToEnd().Split('\n') - // .Where(l => l.Length > 0) //nonempty strings - // .Skip(1) // skip header - // .Select(s => s.Trim()) // delete whitespace - // .Select(l => l.Split(',')) // get arrays of values - // .Select(l => new { Field1 = l[0], Field2 = l[1], Field3 = l[2] }); - // } - //} -} - -public class SearchEntityData -{ - public string Input; - public int ExpectedResultCount; - public string ExpectedEntityClaimValue; -} - -public class ValidateEntityDataSource -{ - public static IEnumerable GetTestData() - { - DataTable dt = DataTable.New.ReadCsv(UnitTestsHelper.DataFile_AllAccounts_Validate); - foreach (Row row in dt.Rows) - { - var registrationData = new ValidateEntityData(); - registrationData.ClaimValue = row["ClaimValue"]; - registrationData.ShouldValidate = Convert.ToBoolean(row["ShouldValidate"]); - registrationData.IsMemberOfTrustedGroup = Convert.ToBoolean(row["IsMemberOfTrustedGroup"]); - yield return new TestCaseData(new object[] { registrationData }); - } - } -} - -public class ValidateEntityData -{ - public string ClaimValue; - public bool ShouldValidate; - public bool IsMemberOfTrustedGroup; -} diff --git a/LDAPCP.Tests/local.runsettings b/LDAPCP.Tests/local.runsettings deleted file mode 100644 index c6f47d1..0000000 --- a/LDAPCP.Tests/local.runsettings +++ /dev/null @@ -1,16 +0,0 @@ - - - - x64 - - - - - - - - - - - - diff --git a/LDAPCP/ADMIN/LDAPCP/ClaimTypesConfig.aspx b/LDAPCP/ADMIN/LDAPCP/ClaimTypesConfig.aspx deleted file mode 100644 index 5880f9e..0000000 --- a/LDAPCP/ADMIN/LDAPCP/ClaimTypesConfig.aspx +++ /dev/null @@ -1,20 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" MasterPageFile="~/_admin/admin.master" %> -<%@ Register TagPrefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> -<%@ Register TagPrefix="Ldapcp" TagName="ClaimTypesConfigUC" src="ClaimTypesConfig.ascx" %> -<%@ Import Namespace="ldapcp" %> -<%@ Import Namespace="System.Diagnostics" %> -<%@ Import Namespace="System.Reflection" %> - - - - - Claim types configuration for LDAPCP - - - <%= String.Format("LDAPCP {0} - LDAPCP.com", FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LDAPCP)).Location).FileVersion, ClaimsProviderConstants.PUBLICSITEURL) %> - - - - -
-
diff --git a/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx b/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx deleted file mode 100644 index b842243..0000000 --- a/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx +++ /dev/null @@ -1,374 +0,0 @@ -<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="GlobalSettings.ascx.cs" Inherits="ldapcp.ControlTemplates.GlobalSettings" %> -<%@ Register TagPrefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> -<%@ Register TagPrefix="wssuc" TagName="InputFormSection" Src="~/_controltemplates/InputFormSection.ascx" %> -<%@ Register TagPrefix="wssuc" TagName="InputFormControl" Src="~/_controltemplates/InputFormControl.ascx" %> -<%@ Register TagPrefix="wssuc" TagName="ButtonSection" Src="~/_controltemplates/ButtonSection.ascx" %> -<%@ Register TagPrefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> - - - - - - -

- -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
    -
  1. - - -
  2. -
  3. - - -
  4. -
  5. - - -
  6. -
-
-
-
- -
- - -
-
-
-

- - -

- -
-
- - - -

- -
- -
- - - - - -
- - - - -
- - LDAP Server "": - - -
-
-
-
-
-
- -
-
-
- - - - - - - - - - -
- - - -
- - - -
-
-
- - - - - - - - - - - - -
- - -
-
- - - - - - - - -

- -
-
-<%-- - - - - - - - - - --%> - - - - - - - -
- -

-

-
-
- - - - - - - - - - - diff --git a/LDAPCP/ClaimTypeConfig.cs b/LDAPCP/ClaimTypeConfig.cs deleted file mode 100644 index 5a6a72e..0000000 --- a/LDAPCP/ClaimTypeConfig.cs +++ /dev/null @@ -1,743 +0,0 @@ -using Microsoft.SharePoint.Administration; -using Microsoft.SharePoint.Administration.Claims; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using WIF4_5 = System.Security.Claims; - -namespace ldapcp -{ - /// - /// Defines an attribute / claim type configuration - /// - public class ClaimTypeConfig : SPAutoSerializingObject, IEquatable - { - /// - /// Name of the attribute in LDAP - /// - public string LDAPAttribute - { - get { return _LDAPAttribute; } - set { _LDAPAttribute = value; } - } - [Persisted] - private string _LDAPAttribute; - - /// - /// Class of the attribute in LDAP, typically 'user' or 'group' - /// - public string LDAPClass - { - get { return _LDAPClass; } - set { _LDAPClass = value; } - } - [Persisted] - private string _LDAPClass; - - public DirectoryObjectType EntityType - { - get { return (DirectoryObjectType)Enum.ToObject(typeof(DirectoryObjectType), _DirectoryObjectType); } - set { _DirectoryObjectType = (int)value; } - } - [Persisted] - private int _DirectoryObjectType; - - public string ClaimType - { - get { return _ClaimType; } - set { _ClaimType = value; } - } - [Persisted] - private string _ClaimType; - - public bool SupportsWildcard - { - get { return _SupportsWildcard; } - set { _SupportsWildcard = value; } - } - [Persisted] - private bool _SupportsWildcard = true; - - /// - /// If set to true, property ClaimType should not be set - /// - public bool UseMainClaimTypeOfDirectoryObject - { - get { return _UseMainClaimTypeOfDirectoryObject; } - set { _UseMainClaimTypeOfDirectoryObject = value; } - } - [Persisted] - private bool _UseMainClaimTypeOfDirectoryObject = false; - - /// - /// When creating a PickerEntry, it's possible to populate entry with additional attributes stored in EntityData hash table - /// - public string EntityDataKey - { - get { return _EntityDataKey; } - set { _EntityDataKey = value; } - } - [Persisted] - private string _EntityDataKey; - - /// - /// Stores property SPTrustedClaimTypeInformation.DisplayName of current claim type. - /// - internal string ClaimTypeDisplayName - { - get { return _ClaimTypeMappingName; } - set { _ClaimTypeMappingName = value; } - } - [Persisted] - private string _ClaimTypeMappingName; - - /// - /// Every claim value type is a string by default - /// - public string ClaimValueType - { - get { return _ClaimValueType; } - set { _ClaimValueType = value; } - } - [Persisted] - private string _ClaimValueType = WIF4_5.ClaimValueTypes.String; - - /// - /// This prefix is added to the value of the permission created. This is useful to add a domain name before a group name (for example "domain\group" instead of "group") - /// - public string ClaimValuePrefix - { - get { return _ClaimValuePrefix; } - set { _ClaimValuePrefix = value; } - } - [Persisted] - private string _ClaimValuePrefix; - - /// - /// If set to true: permission created without LDAP lookup (possible if PrefixToBypassLookup is set and user typed this keyword in the input) should not contain the prefix (set in PrefixToAddToValueReturned) in the value - /// - public bool DoNotAddClaimValuePrefixIfBypassLookup - { - get { return _DoNotAddClaimValuePrefixIfBypassLookup; } - set { _DoNotAddClaimValuePrefixIfBypassLookup = value; } - } - [Persisted] - private bool _DoNotAddClaimValuePrefixIfBypassLookup; - - /// - /// Set this to tell LDAPCP to validate user input (and create the permission) without LDAP lookup if it contains this keyword at the beginning - /// - public string PrefixToBypassLookup - { - get { return _PrefixToBypassLookup; } - set { _PrefixToBypassLookup = value; } - } - [Persisted] - private string _PrefixToBypassLookup; - - /// - /// Set this property to customize display text of the permission with a specific LDAP attribute (different than LDAPAttributeName, that is the actual value of the permission) - /// - public string LDAPAttributeToShowAsDisplayText - { - get { return _LDAPAttributeToShowAsDisplayText; } - set { _LDAPAttributeToShowAsDisplayText = value; } - } - [Persisted] - private string _LDAPAttributeToShowAsDisplayText; - - /// - /// Set to only return values that exactly match the user input - /// - public bool FilterExactMatchOnly - { - get { return _FilterExactMatchOnly; } - set { _FilterExactMatchOnly = value; } - } - [Persisted] - private bool _FilterExactMatchOnly = false; - - /// - /// Set this property to set a specific LDAP filter - /// - public string AdditionalLDAPFilter - { - get { return _AdditionalLDAPFilter; } - set { _AdditionalLDAPFilter = value; } - } - [Persisted] - private string _AdditionalLDAPFilter; - - /// - /// Set to true to show the display name of claim type in parenthesis in display text of permission - /// - public bool ShowClaimNameInDisplayText - { - get { return _ShowClaimNameInDisplayText; } - set { _ShowClaimNameInDisplayText = value; } - } - [Persisted] - private bool _ShowClaimNameInDisplayText = true; - - public ClaimTypeConfig() - { - } - - /// - /// Returns a copy of the current object. This copy does not have any member of the base SharePoint base class set - /// - /// - public ClaimTypeConfig CopyConfiguration() - { - ClaimTypeConfig copy = new ClaimTypeConfig(); - // Copy non-inherited private fields - FieldInfo[] fieldsToCopy = this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (FieldInfo field in fieldsToCopy) - { - field.SetValue(copy, field.GetValue(this)); - } - return copy; - } - - /// - /// Apply configuration in parameter to current object. It does not copy SharePoint base class members - /// - /// - internal void ApplyConfiguration(ClaimTypeConfig configToApply) - { - // Copy non-inherited private fields - FieldInfo[] fieldsToCopy = this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (FieldInfo field in fieldsToCopy) - { - field.SetValue(this, field.GetValue(configToApply)); - } - } - - public bool Equals(ClaimTypeConfig other) - { - if (new ClaimTypeConfigSameConfig().Equals(this, other)) - { - return true; - } - else - { - return false; - } - } - } - - /// - /// Implements ICollection to add validation when collection is changed - /// - public class ClaimTypeConfigCollection : ICollection - { // Follows article https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.icollection-1?view=netframework-4.7.1 - - /// - /// Internal collection serialized in persisted object - /// - internal Collection innerCol = new Collection(); - - public int Count => innerCol.Count; - - public bool IsReadOnly => false; - - /// - /// If set, more checks can be done when collection is changed - /// - public SPTrustedLoginProvider SPTrust; - - public ClaimTypeConfigCollection() - { - } - - internal ClaimTypeConfigCollection(ref Collection innerCol) - { - this.innerCol = innerCol; - } - - public ClaimTypeConfig this[int index] - { - get { return (ClaimTypeConfig)innerCol[index]; } - set { innerCol[index] = value; } - } - - public void Add(ClaimTypeConfig item) - { - Add(item, true); - } - - internal void Add(ClaimTypeConfig item, bool strictChecks) - { - if (String.IsNullOrEmpty(item.LDAPAttribute) || String.IsNullOrEmpty(item.LDAPClass)) - { - throw new InvalidOperationException($"Properties LDAPAttribute and LDAPClass are required"); - } - - if (item.UseMainClaimTypeOfDirectoryObject && !String.IsNullOrEmpty(item.ClaimType)) - { - throw new InvalidOperationException($"No claim type should be set if UseMainClaimTypeOfDirectoryObject is set to true"); - } - - if (!item.UseMainClaimTypeOfDirectoryObject && String.IsNullOrEmpty(item.ClaimType) && String.IsNullOrEmpty(item.EntityDataKey)) - { - throw new InvalidOperationException($"EntityDataKey is required if ClaimType is not set and UseMainClaimTypeOfDirectoryObject is set to false"); - } - - if (Contains(item, new ClaimTypeConfigSamePermissionMetadata())) - { - throw new InvalidOperationException($"Entity metadata '{item.EntityDataKey}' already exists in the collection for the object type '{item.EntityType}'"); - } - - if (Contains(item, new ClaimTypeConfigSameClaimType())) - { - throw new InvalidOperationException($"Claim type '{item.ClaimType}' already exists in the collection"); - } - - if (Contains(item, new ClaimTypeConfigEnsureUniquePrefixToBypassLookup())) - { - throw new InvalidOperationException($"Prefix '{item.PrefixToBypassLookup}' is already set with another claim type and must be unique"); - } - - if (Contains(item, new ClaimTypeConfigSameDirectoryConfiguration())) - { - throw new InvalidOperationException($"An item with LDAP attribute '{item.LDAPAttribute}' and LDAP class '{item.LDAPClass}' already exists for the object type '{item.EntityType}'"); - } - - if (Contains(item)) - { - if (String.IsNullOrEmpty(item.ClaimType)) - throw new InvalidOperationException($"This configuration with LDAP attribute '{item.LDAPAttribute}' and class '{item.LDAPClass}' already exists in the collection"); - else - throw new InvalidOperationException($"This configuration with claim type '{item.ClaimType}' already exists in the collection"); - } - - if (ClaimsProviderConstants.EnforceOnly1ClaimTypeForGroup && item.EntityType == DirectoryObjectType.Group) - { - if (Contains(item, new ClaimTypeConfigEnforeOnly1ClaimTypePerObjectType())) - { - throw new InvalidOperationException($"A claim type for EntityType '{DirectoryObjectType.Group.ToString()}' already exists in the collection"); - } - } - - if (strictChecks) - { - // If current item has UseMainClaimTypeOfDirectoryObject = true: check if another item with same EntityType AND a claim type defined - // Another valid item may be added later, and even if not, code should handle that scenario - if (item.UseMainClaimTypeOfDirectoryObject && innerCol.FirstOrDefault(x => x.EntityType == item.EntityType && !String.IsNullOrEmpty(x.ClaimType)) == null) - { - throw new InvalidOperationException($"Cannot add this item (with UseMainClaimTypeOfDirectoryObject set to true) because collecton does not contain an item with same EntityType '{item.EntityType.ToString()}' AND a ClaimType set"); - } - } - - // If SPTrustedLoginProvider is set, additional checks can be done - if (SPTrust != null) - { - // If current claim type is identity claim type: EntityType must be User - if (String.Equals(item.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) - { - if (item.EntityType != DirectoryObjectType.User) - { - throw new InvalidOperationException($"Identity claim type must be configured with EntityType 'User'"); - } - } - } - - innerCol.Add(item); - } - - /// - /// Only ClaimTypeConfig with property ClaimType already set can be updated - /// - /// Claim type of ClaimTypeConfig object to update - /// New version of ClaimTypeConfig object - public void Update(string oldClaimType, ClaimTypeConfig newItem) - { - if (String.IsNullOrEmpty(oldClaimType)) throw new ArgumentNullException("oldClaimType"); - if (newItem == null) throw new ArgumentNullException("newItem"); - - // If SPTrustedLoginProvider is set, additional checks can be done - if (SPTrust != null) - { - // Specific checks if current claim type is identity claim type - if (String.Equals(oldClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) - { - // We don't allow to change claim type - if (!String.Equals(newItem.ClaimType, oldClaimType, StringComparison.InvariantCultureIgnoreCase)) - { - throw new InvalidOperationException($"Claim type cannot be changed because current item is the configuration of the identity claim type"); - } - - // EntityType must be User - if (newItem.EntityType != DirectoryObjectType.User) - { - throw new InvalidOperationException($"Identity claim type must be configured with EntityType 'User'"); - } - } - } - - // Create a temp collection that is a copy of current collection - ClaimTypeConfigCollection testUpdateCollection = new ClaimTypeConfigCollection(); - foreach (ClaimTypeConfig curCTConfig in innerCol) - { - testUpdateCollection.Add(curCTConfig.CopyConfiguration(), false); - } - - // Update ClaimTypeConfig in testUpdateCollection - ClaimTypeConfig ctConfigToUpdate = testUpdateCollection.First(x => String.Equals(x.ClaimType, oldClaimType, StringComparison.InvariantCultureIgnoreCase)); - ctConfigToUpdate.ApplyConfiguration(newItem); - - // Test change in testUpdateCollection by adding all items in a new temp collection - ClaimTypeConfigCollection testNewItemCollection = new ClaimTypeConfigCollection(); - foreach (ClaimTypeConfig curCTConfig in testUpdateCollection) - { - // ClaimTypeConfigCollection.Add() may thrown an exception if newItem is not valid for any reason - testNewItemCollection.Add(curCTConfig, false); - } - - // No error, current collection can safely be updated - innerCol.First(x => String.Equals(x.ClaimType, oldClaimType, StringComparison.InvariantCultureIgnoreCase)).ApplyConfiguration(newItem); - } - - /// - /// Update the LDAPClass and LDAPAttribute of the identity ClaimTypeConfig. If new values duplicate an existing item, it will be removed from the collection - /// - /// new LDAPClass - /// new newLDAPAttribute - /// True if the identity ClaimTypeConfig was successfully updated - public bool UpdateUserIdentifier(string newLDAPClass, string newLDAPAttribute) - { - if (String.IsNullOrEmpty(newLDAPClass)) throw new ArgumentNullException("newLDAPClass"); - if (String.IsNullOrEmpty(newLDAPAttribute)) throw new ArgumentNullException("newLDAPAttribute"); - - bool identifierUpdated = false; - if (SPTrust == null) - return identifierUpdated; - - ClaimTypeConfig identityClaimType = innerCol.FirstOrDefault(x => String.Equals(x.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)); - if (identityClaimType == null) - return identifierUpdated; - - if (String.Equals(identityClaimType.LDAPClass, newLDAPClass, StringComparison.InvariantCultureIgnoreCase) && - String.Equals(identityClaimType.LDAPAttribute, newLDAPAttribute, StringComparison.InvariantCultureIgnoreCase)) - return identifierUpdated; - - // Check if the new LDAPAttribute / LDAPClass duplicates an existing item, and delete it if so - for (int i = 0; i < innerCol.Count; i++) - { - ClaimTypeConfig curCT = (ClaimTypeConfig)innerCol[i]; - if (curCT.EntityType == DirectoryObjectType.User && - String.Equals(curCT.LDAPAttribute, newLDAPAttribute, StringComparison.InvariantCultureIgnoreCase) && - String.Equals(curCT.LDAPClass, newLDAPClass, StringComparison.InvariantCultureIgnoreCase)) - { - innerCol.RemoveAt(i); - break; // There can be only 1 potential duplicate - } - } - - identityClaimType.LDAPClass = newLDAPClass; - identityClaimType.LDAPAttribute = newLDAPAttribute; - identifierUpdated = true; - return identifierUpdated; - } - - public void Clear() - { - innerCol.Clear(); - } - - /// - /// Test equality based on ClaimTypeConfigSameConfig (default implementation of IEquitable in ClaimTypeConfig) - /// - /// - /// - public bool Contains(ClaimTypeConfig item) - { - bool found = false; - foreach (ClaimTypeConfig ct in innerCol) - { - if (ct.Equals(item)) - { - found = true; - } - } - return found; - } - - public bool Contains(ClaimTypeConfig item, EqualityComparer comp) - { - bool found = false; - foreach (ClaimTypeConfig ct in innerCol) - { - if (comp.Equals(ct, item)) - { - found = true; - } - } - return found; - } - - public void CopyTo(ClaimTypeConfig[] array, int arrayIndex) - { - if (array == null) - throw new ArgumentNullException("The array cannot be null."); - if (arrayIndex < 0) - throw new ArgumentOutOfRangeException("The starting array index cannot be negative."); - if (Count > array.Length - arrayIndex + 1) - throw new ArgumentException("The destination array has fewer elements than the collection."); - - for (int i = 0; i < innerCol.Count; i++) - { - array[i + arrayIndex] = innerCol[i]; - } - } - - public bool Remove(ClaimTypeConfig item) - { - if (SPTrust != null && String.Equals(item.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) throw new InvalidOperationException($"Cannot delete claim type \"{item.ClaimType}\" because it is the identity claim type of \"{SPTrust.Name}\""); - - bool result = false; - for (int i = 0; i < innerCol.Count; i++) - { - ClaimTypeConfig curCT = (ClaimTypeConfig)innerCol[i]; - if (new ClaimTypeConfigSameConfig().Equals(curCT, item)) - { - innerCol.RemoveAt(i); - result = true; - break; - } - } - return result; - } - - public bool Remove(string claimType) - { - if (String.IsNullOrEmpty(claimType)) throw new ArgumentNullException("claimType"); - if (SPTrust != null && String.Equals(claimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) throw new InvalidOperationException($"Cannot delete claim type \"{claimType}\" because it is the identity claim type of \"{SPTrust.Name}\""); - - bool result = false; - for (int i = 0; i < innerCol.Count; i++) - { - ClaimTypeConfig curCT = (ClaimTypeConfig)innerCol[i]; - if (String.Equals(claimType, curCT.ClaimType, StringComparison.InvariantCultureIgnoreCase)) - { - innerCol.RemoveAt(i); - result = true; - break; - } - } - return result; - } - - public IEnumerator GetEnumerator() - { - return new ClaimTypeConfigEnumerator(this); - } - IEnumerator IEnumerable.GetEnumerator() - { - return new ClaimTypeConfigEnumerator(this); - } - - public ClaimTypeConfig GetByClaimType(string claimType) - { - if (String.IsNullOrEmpty(claimType)) throw new ArgumentNullException("claimType"); - ClaimTypeConfig ctConfig = innerCol.FirstOrDefault(x => String.Equals(claimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); - return ctConfig; - } - } - - public class ClaimTypeConfigEnumerator : IEnumerator - { - private ClaimTypeConfigCollection _collection; - private int curIndex; - private ClaimTypeConfig curBox; - - - public ClaimTypeConfigEnumerator(ClaimTypeConfigCollection collection) - { - _collection = collection; - curIndex = -1; - curBox = default(ClaimTypeConfig); - - } - - public bool MoveNext() - { - //Avoids going beyond the end of the collection. - if (++curIndex >= _collection.Count) - { - return false; - } - else - { - // Set current box to next item in collection. - curBox = _collection[curIndex]; - } - return true; - } - - public void Reset() { curIndex = -1; } - - void IDisposable.Dispose() { } - - public ClaimTypeConfig Current - { - get { return curBox; } - } - - - object IEnumerator.Current - { - get { return Current; } - } - - } - - public class ClaimTypeConfigSameConfig : EqualityComparer - { - public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) - { - if (String.Equals(existingCTConfig.ClaimType, newCTConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase) && - String.Equals(existingCTConfig.LDAPAttribute, newCTConfig.LDAPAttribute, StringComparison.InvariantCultureIgnoreCase) && - String.Equals(existingCTConfig.LDAPClass, newCTConfig.LDAPClass, StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } - else - { - return false; - } - } - - public override int GetHashCode(ClaimTypeConfig ct) - { - string hCode = ct.ClaimType + ct.LDAPAttribute + ct.LDAPClass; - return hCode.GetHashCode(); - } - } - - public class ClaimTypeConfigSameClaimType : EqualityComparer - { - public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) - { - if (String.Equals(existingCTConfig.ClaimType, newCTConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase) && - !String.IsNullOrEmpty(newCTConfig.ClaimType)) - { - return true; - } - else - { - return false; - } - } - - public override int GetHashCode(ClaimTypeConfig ct) - { - string hCode = ct.ClaimType + ct.EntityType + ct.LDAPAttribute + ct.LDAPClass; - return hCode.GetHashCode(); - } - } - - public class ClaimTypeConfigSamePermissionMetadata : EqualityComparer - { - public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) - { - if (!String.IsNullOrEmpty(newCTConfig.EntityDataKey) && - String.Equals(existingCTConfig.EntityDataKey, newCTConfig.EntityDataKey, StringComparison.InvariantCultureIgnoreCase) && - existingCTConfig.EntityType == newCTConfig.EntityType) - { - return true; - } - else - { - return false; - } - } - - public override int GetHashCode(ClaimTypeConfig ct) - { - string hCode = ct.ClaimType + ct.EntityType; - return hCode.GetHashCode(); - } - } - - /// - /// Ensure that there is no duplicate of "PrefixToBypassLookup" property - /// - internal class ClaimTypeConfigEnsureUniquePrefixToBypassLookup : EqualityComparer - { - public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) - { - if (!String.IsNullOrEmpty(newCTConfig.PrefixToBypassLookup) && - String.Equals(newCTConfig.PrefixToBypassLookup, existingCTConfig.PrefixToBypassLookup, StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } - else - { - return false; - } - } - - public override int GetHashCode(ClaimTypeConfig ct) - { - string hCode = ct.PrefixToBypassLookup; - return hCode.GetHashCode(); - } - } - - /// - /// Should be used only to ensure that only 1 claim type is set per EntityType - /// - internal class ClaimTypeConfigEnforeOnly1ClaimTypePerObjectType : EqualityComparer - { - public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) - { - if ((!String.IsNullOrEmpty(newCTConfig.ClaimType) && !String.IsNullOrEmpty(existingCTConfig.ClaimType)) && - existingCTConfig.EntityType == newCTConfig.EntityType && - existingCTConfig.UseMainClaimTypeOfDirectoryObject == newCTConfig.UseMainClaimTypeOfDirectoryObject && - newCTConfig.UseMainClaimTypeOfDirectoryObject == false) - { - return true; - } - else - { - return false; - } - } - - public override int GetHashCode(ClaimTypeConfig ct) - { - string hCode = ct.ClaimType + ct.EntityType + ct.UseMainClaimTypeOfDirectoryObject.ToString(); - return hCode.GetHashCode(); - } - } - - /// - /// Check if a given object type (user or group) has 2 ClaimTypeConfig with the same LDAPAttribute and LDAPClass - /// - public class ClaimTypeConfigSameDirectoryConfiguration : EqualityComparer - { - public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) - { - if (String.Equals(existingCTConfig.LDAPAttribute, newCTConfig.LDAPAttribute, StringComparison.InvariantCultureIgnoreCase) && - String.Equals(existingCTConfig.LDAPClass, newCTConfig.LDAPClass, StringComparison.InvariantCultureIgnoreCase) && - existingCTConfig.EntityType == newCTConfig.EntityType) - { - return true; - } - else - { - return false; - } - } - - public override int GetHashCode(ClaimTypeConfig ct) - { - string hCode = ct.LDAPAttribute + ct.LDAPClass + ct.EntityType; - return hCode.GetHashCode(); - } - } -} diff --git a/LDAPCP/Features/LDAPCP.Administration/LDAPCP.Administration.feature b/LDAPCP/Features/LDAPCP.Administration/LDAPCP.Administration.feature deleted file mode 100644 index 4ad43a9..0000000 --- a/LDAPCP/Features/LDAPCP.Administration/LDAPCP.Administration.feature +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/LDAPCP/Features/LDAPCP/LDAPCP.feature b/LDAPCP/Features/LDAPCP/LDAPCP.feature deleted file mode 100644 index b38e724..0000000 --- a/LDAPCP/Features/LDAPCP/LDAPCP.feature +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/LDAPCP/LDAPCP.cs b/LDAPCP/LDAPCP.cs deleted file mode 100644 index 0bf8f6e..0000000 --- a/LDAPCP/LDAPCP.cs +++ /dev/null @@ -1,2125 +0,0 @@ -using Microsoft.SharePoint; -using Microsoft.SharePoint.Administration; -using Microsoft.SharePoint.Administration.Claims; -using Microsoft.SharePoint.Utilities; -using Microsoft.SharePoint.WebControls; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.DirectoryServices; -using System.DirectoryServices.AccountManagement; -using System.DirectoryServices.ActiveDirectory; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using static ldapcp.ClaimsProviderLogging; -using WIF4_5 = System.Security.Claims; - -/* - * DO NOT directly edit LDAPCP class. It is designed to be inherited to customize it as desired. - * Please download "LDAPCP for Developers.zip" on https://github.com/Yvand/LDAPCP to find examples and guidance. - * */ - -namespace ldapcp -{ - /// - /// Query Active Directory and LDAP servers to enhance people picker with a great search experience in federated authentication - /// Please visit https://github.com/Yvand/LDAPCP for updates and to report bugs. - /// Author: Yvan Duhamel - yvandev@outlook.fr - /// - public class LDAPCP : SPClaimProvider - { - public static string _ProviderInternalName => "LDAPCP"; - public virtual string ProviderInternalName => "LDAPCP"; - - public virtual string PersistedObjectName => ClaimsProviderConstants.CONFIG_NAME; - - /// - /// Contains configuration currently in use by claims provider - /// - public ILDAPCPConfiguration CurrentConfiguration; - - private object Sync_Init = new object(); - private ReaderWriterLockSlim Lock_Config = new ReaderWriterLockSlim(); - private long CurrentConfigurationVersion = 0; - - /// - /// Contains the attribute mapped to the identity claim in the SPTrustedLoginProvider - /// - protected ClaimTypeConfig IdentityClaimTypeConfig; - - /// - /// Group ClaimTypeConfig used to set the claim type for other group ClaimTypeConfig that have UseMainClaimTypeOfDirectoryObject set to true - /// - ClaimTypeConfig MainGroupClaimTypeConfig; - - /// - /// Contains attributes that are not used in the filter (both ClaimTypeProp AND UseMainClaimTypeOfDirectoryObject are not set), but have EntityDataKey set - /// - protected IEnumerable MetadataConfig; - - /// - /// SPTrust associated with the claims provider - /// - protected SPTrustedLoginProvider SPTrust; - - /// - /// List of attributes actually defined in the trust - /// + list of LDAP attributes that are always queried, even if not defined in the trust (typically the displayName) - /// - private List ProcessedClaimTypesList; - - protected virtual string LDAPObjectClassName => "objectclass"; - protected virtual string LDAPFilter => "(&(" + LDAPObjectClassName + "={2})({0}={1}){3}) "; - protected virtual string EntityDisplayText => "({0}) {1}"; - protected virtual string EntityOnMouseOver => "{0}={1}"; - protected virtual string LDAPFilterEnabledUsersOnly => "(&(!(userAccountControl:1.2.840.113556.1.4.803:=2))"; - protected virtual string LDAPFilterADSecurityGroupsOnly => "(groupType:1.2.840.113556.1.4.803:=2147483648)"; - - /// - /// Returned issuer formatted like the property SPClaim.OriginalIssuer: "TrustedProvider:TrustedProviderName" - /// - protected string IssuerName => SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, SPTrust.Name); - - public LDAPCP(string displayName) : base(displayName) { } - - /// - /// Initializes claim provider. This method is reserved for internal use and is not intended to be called from external code or changed - /// - protected bool Initialize(Uri context, string[] entityTypes) - { - // Ensures thread safety to initialize class variables - lock (Sync_Init) - { - // 1ST PART: GET CONFIGURATION - ILDAPCPConfiguration globalConfiguration = null; - bool refreshConfig = false; - bool success = true; - try - { - if (SPTrust == null) - { - SPTrust = GetSPTrustAssociatedWithCP(ProviderInternalName); - if (SPTrust == null) { return false; } - } - if (!CheckIfShouldProcessInput(context)) { return false; } - - globalConfiguration = GetConfiguration(context, entityTypes, PersistedObjectName, SPTrust.Name); - if (globalConfiguration == null) - { - globalConfiguration = LDAPCPConfig.ReturnDefaultConfiguration(SPTrust.Name); - // There is no thread safety issue in reading this.CurrentConfiguration here, thanks to the lock Sync_Init - if (this.CurrentConfiguration == null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' was not found in configuration database, switch to default. Visit LDAPCP admin pages in central administration to create it.", - TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - // Run with default configuration, which creates a connection to connect to current AD domain - refreshConfig = true; - } - else - { - // Default configuration has only 1 connection to AD domain of current SP server. - // If its property LDAPConnection.RootContainer is null, it means that LDAPConnection properties were not set because it was previously done in a thread that did not run as application pool account (https://github.com/Yvand/LDAPCP/issues/87) - // If so, recreate default configuration - if (String.IsNullOrEmpty(this.CurrentConfiguration.LDAPConnectionsProp.First().RootContainer)) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Default configuration was not fully initialized, recreating it...", - TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - refreshConfig = true; - } - } - } - else - { - ((LDAPCPConfig)globalConfiguration).CheckAndCleanConfiguration(SPTrust.Name); - } - - if (globalConfiguration.ClaimTypes == null || globalConfiguration.ClaimTypes.Count == 0) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' was found but collection ClaimTypes is null or empty. Visit LDAPCP admin pages in central administration to create it.", - TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - // Cannot continue - success = false; - } - - if (globalConfiguration.LDAPConnectionsProp == null || globalConfiguration.LDAPConnectionsProp.Count == 0) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' was found but there is no LDAP connection registered. Visit LDAPCP admin pages in central administration to register one.", - TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - // Cannot continue - success = false; - } - - if (success) - { - if (this.CurrentConfigurationVersion == ((SPPersistedObject)globalConfiguration).Version) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' version {((SPPersistedObject)globalConfiguration).Version.ToString()} was found", - TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Core); - - // Local configuration may need to be recreated due to bug https://github.com/Yvand/LDAPCP/issues/87: - // If a LDAPConnection with UseSPServerConnectionToAD true has its property LDAPConnection.RootContainer null, it means that LDAPConnection properties failed to be set, because thread did not run as application pool account - if (this.CurrentConfiguration != null && - this.CurrentConfiguration.LDAPConnectionsProp.FirstOrDefault(x => x.UseSPServerConnectionToAD && String.IsNullOrEmpty(x.RootContainer)) != null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' version {((SPPersistedObject)globalConfiguration).Version.ToString()} was found, but local copy is not fully initialized. Refreshing local copy...", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); - refreshConfig = true; - } - } - else - { - refreshConfig = true; - this.CurrentConfigurationVersion = ((SPPersistedObject)globalConfiguration).Version; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' changed to version {((SPPersistedObject)globalConfiguration).Version.ToString()}, refreshing local copy...", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); - } - } - - // ProcessedClaimTypesList can be null if: - // - 1st initialization - // - Initialized before but it failed. If so, try again to refresh config - if (this.ProcessedClaimTypesList == null) { refreshConfig = true; } - } - catch (Exception ex) - { - success = false; - ClaimsProviderLogging.LogException(ProviderInternalName, "in Initialize", TraceCategory.Core, ex); - } - - //refreshConfig = true; // DEBUG - if (!success || !refreshConfig) - { - return success; - } - - // 2ND PART: APPLY CONFIGURATION - // Configuration needs to be refreshed, lock current thread in write mode - Lock_Config.EnterWriteLock(); - try - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Refreshing local copy of configuration '{PersistedObjectName}'", - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Core); - - // Create local version of the persisted object, that will never be saved in config DB - // This copy is unique to current object instance to avoid thread safety issues - this.CurrentConfiguration = ((LDAPCPConfig)globalConfiguration).CopyConfiguration(); - -#pragma warning disable CS0618 // Type or member is obsolete - SetCustomConfiguration(context, entityTypes); -#pragma warning restore CS0618 // Type or member is obsolete - if (this.CurrentConfiguration.ClaimTypes == null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] List if claim types was set to null in method SetCustomConfiguration for configuration '{PersistedObjectName}'.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - return false; - } - - foreach (LDAPConnection connection in this.CurrentConfiguration.LDAPConnectionsProp) - { - SetLDAPConnection(context, connection); - } - - success = InitializeClaimTypeConfigList(this.CurrentConfiguration.ClaimTypes); - } - catch (Exception ex) - { - success = false; - ClaimsProviderLogging.LogException(ProviderInternalName, "in Initialize, while refreshing configuration", TraceCategory.Core, ex); - } - finally - { - Lock_Config.ExitWriteLock(); - } - return success; - } - } - - /// - /// Initializes claim provider. This method is reserved for internal use and is not intended to be called from external code or changed - /// - private bool InitializeClaimTypeConfigList(ClaimTypeConfigCollection nonProcessedClaimTypes) - { - bool success = true; - try - { - bool identityClaimTypeFound = false; - bool groupClaimTypeFound = false; - List claimTypesSetInTrust = new List(); - // Foreach MappedClaimType in the SPTrustedLoginProvider - foreach (SPTrustedClaimTypeInformation claimTypeInformation in SPTrust.ClaimTypeInformation) - { - // Search if current claim type in trust exists in ClaimTypeConfigCollection - ClaimTypeConfig claimTypeConfig = nonProcessedClaimTypes.FirstOrDefault(x => - !String.IsNullOrWhiteSpace(x.ClaimType) && - SPClaimTypes.Equals(x.ClaimType, claimTypeInformation.MappedClaimType) && - !x.UseMainClaimTypeOfDirectoryObject && - !String.IsNullOrEmpty(x.LDAPAttribute) && - !String.IsNullOrEmpty(x.LDAPClass)); - - if (claimTypeConfig == null) { continue; } - claimTypeConfig.ClaimTypeDisplayName = claimTypeInformation.DisplayName; - claimTypesSetInTrust.Add(claimTypeConfig); - if (SPClaimTypes.Equals(SPTrust.IdentityClaimTypeInformation.MappedClaimType, claimTypeConfig.ClaimType)) - { - // Identity claim type found, set IdentityClaimTypeConfig property - identityClaimTypeFound = true; - IdentityClaimTypeConfig = claimTypeConfig; - } - else if (!groupClaimTypeFound && claimTypeConfig.EntityType == DirectoryObjectType.Group) - { - if (!String.IsNullOrEmpty(this.CurrentConfiguration.MainGroupClaimType)) - { - // If MainGroupClaimType is set, try to set MainGroupClaimTypeConfig with the ClaimTypeConfig that has the same ClaimType - if (SPClaimTypes.Equals(claimTypeConfig.ClaimType, this.CurrentConfiguration.MainGroupClaimType)) - { - MainGroupClaimTypeConfig = claimTypeConfig; - groupClaimTypeFound = true; - } - } - else - { - // Otherwise, use arbitrarily the first valid group ClaimTypeConfig found - // EDIT: setting this arbitrarily may not be a good option as LDAPCP may not behave as the actual configuration - //MainGroupClaimTypeConfig = claimTypeConfig; - //this.CurrentConfiguration.MainGroupClaimType = claimTypeConfig.ClaimType; - //groupClaimTypeFound = true; - } - } - } - - if (!identityClaimTypeFound) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Cannot continue because identity claim type '{SPTrust.IdentityClaimTypeInformation.MappedClaimType}' set in the SPTrustedIdentityTokenIssuer '{SPTrust.Name}' is missing in the ClaimTypeConfig list.", TraceSeverity.Unexpected, EventSeverity.ErrorCritical, TraceCategory.Core); - return false; - } - - // Check if there are additional properties to use in queries (UseMainClaimTypeOfDirectoryObject set to true) - List additionalClaimTypeConfigList = new List(); - foreach (ClaimTypeConfig claimTypeConfig in nonProcessedClaimTypes.Where(x => x.UseMainClaimTypeOfDirectoryObject)) - { - if (claimTypeConfig.EntityType == DirectoryObjectType.User) - { - claimTypeConfig.ClaimType = IdentityClaimTypeConfig.ClaimType; - claimTypeConfig.LDAPAttributeToShowAsDisplayText = IdentityClaimTypeConfig.LDAPAttributeToShowAsDisplayText; - } - else - { - // If not a user, it must be a group - if (MainGroupClaimTypeConfig == null) { continue; } - claimTypeConfig.ClaimType = MainGroupClaimTypeConfig.ClaimType; - claimTypeConfig.LDAPAttributeToShowAsDisplayText = MainGroupClaimTypeConfig.LDAPAttributeToShowAsDisplayText; - } - additionalClaimTypeConfigList.Add(claimTypeConfig); - } - - this.ProcessedClaimTypesList = new List(claimTypesSetInTrust.Count + additionalClaimTypeConfigList.Count); - this.ProcessedClaimTypesList.AddRange(claimTypesSetInTrust); - this.ProcessedClaimTypesList.AddRange(additionalClaimTypeConfigList); - - // Any metadata for a user with at least an LDAP attribute and a LDAP class is valid - this.MetadataConfig = nonProcessedClaimTypes.Where(x => - !String.IsNullOrEmpty(x.EntityDataKey) && - !String.IsNullOrEmpty(x.LDAPAttribute) && - !String.IsNullOrEmpty(x.LDAPClass)); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in InitializeClaimTypeConfigList", TraceCategory.Core, ex); - success = false; - } - return success; - } - - /// - /// Override this method to create read-only configuration that will not be persisted in configuration database, and that cannot be changed with admin pages or PowerShell. - /// DO NOT Override this method if you want to store configuration in config DB, and be able to updated it with admin pages or PowerShel. - /// For that, override property PersistedObjectName and set its name - /// - /// - /// - /// - /// Read-only configuration to use - protected virtual ILDAPCPConfiguration GetConfiguration(Uri context, string[] entityTypes, string persistedObjectName, string spTrustName) - { - return LDAPCPConfig.GetConfiguration(persistedObjectName, spTrustName); - } - - /// - /// [Deprecated] Override this method to customize the configuration of LDAPCP. Please override GetConfiguration instead. - /// - /// The context, as a URI - /// The EntityType entity types set to scope the search to - [Obsolete("SetCustomConfiguration is deprecated, please override GetConfiguration instead.")] - protected virtual void SetCustomConfiguration(Uri context, string[] entityTypes) - { - } - - /// - /// Check if LDAPCP should process input (and show results) based on current URL (context) - /// - /// - /// - protected virtual bool CheckIfShouldProcessInput(Uri context) - { - if (context == null) { return true; } - var webApp = SPWebApplication.Lookup(context); - if (webApp == null) { return false; } - if (webApp.IsAdministrationWebApplication) { return true; } - - // Not central admin web app, enable LDAPCP only if current web app uses it - // It is not possible to exclude zones where LDAPCP is not used because: - // Consider following scenario: default zone is NTLM, intranet zone is claims - // In intranet zone, when creating entity, LDAPCP will be called 2 times, but the 2nd time (from FillResolve (SPClaim)) the context will always be the URL of default zone - foreach (var zone in Enum.GetValues(typeof(SPUrlZone))) - { - SPIisSettings iisSettings = webApp.GetIisSettingsWithFallback((SPUrlZone)zone); - if (!iisSettings.UseTrustedClaimsAuthenticationProvider) { continue; } - - // Get the list of authentication providers associated with the zone - foreach (SPAuthenticationProvider prov in iisSettings.ClaimsAuthenticationProviders) - { - if (prov.GetType() == typeof(Microsoft.SharePoint.Administration.SPTrustedAuthenticationProvider)) - { - // Check if the current SPTrustedAuthenticationProvider is associated with the claim provider - if (String.Equals(prov.ClaimProviderName, ProviderInternalName, StringComparison.OrdinalIgnoreCase)) { return true; } - } - } - } - return false; - } - - /// - /// Get the first TrustedLoginProvider associated with current claim provider - /// LIMITATION: The same claims provider (uniquely identified by its name) cannot be associated to multiple TrustedLoginProvider because at runtime there is no way to determine what TrustedLoginProvider is currently calling - /// - /// - /// - public static SPTrustedLoginProvider GetSPTrustAssociatedWithCP(string providerInternalName) - { - var lp = SPSecurityTokenServiceManager.Local.TrustedLoginProviders.Where(x => String.Equals(x.ClaimProviderName, providerInternalName, StringComparison.OrdinalIgnoreCase)); - - if (lp != null && lp.Count() == 1) - { - return lp.First(); - } - - if (lp != null && lp.Count() > 1) - { - ClaimsProviderLogging.Log($"[{providerInternalName}] Cannot continue because '{providerInternalName}' is set with multiple SPTrustedIdentityTokenIssuer", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - } - - ClaimsProviderLogging.Log($"[{providerInternalName}] Cannot continue because '{providerInternalName}' is not set with any SPTrustedIdentityTokenIssuer.\r\nVisit {ClaimsProviderConstants.PUBLICSITEURL} for more information.", TraceSeverity.High, EventSeverity.Warning, TraceCategory.Core); - return null; - } - - /// - /// PickerEntity is resolved (underlined) but claim must be resolved to provide again a PickerEntity so that SharePoint can actually create the entity - /// - /// - /// - /// - /// - protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List resolved) - { - //ClaimsProviderLogging.LogDebug(String.Format("[{0}] FillResolve(SPClaim) called, incoming claim value: \"{1}\", claim type: \"{2}\", claim issuer: \"{3}\"", ProviderInternalName, resolveInput.Value, resolveInput.ClaimType, resolveInput.OriginalIssuer)); - SPSecurity.RunWithElevatedPrivileges(delegate () - { - if (!Initialize(context, entityTypes)) { return; } - - // Ensure incoming claim should be validated by LDAPCP - // Must be made after call to Initialize because SPTrustedLoginProvider name must be known - if (!String.Equals(resolveInput.OriginalIssuer, IssuerName, StringComparison.InvariantCultureIgnoreCase)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - OperationContext currentContext = new OperationContext(CurrentConfiguration, OperationType.Validation, ProcessedClaimTypesList, resolveInput.Value, resolveInput, context, entityTypes, null, 1); - List entities = SearchOrValidate(currentContext); - if (entities?.Count == 1) - { - resolved.Add(entities[0]); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Validated entity: display text: '{entities[0].DisplayText}', claim value: '{entities[0].Claim.Value}', claim type: '{entities[0].Claim.ClaimType}'", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Claims_Picking); - } - else - { - int entityCount = entities == null ? 0 : entities.Count; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Validation failed: found {entityCount.ToString()} entities instead of 1 for incoming claim with value '{currentContext.IncomingEntity.Value}' and type '{currentContext.IncomingEntity.ClaimType}'", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Claims_Picking); - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in FillResolve(SPClaim)", TraceCategory.Claims_Picking, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - }); - } - - /// - /// Called during a search in the small people picker control - /// - /// - /// - /// - /// - protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List resolved) - { - //ClaimsProviderLogging.LogDebug(String.Format("[{0}] FillResolve(string) called, incoming input \"{1}\"", ProviderInternalName, resolveInput)); - - SPSecurity.RunWithElevatedPrivileges(delegate () - { - if (!Initialize(context, entityTypes)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - OperationContext currentContext = new OperationContext(CurrentConfiguration, OperationType.Search, ProcessedClaimTypesList, resolveInput, null, context, entityTypes, null, CurrentConfiguration.MaxSearchResultsCount); - List entities = SearchOrValidate(currentContext); - FillEntities(context, entityTypes, resolveInput, ref entities); - if (entities == null || entities.Count == 0) { return; } - foreach (PickerEntity entity in entities) - { - resolved.Add(entity); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Added entity: display text: '{entity.DisplayText}', claim value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'", - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Claims_Picking); - } - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Returned {entities.Count} entities with input '{currentContext.Input}'", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in FillResolve(string)", TraceCategory.Claims_Picking, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - }); - } - - protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, Microsoft.SharePoint.WebControls.SPProviderHierarchyTree searchTree) - { - //ClaimsProviderLogging.LogDebug(String.Format("[{0}] FillSearch called, incoming input: \"{1}\"", ProviderInternalName, searchPattern)); - SPSecurity.RunWithElevatedPrivileges(delegate () - { - if (!Initialize(context, entityTypes)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - OperationContext currentContext = new OperationContext(CurrentConfiguration, OperationType.Search, ProcessedClaimTypesList, searchPattern, null, context, entityTypes, hierarchyNodeID, CurrentConfiguration.MaxSearchResultsCount); - List entities = SearchOrValidate(currentContext); - FillEntities(context, entityTypes, searchPattern, ref entities); - if (entities == null || entities.Count == 0) { return; } - SPProviderHierarchyNode matchNode = null; - foreach (PickerEntity entity in entities) - { - // Add current PickerEntity to the corresponding attribute in the hierarchy - if (searchTree.HasChild(entity.Claim.ClaimType)) - { - matchNode = searchTree.Children.First(x => x.HierarchyNodeID == entity.Claim.ClaimType); - } - else - { - ClaimTypeConfig ctConfig = ProcessedClaimTypesList.FirstOrDefault(x => - !x.UseMainClaimTypeOfDirectoryObject && - SPClaimTypes.Equals(x.ClaimType, entity.Claim.ClaimType)); - - string nodeName = ctConfig != null ? ctConfig.ClaimTypeDisplayName : entity.Claim.ClaimType; - matchNode = new SPProviderHierarchyNode(_ProviderInternalName, nodeName, entity.Claim.ClaimType, true); - searchTree.AddChild(matchNode); - } - matchNode.AddEntity(entity); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Added entity: display text: '{entity.DisplayText}', claim value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'", - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Claims_Picking); - } - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Returned {entities.Count} entities from input '{currentContext.Input}'", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in FillSearch", TraceCategory.Claims_Picking, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - }); - } - - /// - /// Search or validate incoming input or entity - /// - /// Information about current context and operation - /// Entities generated by AzureCP - protected virtual List SearchOrValidate(OperationContext currentContext) - { - List entities = new List(); - try - { - if (this.CurrentConfiguration.BypassLDAPLookup) - { - // Completely bypass LDAP lookp - entities = CreatePickerEntityForSpecificClaimTypes( - currentContext.Input, - currentContext.CurrentClaimTypeConfigList.Where(x => !x.UseMainClaimTypeOfDirectoryObject), - false); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created {entities.Count} entity(ies) from input \"{currentContext.Input}\" without contacting LDAP server(s) because LDAPCP property BypassLDAPLookup is set to true.", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); - return entities; - } - - if (currentContext.OperationType == OperationType.Search) - { - entities = SearchOrValidateInLDAP(currentContext); - - // Check if input starts with a prefix configured on a ClaimTypeConfig. If so an entity should be returned using ClaimTypeConfig found - // ClaimTypeConfigEnsureUniquePrefixToBypassLookup ensures that collection cannot contain duplicates - ClaimTypeConfig ctConfigWithInputPrefixMatch = currentContext.CurrentClaimTypeConfigList.FirstOrDefault(x => - !String.IsNullOrEmpty(x.PrefixToBypassLookup) && - currentContext.Input.StartsWith(x.PrefixToBypassLookup, StringComparison.InvariantCultureIgnoreCase)); - if (ctConfigWithInputPrefixMatch != null) - { - string inputWithoutPrefix = currentContext.Input.Substring(ctConfigWithInputPrefixMatch.PrefixToBypassLookup.Length); - if (String.IsNullOrEmpty(inputWithoutPrefix)) - { - // No value in the input after the prefix, return - return entities; - } - PickerEntity entity = CreatePickerEntityForSpecificClaimType( - inputWithoutPrefix, - ctConfigWithInputPrefixMatch, - true); - if (entity != null) - { - if (entities == null) { entities = new List(); } - entities.Add(entity); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created entity from input \"{currentContext.Input}\" without contacting LDAP server(s) because input started with prefix '{ctConfigWithInputPrefixMatch.PrefixToBypassLookup}', which is configured for claim type '{ctConfigWithInputPrefixMatch.ClaimType}'. Claim value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'", - TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - //return entities; - } - } - } - else if (currentContext.OperationType == OperationType.Validation) - { - entities = SearchOrValidateInLDAP(currentContext); - if (entities?.Count == 1) { return entities; } - - if (!String.IsNullOrEmpty(currentContext.IncomingEntityClaimTypeConfig.PrefixToBypassLookup)) - { - // At this stage, it is impossible to know if entity was originally created with the keyword that bypass query to Azure AD - // But it should be always validated since property PrefixToBypassLookup is set for current ClaimTypeConfig, so create entity manually - PickerEntity entity = CreatePickerEntityForSpecificClaimType( - currentContext.IncomingEntity.Value, - currentContext.IncomingEntityClaimTypeConfig, - currentContext.InputHasKeyword); - if (entity != null) - { - entities = new List(1) { entity }; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Validated entity without contacting LDAP server(s) because its claim type ('{currentContext.IncomingEntityClaimTypeConfig.ClaimType}') has property 'PrefixToBypassLookup' set in AzureCPConfig.ClaimTypes. Claim value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'", - TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - } - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in SearchOrValidate", TraceCategory.Claims_Picking, ex); - } - return entities; - } - - /// - /// Search or validate incoming input or entity with LDAP lookup - /// - /// Information about current context and operation - /// - /// - protected virtual List SearchOrValidateInLDAP(OperationContext currentContext) - { - if (this.CurrentConfiguration.LDAPConnectionsProp == null || this.CurrentConfiguration.LDAPConnectionsProp.Count == 0) - { - ClaimsProviderLogging.Log(String.Format("[{0}] No LDAP server is configured.", ProviderInternalName), TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); - return null; - } - - // BUG: Filters must be set in an object created in this method (to be bound to current thread), otherwise filter may be updated by multiple threads - List ldapServers = new List(this.CurrentConfiguration.LDAPConnectionsProp.Count); - foreach (LDAPConnection ldapServer in this.CurrentConfiguration.LDAPConnectionsProp) - { - ldapServers.Add(ldapServer.CopyConfiguration()); - } - - BuildLDAPFilter(currentContext, ldapServers); - bool resultsfound = false; - List LDAPSearchResultWrappers = new List(); - using (new SPMonitoredScope(String.Format("[{0}] Total time spent in all LDAP server(s)", ProviderInternalName), 1000)) - { - resultsfound = QueryLDAPServers(currentContext, ldapServers, ref LDAPSearchResultWrappers); - } - - if (!resultsfound) { return null; } - ConsolidatedResultCollection results = ProcessLdapResults(currentContext, ref LDAPSearchResultWrappers); - if (results == null || results.Count <= 0) { return null; } - - // There may be some extra work based on currentContext associated with the input claim type - // Check to see if we have a prefix and have a domain token - if (currentContext.OperationType == OperationType.Validation && currentContext.IncomingEntityClaimTypeConfig.ClaimValuePrefix != null) - { - // Extract just the domain from the input - bool tokenFound = false; - string domainOnly = String.Empty; - if (currentContext.IncomingEntityClaimTypeConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME)) - { - tokenFound = true; - domainOnly = OperationContext.GetDomainFromFullAccountName(currentContext.IncomingEntity.Value); - } - else if (currentContext.IncomingEntityClaimTypeConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN)) - { - tokenFound = true; - string fqdn = OperationContext.GetDomainFromFullAccountName(currentContext.IncomingEntity.Value); - domainOnly = OperationContext.GetFirstSubString(fqdn, "."); - } - - if (tokenFound) - { - // Only keep results where the domain is a match - ConsolidatedResultCollection filteredResults = new ConsolidatedResultCollection(); - foreach (var result in results) - { - if (String.Equals(result.DomainName, domainOnly, StringComparison.InvariantCultureIgnoreCase)) - { - filteredResults.Add(result); - } - } - results = filteredResults; - } - } - - List entities = new List(); - foreach (var result in results) - { - entities.Add(result.PickerEntity); - //ClaimsProviderLogging.Log(String.Format("[{0}] Added entity created with LDAP lookup: claim value: \"{1}\", claim type: \"{2}\"", ProviderInternalName, result.PickerEntity.Claim.Value, result.PickerEntity.Claim.ClaimType), - // TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - return entities; - } - - /// - /// Processes LDAP results stored in LDAPSearchResultWrappers and returns result in parameter results - /// - /// Information about current context and operation - /// - /// - protected virtual ConsolidatedResultCollection ProcessLdapResults(OperationContext currentContext, ref List LDAPSearchResults) - { - ConsolidatedResultCollection results = new ConsolidatedResultCollection(); - ResultPropertyCollection LDAPResultProperties; - IEnumerable ctConfigs = currentContext.CurrentClaimTypeConfigList; - if (currentContext.ExactSearch) - { - ctConfigs = currentContext.CurrentClaimTypeConfigList.Where(x => !x.UseMainClaimTypeOfDirectoryObject); - } - - foreach (LDAPSearchResult LDAPResult in LDAPSearchResults) - { - LDAPResultProperties = LDAPResult.SearchResult.Properties; - // objectclass attribute should never be missing because it is explicitely requested in LDAP query - if (!LDAPResultProperties.Contains(LDAPObjectClassName)) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Property '{LDAPObjectClassName}' is missing in LDAP result, this may be due to insufficient entities of the account connecting to LDAP server '{LDAPResult.DomainFQDN}'. Skipping result.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.LDAP_Lookup); - continue; - } - - // Cast collection to be able to use StringComparer.InvariantCultureIgnoreCase for case insensitive search of ldap properties - IEnumerable LDAPResultPropertyNames = LDAPResultProperties.PropertyNames.Cast(); - - // Issue https://github.com/Yvand/LDAPCP/issues/16: If current result is a user, ensure LDAP attribute of identity ClaimTypeConfig exists in current LDAP result - bool isUserWithNoIdentityAttribute = false; - if (LDAPResultProperties[LDAPObjectClassName].Cast().Contains(IdentityClaimTypeConfig.LDAPClass, StringComparer.InvariantCultureIgnoreCase)) - { - // This is a user: check if his identity LDAP attribute (e.g. mail or sAMAccountName) is present - if (!LDAPResultPropertyNames.Contains(IdentityClaimTypeConfig.LDAPAttribute, StringComparer.InvariantCultureIgnoreCase)) - { - // This may match a result like PrimaryGroupID, which has EntityType "Group", but LDAPClass "User" - // So it cannot be ruled out immediately, but needs be tested against each ClaimTypeConfig - //ClaimsProviderLogging.Log($"[{ProviderInternalName}] Ignoring a user because he doesn't have the LDAP attribute '{IdentityClaimTypeConfig.LDAPAttribute}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.LDAP_Lookup); - //continue; - isUserWithNoIdentityAttribute = true; - } - } - - foreach (ClaimTypeConfig ctConfig in ctConfigs) - { - // Skip if: current config is for users AND LDAP result is a user AND LDAP result doesn't have identity attribute set - if (ctConfig.EntityType == DirectoryObjectType.User && isUserWithNoIdentityAttribute) - { - continue; - } - - // Skip if: LDAPClass of current config does not match objectclass of LDAP result - if (!LDAPResultProperties[LDAPObjectClassName].Cast().Contains(ctConfig.LDAPClass, StringComparer.InvariantCultureIgnoreCase)) - { - continue; - } - - // Skip if: LDAPAttribute of current config is not found in LDAP result - if (!LDAPResultPropertyNames.Contains(ctConfig.LDAPAttribute, StringComparer.InvariantCultureIgnoreCase)) - { - continue; - } - - // Get value with of current LDAP attribute - // TODO: investigate https://github.com/Yvand/LDAPCP/issues/43 - string directoryObjectPropertyValue = LDAPResultProperties[LDAPResultPropertyNames.First(x => String.Equals(x, ctConfig.LDAPAttribute, StringComparison.InvariantCultureIgnoreCase))][0].ToString(); - - // Check if current LDAP attribute value matches the input - if (currentContext.ExactSearch) - { - if (!String.Equals(directoryObjectPropertyValue, currentContext.Input, StringComparison.InvariantCultureIgnoreCase)) - { - continue; - } - } - else - { - if (this.CurrentConfiguration.AddWildcardAsPrefixOfInput) - { - if (directoryObjectPropertyValue.IndexOf(currentContext.Input, StringComparison.InvariantCultureIgnoreCase) != -1) - { - continue; - } - } - else - { - if (!directoryObjectPropertyValue.StartsWith(currentContext.Input, StringComparison.InvariantCultureIgnoreCase)) - { - continue; - } - } - } - - // Check if current result (association of LDAP result + ClaimTypeConfig) is not already in results list - // Get ClaimTypeConfig to use to check if result is already present in the results list - ClaimTypeConfig ctConfigToUseForDuplicateCheck = ctConfig; - if (ctConfig.UseMainClaimTypeOfDirectoryObject) - { - if (ctConfig.EntityType == DirectoryObjectType.User) - { - if (String.Equals(ctConfig.LDAPClass, IdentityClaimTypeConfig.LDAPClass, StringComparison.InvariantCultureIgnoreCase)) - { - ctConfigToUseForDuplicateCheck = IdentityClaimTypeConfig; - } - else - { - continue; // Current ClaimTypeConfig is a user but current LDAP result is not, skip - } - } - else - { - if (MainGroupClaimTypeConfig != null && String.Equals(ctConfig.LDAPClass, MainGroupClaimTypeConfig.LDAPClass, StringComparison.InvariantCultureIgnoreCase)) - { - ctConfigToUseForDuplicateCheck = MainGroupClaimTypeConfig; - } - else - { - continue; // Current ClaimTypeConfig is a group but current LDAP result is not, skip - } - } - } - - // When token domain is present, then ensure we do compare with the actual domain name - bool compareWithDomain = HasPrefixToken(ctConfig.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME) ? true : this.CurrentConfiguration.CompareResultsWithDomainNameProp; - if (!compareWithDomain) - { - compareWithDomain = HasPrefixToken(ctConfig.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN) ? true : this.CurrentConfiguration.CompareResultsWithDomainNameProp; - } - if (results.Contains(LDAPResult, ctConfigToUseForDuplicateCheck, compareWithDomain)) - { - continue; - } - - results.Add( - new ConsolidatedResult - { - ClaimTypeConfig = ctConfig, - LDAPResults = LDAPResultProperties, - Value = directoryObjectPropertyValue, - DomainName = LDAPResult.DomainName, - DomainFQDN = LDAPResult.DomainFQDN, - //DEBUG = String.Format("LDAPAttribute: {0}, LDAPAttributeValue: {1}, AlwaysResolveAgainstIdentityClaim: {2}", attr.LDAPAttribute, LDAPResultProperties[attr.LDAPAttribute][0].ToString(), attr.AlwaysResolveAgainstIdentityClaim.ToString()) - }); - } - } - ClaimsProviderLogging.Log(String.Format("[{0}] {1} entity(ies) to create after filtering", ProviderInternalName, results.Count), TraceSeverity.Medium, EventSeverity.Information, TraceCategory.LDAP_Lookup); - foreach (var result in results) - { - PickerEntity pe = CreatePickerEntityHelper(result); - // Add it to the return list of picker entries. - result.PickerEntity = pe; - } - return results; - } - - /// - /// Override this method to change LDAP filter created by LDAPCP. It's possible to set a different LDAP filter for each LDAP server - /// - /// Information about current context and operation - /// List to be populated by this method - protected virtual void BuildLDAPFilter(OperationContext currentContext, List ldapServers) - { - // Build LDAP filter as documented in http://technet.microsoft.com/fr-fr/library/aa996205(v=EXCHG.65).aspx - StringBuilder filter = new StringBuilder(); - if (this.CurrentConfiguration.FilterEnabledUsersOnlyProp) - { - filter.Append(LDAPFilterEnabledUsersOnly); - } - filter.Append("(| "); // START OR - - string preferredFilterPattern; - // Fix bug https://github.com/Yvand/LDAPCP/issues/53 by escaping special characters with their hex representation as documented in https://ldap.com/ldap-filters/ - string input = OperationContext.EscapeSpecialCharacters(currentContext.Input); - if (currentContext.ExactSearch) - { - preferredFilterPattern = input; - } - else - { - preferredFilterPattern = this.CurrentConfiguration.AddWildcardAsPrefixOfInput ? "*" + input + "*" : input + "*"; - } - - foreach (var ctConfig in currentContext.CurrentClaimTypeConfigList) - { - if (ctConfig.SupportsWildcard) - { - filter.Append(AddAttributeToFilter(ctConfig, preferredFilterPattern)); - } - else - { - filter.Append(AddAttributeToFilter(ctConfig, input)); - } - } - - if (this.CurrentConfiguration.FilterEnabledUsersOnlyProp) - { - filter.Append(")"); - } - filter.Append(")"); // END OR - - foreach (LDAPConnection ldapServer in ldapServers) - { - ldapServer.Filter = filter.ToString(); - } - } - - /// - /// Query LDAP servers in parallel - /// - /// LDAP servers to query - /// Information about current context and operation - /// LDAP search results list to be populated by this method - /// true if a result was found - protected bool QueryLDAPServers(OperationContext currentContext, List ldapServers, ref List LDAPSearchResults) - { - if (ldapServers == null || ldapServers.Count == 0) { return false; } - object lockResults = new object(); - List results = new List(); - Stopwatch globalStopWatch = new Stopwatch(); - globalStopWatch.Start(); - - Parallel.ForEach(ldapServers.Where(x => !String.IsNullOrEmpty(x.Filter)), ldapConnection => - { - Debug.WriteLine($"ldapConnection: Path: {ldapConnection.Directory.Path}, UseSPServerConnectionToAD: {ldapConnection.UseSPServerConnectionToAD}"); - ClaimsProviderLogging.LogDebug($"ldapConnection: Path: {ldapConnection.Directory.Path}, UseSPServerConnectionToAD: {ldapConnection.UseSPServerConnectionToAD}"); -#pragma warning disable CS0618 // Type or member is obsolete - SetLDAPConnection(currentContext, ldapConnection); -#pragma warning restore CS0618 // Type or member is obsolete - DirectoryEntry directory = ldapConnection.Directory; - using (DirectorySearcher ds = new DirectorySearcher(ldapConnection.Filter)) - { - ds.SearchRoot = directory; - ds.SizeLimit = currentContext.MaxCount; - ds.ClientTimeout = new TimeSpan(0, 0, this.CurrentConfiguration.LDAPQueryTimeout); // Set the timeout of the query - ds.PropertiesToLoad.Add(LDAPObjectClassName); - ds.PropertiesToLoad.Add("nETBIOSName"); - foreach (var ldapAttribute in ProcessedClaimTypesList.Where(x => !String.IsNullOrEmpty(x.LDAPAttribute))) - { - ds.PropertiesToLoad.Add(ldapAttribute.LDAPAttribute); - if (!String.IsNullOrEmpty(ldapAttribute.LDAPAttributeToShowAsDisplayText)) - { - ds.PropertiesToLoad.Add(ldapAttribute.LDAPAttributeToShowAsDisplayText); - } - } - // Populate additional attributes that are not part of the filter but are requested in the result - foreach (var metadataAttribute in MetadataConfig) - { - if (!ds.PropertiesToLoad.Contains(metadataAttribute.LDAPAttribute)) - { - ds.PropertiesToLoad.Add(metadataAttribute.LDAPAttribute); - } - } - - string loggMessage = $"[{ProviderInternalName}] Connecting to \"{ldapConnection.Directory.Path}\" with AuthenticationType \"{ldapConnection.Directory.AuthenticationType}\", authenticating "; - loggMessage += String.IsNullOrWhiteSpace(ldapConnection.Directory.Username) ? "as process identity" : $"with credentials \"{ldapConnection.Directory.Username}\""; - loggMessage += $" and sending a query with filter \"{ds.Filter}\"..."; - using (new SPMonitoredScope(loggMessage, 3000)) // threshold of 3 seconds before it's considered too much. If exceeded it is recorded in a higher logging level - { - try - { - ClaimsProviderLogging.Log(loggMessage, TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.LDAP_Lookup); - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - using (SearchResultCollection directoryResults = ds.FindAll()) - { - stopWatch.Stop(); - if (directoryResults != null && directoryResults.Count > 0) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Got {directoryResults.Count.ToString()} result(s) in {stopWatch.ElapsedMilliseconds.ToString()}ms from '{directory.Path}' with filter '{ds.Filter}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.LDAP_Lookup); - lock (lockResults) - { - foreach (SearchResult item in directoryResults) - { - results.Add(new LDAPSearchResult() - { - SearchResult = item, - DomainName = ldapConnection.DomainName, - DomainFQDN = ldapConnection.DomainFQDN, - }); - } - } - } - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "while connecting to LDAP server " + directory.Path, TraceCategory.LDAP_Lookup, ex); - } - finally - { - directory.Dispose(); - } - } - } - }); - - globalStopWatch.Stop(); - LDAPSearchResults = results; - ClaimsProviderLogging.Log(String.Format("[{0}] Got {1} result(s) in {2}ms from all servers with query \"{3}\"", ProviderInternalName, LDAPSearchResults.Count, globalStopWatch.ElapsedMilliseconds.ToString(), ldapServers[0].Filter), TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.LDAP_Lookup); - return LDAPSearchResults.Count > 0; - } - - /// - /// Override this method to configure LDAPConnection passed in parameter - /// - /// Uri for which current operation applies - /// LDAPConnection to configure - protected virtual void SetLDAPConnection(Uri currentContext, LDAPConnection ldapConnection) - { - if (!ldapConnection.UseSPServerConnectionToAD) - { - ldapConnection.Directory = new DirectoryEntry(ldapConnection.LDAPPath, ldapConnection.LDAPUsername, ldapConnection.LDAPPassword, ldapConnection.AuthenticationSettings); - } - else - { - try - { - // This try block is to get domain name information about AD domain of current computer - // If this fails, execution should still continue as: - // - It will be attempted again in a different way in OperationContext.GetDomainInformation(), so it should be given a chance - // - It often (only) fails with COMException, which tend to occur only in some code path, but finally works depending on how LDAPCP is called - // - It's not essential, even though it can have serious impacts, for example, value of role claims miss the domain name - Domain computerDomain = Domain.GetComputerDomain(); - ldapConnection.Directory = computerDomain.GetDirectoryEntry(); - - // Set properties LDAPConnection.DomainFQDN and LDAPConnection.DomainName here as a workaround to issue https://github.com/Yvand/LDAPCP/issues/87 - ldapConnection.DomainFQDN = computerDomain.Name; - ldapConnection.DomainName = OperationContext.GetDomainName(ldapConnection.DomainFQDN); - - // Property LDAPConnection.AuthenticationSettings must be set, in order to build the PrincipalContext correctly in GetGroupsFromActiveDirectory() - ldapConnection.AuthenticationSettings = ldapConnection.Directory.AuthenticationType; - } - catch (System.Runtime.InteropServices.COMException ex) - { - // Domain.GetDomain() may fail with the following error: System.Runtime.InteropServices.COMException: Retrieving the COM class factory for component with CLSID {080D0D78-F421-11D0-A36E-00C04FB950DC} failed due to the following error: 800703fa Illegal operation attempted on a registry key that has been marked for deletion. (Exception from HRESULT: 0x800703FA). - ClaimsProviderLogging.LogException("", $"while getting domain names information about AD domain of current computer (COMException)", TraceCategory.Configuration, ex); - } - catch (Exception ex) - { - // Domain.GetDomain() may fail with the following error: System.Runtime.InteropServices.COMException: Retrieving the COM class factory for component with CLSID {080D0D78-F421-11D0-A36E-00C04FB950DC} failed due to the following error: 800703fa Illegal operation attempted on a registry key that has been marked for deletion. (Exception from HRESULT: 0x800703FA). - ClaimsProviderLogging.LogException("", $"while getting domain names information about AD domain of current computer", TraceCategory.Configuration, ex); - } - } - - if (String.IsNullOrEmpty(ldapConnection.RootContainer) || String.IsNullOrEmpty(ldapConnection.DomainFQDN) || String.IsNullOrEmpty(ldapConnection.DomainName)) - { - // This block does LDAP operations - using (new SPMonitoredScope($"[{ProviderInternalName}] Get domain names / root container information about LDAP server \"{ldapConnection.Directory.Path}\"", 2000)) - { - // Retrieve FQDN and domain name of current DirectoryEntry - string domainName = String.Empty; - string domainFQDN = String.Empty; - string domaindistinguishedName = String.Empty; - - // If there is no existing LDAPCP configuration, this method will be called each time as property LDAPConnection.RootContainer will be null - OperationContext.GetDomainInformation(ldapConnection.Directory, out domaindistinguishedName, out domainName, out domainFQDN); - // Cache those values for the whole lifetime of the process, because getting them requires LDAP operations - if (!String.IsNullOrWhiteSpace(domaindistinguishedName)) - { - ldapConnection.RootContainer = domaindistinguishedName; - } - if (!String.IsNullOrWhiteSpace(domainName)) - { - ldapConnection.DomainName = domainName; - } - if (!String.IsNullOrWhiteSpace(domainFQDN)) - { - ldapConnection.DomainFQDN = domainFQDN; - } - } - } - } - - /// - /// Override this method to configure LDAPConnection passed in parameter - /// - /// Information about current context and operation - /// LDAPConnection to configure - [Obsolete("SetLDAPConnection is deprecated, please override the other definition of SetLDAPConnection instead.")] - protected virtual void SetLDAPConnection(OperationContext currentContext, LDAPConnection ldapConnection) - { - } - - protected override void FillClaimTypes(List claimTypes) - { - if (claimTypes == null) - { - throw new ArgumentNullException("claimTypes"); - } - - ClaimsProviderLogging.LogDebug(String.Format("[{0}] FillClaimValueTypes called, ProcessedAttributes null: {1}", ProviderInternalName, ProcessedClaimTypesList == null ? true : false)); - - if (ProcessedClaimTypesList == null) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - foreach (var attribute in ProcessedClaimTypesList.Where(x => !String.IsNullOrEmpty(x.ClaimType))) - { - claimTypes.Add(attribute.ClaimType); - } - } - finally - { - this.Lock_Config.ExitReadLock(); - } - } - - protected override void FillClaimValueTypes(List claimValueTypes) - { - if (claimValueTypes == null) - { - throw new ArgumentNullException("claimValueTypes"); - } - - ClaimsProviderLogging.LogDebug(String.Format("[{0}] FillClaimValueTypes called, ProcessedAttributes null: {1}", ProviderInternalName, ProcessedClaimTypesList == null ? true : false)); - - if (ProcessedClaimTypesList == null) - { - return; - } - - this.Lock_Config.EnterReadLock(); - try - { - foreach (var attribute in ProcessedClaimTypesList.Where(x => !String.IsNullOrEmpty(x.ClaimValueType))) - { - claimValueTypes.Add(attribute.ClaimValueType); - } - } - finally - { - this.Lock_Config.ExitReadLock(); - } - } - - protected override void FillClaimsForEntity(Uri context, SPClaim entity, List claims) - { - AugmentEntity(context, entity, null, claims); - } - - protected override void FillClaimsForEntity(Uri context, SPClaim entity, SPClaimProviderContext claimProviderContext, List claims) - { - AugmentEntity(context, entity, claimProviderContext, claims); - } - - /// - /// Perform augmentation of entity supplied - /// - /// - /// entity to augment - /// Can be null - /// - protected virtual void AugmentEntity(Uri context, SPClaim entity, SPClaimProviderContext claimProviderContext, List claims) - { - SPClaim decodedEntity; - if (SPClaimProviderManager.IsUserIdentifierClaim(entity)) - { - decodedEntity = SPClaimProviderManager.DecodeUserIdentifierClaim(entity); - } - else - { - if (SPClaimProviderManager.IsEncodedClaim(entity.Value)) - { - decodedEntity = SPClaimProviderManager.Local.DecodeClaim(entity.Value); - } - else - { - decodedEntity = entity; - } - } - - SPOriginalIssuerType loginType = SPOriginalIssuers.GetIssuerType(decodedEntity.OriginalIssuer); - if (loginType != SPOriginalIssuerType.TrustedProvider && loginType != SPOriginalIssuerType.ClaimProvider) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Not trying to augment '{decodedEntity.Value}' because his OriginalIssuer is '{decodedEntity.OriginalIssuer}'.", - TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Augmentation); - return; - } - - SPSecurity.RunWithElevatedPrivileges(delegate () - { - if (!Initialize(context, null)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - // There can be multiple TrustedProvider on the farm, but LDAPCP should only do augmentation if current entity is from TrustedProvider it is associated with - if (!String.Equals(decodedEntity.OriginalIssuer, IssuerName, StringComparison.InvariantCultureIgnoreCase)) { return; } - - if (!this.CurrentConfiguration.EnableAugmentation) { return; } - - if (String.IsNullOrEmpty(this.CurrentConfiguration.MainGroupClaimType)) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Augmentation is enabled but property MainGroupClaimType is not set.", TraceSeverity.High, EventSeverity.Error, TraceCategory.Augmentation); - return; - } - - IEnumerable allGroupsCTConfig = this.ProcessedClaimTypesList.Where(x => x.EntityType == DirectoryObjectType.Group && !x.UseMainClaimTypeOfDirectoryObject); - ClaimTypeConfig mainGroupCTConfig = allGroupsCTConfig.FirstOrDefault(x => SPClaimTypes.Equals(x.ClaimType, this.CurrentConfiguration.MainGroupClaimType)); - if (mainGroupCTConfig == null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration for claim type '{this.CurrentConfiguration.MainGroupClaimType}' cannot be found, please add it in claim types configuration list.", - TraceSeverity.High, EventSeverity.Error, TraceCategory.Augmentation); - return; - } - - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Starting augmentation for user '{decodedEntity.Value}'.", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - OperationContext currentContext = new OperationContext(CurrentConfiguration, OperationType.Augmentation, ProcessedClaimTypesList, null, decodedEntity, context, null, null, Int32.MaxValue); - Stopwatch timer = new Stopwatch(); - timer.Start(); - List groups = new List(); - object lockResults = new object(); - - // BUG: Filters must be set in an object created in this method (to be bound to current thread), otherwise filter may be updated by multiple threads - List ldapServers = new List(this.CurrentConfiguration.LDAPConnectionsProp.Count); - foreach (LDAPConnection ldapServer in this.CurrentConfiguration.LDAPConnectionsProp.Where(x => x.EnableAugmentation)) - { - ldapServers.Add(ldapServer.CopyConfiguration()); - } - - Parallel.ForEach(ldapServers, ldapConnection => - { - List directoryGroups; - if (ldapConnection.GetGroupMembershipUsingDotNetHelpers) - { - directoryGroups = GetGroupsFromActiveDirectory(ldapConnection, currentContext, mainGroupCTConfig); - directoryGroups.AddRange(GetGroupsFromLDAPDirectory(ldapConnection, currentContext, allGroupsCTConfig.Where(x => !SPClaimTypes.Equals(x.ClaimType, this.CurrentConfiguration.MainGroupClaimType)))); - } - else - { - directoryGroups = GetGroupsFromLDAPDirectory(ldapConnection, currentContext, allGroupsCTConfig); - } - - lock (lockResults) - { - groups.AddRange(directoryGroups); - } - }); - timer.Stop(); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] LDAP queries to get group membership on all servers completed in {timer.ElapsedMilliseconds.ToString()}ms", - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - - foreach (SPClaim group in groups) - { - claims.Add(group); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Added group '{group.Value}', claim type '{group.ClaimType}' to user '{currentContext.IncomingEntity.Value}'", - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - } - if (groups.Count > 0) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] User '{currentContext.IncomingEntity.Value}' was augmented with {groups.Count.ToString()} groups", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); - } - else - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] No group found for user '{currentContext.IncomingEntity.Value}' during augmentation", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in AugmentEntity", TraceCategory.Augmentation, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - }); - } - - /// - /// Get group membership using UserPrincipal.GetAuthorizationGroups(), which works only with AD - /// UserPrincipal.GetAuthorizationGroups() gets groups using Kerberos protocol transition (preferred way), and falls back to LDAP queries otherwise. - /// - /// - /// - /// - /// - protected virtual List GetGroupsFromActiveDirectory(LDAPConnection ldapConnection, OperationContext currentContext, ClaimTypeConfig groupCTConfig) - { - // Convert AuthenticationTypes to ContextOptions, slightly inspired by https://stackoverflow.com/questions/17451277/what-equivalent-of-authenticationtypes-secure-in-principalcontexts-contextoptio - // AuthenticationTypes Enum: https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.authenticationtypes?view=netframework-4.8.1 - // ContextOptions Enum: https://learn.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.contextoptions?view=netframework-4.8.1 - ContextOptions contextOptions = new ContextOptions(); - if (ldapConnection.AuthenticationSettings == AuthenticationTypes.None) - { - contextOptions |= ContextOptions.SimpleBind; - } - else - { - if ((ldapConnection.AuthenticationSettings & AuthenticationTypes.Sealing) == AuthenticationTypes.Sealing) { contextOptions |= ContextOptions.Sealing; } - if ( - (ldapConnection.AuthenticationSettings & AuthenticationTypes.Encryption) == AuthenticationTypes.Encryption || - (ldapConnection.AuthenticationSettings & AuthenticationTypes.SecureSocketsLayer) == AuthenticationTypes.SecureSocketsLayer - ) { contextOptions |= ContextOptions.SecureSocketLayer; } - if ((ldapConnection.AuthenticationSettings & AuthenticationTypes.ServerBind) == AuthenticationTypes.ServerBind) { contextOptions |= ContextOptions.ServerBind; } - if ((ldapConnection.AuthenticationSettings & AuthenticationTypes.Signing) == AuthenticationTypes.Signing) { contextOptions |= ContextOptions.Signing; } - if ((ldapConnection.AuthenticationSettings & AuthenticationTypes.Secure) == AuthenticationTypes.Secure) { contextOptions |= ContextOptions.Negotiate; } - } - - List groups = new List(); - string logMessageCredentials = ldapConnection.UseSPServerConnectionToAD ? "process identity" : ldapConnection.LDAPUsername; - string directoryDetails = $"from AD domain \"{ldapConnection.DomainFQDN}\" (authenticate as \"{logMessageCredentials}\" with AuthenticationType \"{contextOptions}\")."; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Getting AD groups of user \"{currentContext.IncomingEntity.Value}\" {directoryDetails}", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - using (new SPMonitoredScope($"[{ProviderInternalName}] Get AD groups of user \"{currentContext.IncomingEntity.Value}\" {directoryDetails}", 2000)) - { - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - UserPrincipal adUser = null; - PrincipalContext principalContext = null; - try - { - using (new SPMonitoredScope($"[{ProviderInternalName}] Get AD Principal of user {currentContext.IncomingEntity.Value} " + directoryDetails, 1000)) - { - // Constructor of PrincipalContext does connect to LDAP server and may throw an exception if it fails, so it should be in try/catch - // To use ContextOptions in constructor of PrincipalContext, "container" must also be set, but it can be null as per tests in https://stackoverflow.com/questions/2538064/active-directory-services-principalcontext-what-is-the-dn-of-a-container-o - // Tests: if "container" is null, it always fails in PowerShell (tested only with AD) but somehow it works fine here - if (ldapConnection.UseSPServerConnectionToAD) - { - principalContext = new PrincipalContext(ContextType.Domain, ldapConnection.DomainFQDN, ldapConnection.RootContainer, contextOptions); - } - else - { - principalContext = new PrincipalContext(ContextType.Domain, ldapConnection.DomainFQDN, ldapConnection.RootContainer, contextOptions, ldapConnection.LDAPUsername, ldapConnection.LDAPPassword); - } - - // https://github.com/Yvand/LDAPCP/issues/22: UserPrincipal.FindByIdentity() doesn't support emails, so if IncomingEntity is an email, user needs to be retrieved in a different way - if (SPClaimTypes.Equals(currentContext.IncomingEntity.ClaimType, WIF4_5.ClaimTypes.Email)) - { - using (UserPrincipal userEmailPrincipal = new UserPrincipal(principalContext) { Enabled = true, EmailAddress = currentContext.IncomingEntity.Value }) - { - using (PrincipalSearcher userEmailSearcher = new PrincipalSearcher(userEmailPrincipal)) - { - adUser = userEmailSearcher.FindOne() as UserPrincipal; - } - } - } - else - { - adUser = UserPrincipal.FindByIdentity(principalContext, currentContext.IncomingEntity.Value); - } - } - - if (adUser == null) - { - stopWatch.Stop(); - return groups; - } - - IEnumerable adGroups; - using (new SPMonitoredScope($"[{ProviderInternalName}] Get group membership of \"{currentContext.IncomingEntity.Value}\" " + directoryDetails, 1000)) - { - adGroups = adUser.GetAuthorizationGroups(); - } - - using (new SPMonitoredScope($"[{ProviderInternalName}] Process {adGroups.Count()} AD groups returned for user \"{currentContext.IncomingEntity.Value}\" " + directoryDetails + " Eeach AD group triggers a specific LDAP operation.", 1000)) - { - // https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.accountmanagement.userprincipal.getauthorizationgroups?view=netframework-4.7.1#System_DirectoryServices_AccountManagement_UserPrincipal_GetAuthorizationGroups - // UserPrincipal.GetAuthorizationGroups() only returns security groups, and includes nested groups and special groups like "Domain Users". - // The foreach calls AccountManagement.FindResultEnumerator`1.get_Current() that does LDAP binds (call to DirectoryEntry.Bind()) for each group - // It may impact performance if there are many groups and/or if DC is slow - foreach (Principal adGroup in adGroups) - { - string groupDomainName, groupDomainFqdn; - - // https://github.com/Yvand/LDAPCP/issues/148 - the group property used for the group value should be based on the LDAPCP configuration - // By default it should be the SamAccountName, since it's also the default attribute set in LDAPCP configuration - string claimValue = adGroup.SamAccountName; - switch (groupCTConfig.LDAPAttribute.ToLower()) - { - case "name": - claimValue = adGroup.Name; - break; - - case "distinguishedname": - claimValue = adGroup.DistinguishedName; - break; - - case "samaccountname": - claimValue = adGroup.SamAccountName; - break; - } - - if (!String.IsNullOrEmpty(groupCTConfig.ClaimValuePrefix)) - { - // Principal.DistinguishedName is used to build the domain name / FQDN of the current group. Example of value: CN=group1,CN=Users,DC=contoso,DC=local - string groupDN = adGroup.DistinguishedName; - if (String.IsNullOrEmpty(groupDN)) { continue; } - - OperationContext.GetDomainInformation(groupDN, out groupDomainName, out groupDomainFqdn); - if (groupCTConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME)) - { - claimValue = groupCTConfig.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME, groupDomainName) + claimValue; - } - else if (groupCTConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN)) - { - claimValue = groupCTConfig.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN, groupDomainFqdn) + claimValue; - } - } - - SPClaim claim = CreateClaim(groupCTConfig.ClaimType, claimValue, groupCTConfig.ClaimValueType, false); - groups.Add(claim); - } - } - } - catch (PrincipalOperationException ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"while getting AD groups of user \"{currentContext.IncomingEntity.Value}\" using UserPrincipal.GetAuthorizationGroups() {directoryDetails} This is likely due to a bug in .NET framework in UserPrincipal.GetAuthorizationGroups (as of v4.6.1), especially if user is member (directly or not) of a group either in a child domain that was migrated, or a group that has special (deny) entities.", TraceCategory.Augmentation, ex); - // In this case, fallback to LDAP method to get group membership. - return GetGroupsFromLDAPDirectory(ldapConnection, currentContext, new List(1) { groupCTConfig }); - } - catch (PrincipalServerDownException ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"while getting AD groups of user \"{currentContext.IncomingEntity.Value}\" using UserPrincipal.GetAuthorizationGroups() {directoryDetails} Is this server an Active Directory server?", TraceCategory.Augmentation, ex); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"while getting AD groups of user \"{currentContext.IncomingEntity.Value}\" using UserPrincipal.GetAuthorizationGroups() {directoryDetails}", TraceCategory.Augmentation, ex); - } - finally - { - if (principalContext != null) { principalContext.Dispose(); } - if (adUser != null) { adUser.Dispose(); } - - stopWatch.Stop(); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Got and processed {groups.Count} group(s) for user \"{currentContext.IncomingEntity.Value}\" in {stopWatch.ElapsedMilliseconds.ToString()} ms {directoryDetails}", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); - } - } - return groups; - } - - /// - /// Get group membership with a LDAP query - /// - /// LDAP server to query - /// Information about current context and operation - /// - /// - protected virtual List GetGroupsFromLDAPDirectory(LDAPConnection ldapConnection, OperationContext currentContext, IEnumerable groupsCTConfig) - { - List groups = new List(); - if (groupsCTConfig == null || groupsCTConfig.Count() == 0) { return groups; } -#pragma warning disable CS0618 // Type or member is obsolete - SetLDAPConnection(currentContext, ldapConnection); -#pragma warning restore CS0618 // Type or member is obsolete - string ldapFilter = string.Format("(&(ObjectClass={0}) ({1}={2}){3})", IdentityClaimTypeConfig.LDAPClass, IdentityClaimTypeConfig.LDAPAttribute, currentContext.IncomingEntity.Value, IdentityClaimTypeConfig.AdditionalLDAPFilter); - string logMessageCredentials = String.IsNullOrWhiteSpace(ldapConnection.Directory.Username) ? "process identity" : ldapConnection.Directory.Username; - string directoryDetails = $"from LDAP server \"{ldapConnection.Directory.Path}\" with LDAP filter \"{ldapFilter}\" (authenticate as \"{logMessageCredentials}\" with AuthenticationType \"{ldapConnection.Directory.AuthenticationType}\")."; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Getting LDAP groups of user \"{currentContext.IncomingEntity.Value}\" {directoryDetails}", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - using (new SPMonitoredScope($"[{ProviderInternalName}] Get LDAP groups of user \"{currentContext.IncomingEntity.Value}\" {directoryDetails}", 1000)) - { - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - try - { - using (DirectorySearcher searcher = new DirectorySearcher(ldapConnection.Directory)) - { - searcher.ClientTimeout = new TimeSpan(0, 0, this.CurrentConfiguration.LDAPQueryTimeout); // Set the timeout of the query - searcher.Filter = ldapFilter; - foreach (string memberOfPropertyName in ldapConnection.GroupMembershipLDAPAttributes) - { - searcher.PropertiesToLoad.Add(memberOfPropertyName); - } - foreach (ClaimTypeConfig groupCTConfig in groupsCTConfig) - { - searcher.PropertiesToLoad.Add(groupCTConfig.LDAPAttribute); - } - - SearchResult result; - using (new SPMonitoredScope($"[{ProviderInternalName}] Get group membership of user \"{currentContext.IncomingEntity.Value}\" {directoryDetails}", 1000)) - { - result = searcher.FindOne(); - } - - if (result == null) - { - stopWatch.Stop(); - return groups; // User was not found in this LDAP server - } - - using (new SPMonitoredScope($"[{ProviderInternalName}] Process LDAP groups of user \"{currentContext.IncomingEntity.Value}\" returned {directoryDetails}", 1000)) - { - foreach (ClaimTypeConfig groupCTConfig in groupsCTConfig) - { - int propertyCount = 0; - ResultPropertyValueCollection groupValues = null; - bool valueIsDistinguishedNameFormat; - if (groupCTConfig.ClaimType == MainGroupClaimTypeConfig.ClaimType) - { - valueIsDistinguishedNameFormat = true; - foreach (string groupMembershipAttributes in ldapConnection.GroupMembershipLDAPAttributes) - { - if (result.Properties.Contains(groupMembershipAttributes)) - { - propertyCount = result.Properties[groupMembershipAttributes].Count; - groupValues = result.Properties[groupMembershipAttributes]; - break; - } - } - } - else - { - valueIsDistinguishedNameFormat = false; - if (result.Properties.Contains(groupCTConfig.LDAPAttribute)) - { - propertyCount = result.Properties[groupCTConfig.LDAPAttribute].Count; - groupValues = result.Properties[groupCTConfig.LDAPAttribute]; - } - } - - if (groupValues == null) { continue; } - string value; - for (int propertyCounter = 0; propertyCounter < propertyCount; propertyCounter++) - { - value = groupValues[propertyCounter].ToString(); - string claimValue; - if (valueIsDistinguishedNameFormat) - { - claimValue = OperationContext.GetValueFromDistinguishedName(value); - if (String.IsNullOrEmpty(claimValue)) { continue; } - - string groupDomainName, groupDomainFqdn; - OperationContext.GetDomainInformation(value, out groupDomainName, out groupDomainFqdn); - if (!String.IsNullOrEmpty(groupCTConfig.ClaimValuePrefix) && groupCTConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME)) - { - claimValue = groupCTConfig.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME, groupDomainName) + claimValue; - } - else if (!String.IsNullOrEmpty(groupCTConfig.ClaimValuePrefix) && groupCTConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN)) - { - claimValue = groupCTConfig.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN, groupDomainFqdn) + claimValue; - } - } - else - { - claimValue = value; - } - - SPClaim claim = CreateClaim(groupCTConfig.ClaimType, claimValue, groupCTConfig.ClaimValueType, false); - groups.Add(claim); - } - } - } - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"while getting LDAP groups of user \"{currentContext.IncomingEntity.Value}\" {directoryDetails}", TraceCategory.Augmentation, ex); - } - finally - { - if (ldapConnection.Directory != null) - { - ldapConnection.Directory.Dispose(); - } - stopWatch.Stop(); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Got {groups.Count} group(s) for user \"{currentContext.IncomingEntity.Value}\" in {stopWatch.ElapsedMilliseconds.ToString()} ms {directoryDetails}", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); - - } - } - return groups; - } - - /// - /// the type of SPClaimEntityTypes that this provider support, such as SPClaimEntityTypes.User or SPClaimEntityTypes.FormsRole - /// - /// - protected override void FillEntityTypes(List entityTypes) - { - entityTypes.Add(SPClaimEntityTypes.User); - entityTypes.Add(ClaimsProviderConstants.GroupClaimEntityType); - } - - /// - /// Populates the list of attributes in the left side of the people picker - /// - /// the current web application - /// the type of SPClaimEntityTypes we should return - /// - /// - /// - protected override void FillHierarchy(Uri context, string[] entityTypes, string hierarchyNodeID, int numberOfLevels, Microsoft.SharePoint.WebControls.SPProviderHierarchyTree hierarchy) - { - List aadEntityTypes = new List(); - if (entityTypes.Contains(SPClaimEntityTypes.User)) - { - aadEntityTypes.Add(DirectoryObjectType.User); - } - if (entityTypes.Contains(ClaimsProviderConstants.GroupClaimEntityType)) - { - aadEntityTypes.Add(DirectoryObjectType.Group); - } - - SPSecurity.RunWithElevatedPrivileges(delegate () - { - if (!Initialize(context, entityTypes)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - if (hierarchyNodeID == null) - { - // Root level - foreach (var attribute in ProcessedClaimTypesList.Where(x => !x.UseMainClaimTypeOfDirectoryObject && aadEntityTypes.Contains(x.EntityType))) - { - hierarchy.AddChild( - new Microsoft.SharePoint.WebControls.SPProviderHierarchyNode( - _ProviderInternalName, - attribute.ClaimTypeDisplayName, - attribute.ClaimType, - true)); - } - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in FillHierarchy", TraceCategory.Claims_Picking, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - }); - } - - /// - /// Override this method to change / remove entities created by LDAPCP, or add new ones - /// - /// - /// - /// - /// List of entities created by LDAPCP - protected virtual void FillEntities(Uri context, string[] entityTypes, string input, ref List resolved) - { - } - - protected virtual string AddAttributeToFilter(ClaimTypeConfig attribute, string searchPattern) - { - string filter = String.Empty; - string additionalFilter = String.Empty; - - if (this.CurrentConfiguration.FilterSecurityGroupsOnlyProp && String.Equals(attribute.LDAPClass, "group", StringComparison.OrdinalIgnoreCase)) - { - additionalFilter = LDAPFilterADSecurityGroupsOnly; - } - - if (!String.IsNullOrEmpty(attribute.AdditionalLDAPFilter)) - { - additionalFilter += attribute.AdditionalLDAPFilter; - } - - filter = String.Format(LDAPFilter, attribute.LDAPAttribute, searchPattern, attribute.LDAPClass, additionalFilter); - return filter; - } - - /// - /// Create the SPClaim with proper trust name - /// - /// Claim type - /// Claim value - /// Claim valueType - /// Did the original input contain a keyword? - /// - protected virtual SPClaim CreateClaim(string type, string value, string valueType, bool inputHasKeyword) - { - string claimValue = String.Empty; - //var attr = ProcessedAttributes.Where(x => x.ClaimTypeProp == type).FirstOrDefault(); - var attr = ProcessedClaimTypesList.FirstOrDefault(x => SPClaimTypes.Equals(x.ClaimType, type)); - //if (inputHasKeyword && attr.DoNotAddPrefixIfInputHasKeywordProp) - if ((!inputHasKeyword || !attr.DoNotAddClaimValuePrefixIfBypassLookup) && - !HasPrefixToken(attr.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME) && - !HasPrefixToken(attr.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN) - ) - { - claimValue = attr.ClaimValuePrefix; - } - - claimValue += value; - // SPClaimProvider.CreateClaim issues with SPOriginalIssuerType.ClaimProvider - //return CreateClaim(type, claimValue, valueType); - return new SPClaim(type, claimValue, valueType, IssuerName); - } - - protected virtual PickerEntity CreatePickerEntityHelper(ConsolidatedResult result) - { - PickerEntity pe = CreatePickerEntity(); - SPClaim claim; - string permissionValue = result.Value; - string permissionClaimType = result.ClaimTypeConfig.ClaimType; - bool isIdentityClaimType = false; - - if ((SPClaimTypes.Equals(permissionClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType) - || result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject) && result.ClaimTypeConfig.LDAPClass == IdentityClaimTypeConfig.LDAPClass) - { - isIdentityClaimType = true; - } - - if (result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject && result.ClaimTypeConfig.LDAPClass != IdentityClaimTypeConfig.LDAPClass) - { - // Get reference attribute to use to create actual entity (claim type and its LDAPAttribute) from current result - ClaimTypeConfig attribute = ProcessedClaimTypesList.FirstOrDefault(x => !x.UseMainClaimTypeOfDirectoryObject && x.LDAPClass == result.ClaimTypeConfig.LDAPClass); - if (attribute != null) - { - permissionClaimType = attribute.ClaimType; - result.ClaimTypeConfig.ClaimType = attribute.ClaimType; - result.ClaimTypeConfig.EntityType = attribute.EntityType; - result.ClaimTypeConfig.ClaimTypeDisplayName = attribute.ClaimTypeDisplayName; - permissionValue = result.LDAPResults[attribute.LDAPAttribute][0].ToString(); // Pick value of current result from actual LDAP attribute to use (which is not the LDAP attribute that matches input) - result.ClaimTypeConfig.LDAPAttributeToShowAsDisplayText = attribute.LDAPAttributeToShowAsDisplayText; - result.ClaimTypeConfig.ClaimValuePrefix = attribute.ClaimValuePrefix; - result.ClaimTypeConfig.PrefixToBypassLookup = attribute.PrefixToBypassLookup; - } - } - - if (result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject && result.ClaimTypeConfig.LDAPClass == IdentityClaimTypeConfig.LDAPClass) - { - // This attribute is not directly linked to a claim type, so entity is created with identity claim type - permissionClaimType = IdentityClaimTypeConfig.ClaimType; - permissionValue = FormatPermissionValue(permissionClaimType, result.LDAPResults[IdentityClaimTypeConfig.LDAPAttribute][0].ToString(), result.DomainName, result.DomainFQDN, isIdentityClaimType, result); - claim = CreateClaim( - permissionClaimType, - permissionValue, - IdentityClaimTypeConfig.ClaimValueType, - false); - pe.EntityType = IdentityClaimTypeConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; - } - else - { - permissionValue = FormatPermissionValue(result.ClaimTypeConfig.ClaimType, permissionValue, result.DomainName, result.DomainFQDN, isIdentityClaimType, result); - claim = CreateClaim( - permissionClaimType, - permissionValue, - result.ClaimTypeConfig.ClaimValueType, - false); - pe.EntityType = result.ClaimTypeConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; - } - - int nbMetadata = 0; - // Populate metadata of new PickerEntity - // Change condition to fix bug http://ldapcp.codeplex.com/discussions/653087: only rely on the LDAP class - foreach (ClaimTypeConfig ctConfig in MetadataConfig.Where(x => String.Equals(x.LDAPClass, result.ClaimTypeConfig.LDAPClass, StringComparison.InvariantCultureIgnoreCase))) - { - // if there is actally a value in the LDAP result, then it can be set - if (result.LDAPResults.Contains(ctConfig.LDAPAttribute) && result.LDAPResults[ctConfig.LDAPAttribute].Count > 0) - { - pe.EntityData[ctConfig.EntityDataKey] = result.LDAPResults[ctConfig.LDAPAttribute][0].ToString(); - nbMetadata++; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Set metadata '{ctConfig.EntityDataKey}' of new entity to '{pe.EntityData[ctConfig.EntityDataKey]}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - } - - pe.Claim = claim; - pe.IsResolved = true; - pe.EntityGroupName = this.CurrentConfiguration.PickerEntityGroupNameProp; - pe.Description = String.Format( - EntityOnMouseOver, - result.ClaimTypeConfig.LDAPAttribute, - result.Value); - - pe.DisplayText = FormatPermissionDisplayText(pe, isIdentityClaimType, result); - - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created entity: display text: '{pe.DisplayText}', value: '{pe.Claim.Value}', claim type: '{pe.Claim.ClaimType}', and filled with {nbMetadata.ToString()} metadata.", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - return pe; - } - - /// - /// Override this method to customize value of entity created. - /// - /// - /// - /// - /// - protected virtual string FormatPermissionValue(string claimType, string claimValue, string domainName, string domainFQDN, bool isIdentityClaimType, ConsolidatedResult result) - { - string value = claimValue; - - var attr = ProcessedClaimTypesList.FirstOrDefault(x => SPClaimTypes.Equals(x.ClaimType, claimType)); - if (HasPrefixToken(attr.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME)) - { - value = string.Format("{0}{1}", attr.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME, domainName), value); - } - - if (HasPrefixToken(attr.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN)) - { - value = string.Format("{0}{1}", attr.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN, domainFQDN), value); - } - - return value; - } - - private static bool HasPrefixToken(string prefix, string tokenToSearch) - { - return prefix != null && prefix.Contains(tokenToSearch); - } - - /// - /// Override this method to customize display text of entity created - /// - /// - /// - /// - /// Display text of entity - protected virtual string FormatPermissionDisplayText(PickerEntity entity, bool isIdentityClaimType, ConsolidatedResult result) - { - string entityDisplayText = this.CurrentConfiguration.EntityDisplayTextPrefix; - string claimValue = entity.Claim.Value; - string valueDisplayedInPermission = String.Empty; - bool displayLdapMatchForIdentityClaimType = false; - string prefixToAdd = string.Empty; - - if (result.LDAPResults == null) - { - // Result does not come from a LDAP server, it was created manually - if (isIdentityClaimType) - { - entityDisplayText += claimValue; - } - else - { - entityDisplayText += String.Format(EntityDisplayText, result.ClaimTypeConfig.ClaimTypeDisplayName, claimValue); - } - } - else - { - if (HasPrefixToken(result.ClaimTypeConfig.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME)) - { - prefixToAdd = string.Format("{0}", result.ClaimTypeConfig.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME, result.DomainName)); - } - - if (HasPrefixToken(result.ClaimTypeConfig.ClaimValuePrefix, ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN)) - { - prefixToAdd = string.Format("{0}", result.ClaimTypeConfig.ClaimValuePrefix.Replace(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN, result.DomainFQDN)); - } - - if (isIdentityClaimType) - { - displayLdapMatchForIdentityClaimType = this.CurrentConfiguration.DisplayLdapMatchForIdentityClaimTypeProp; - } - - if (!String.IsNullOrEmpty(result.ClaimTypeConfig.LDAPAttributeToShowAsDisplayText) && result.LDAPResults.Contains(result.ClaimTypeConfig.LDAPAttributeToShowAsDisplayText)) - { // AttributeHelper is set to use a specific LDAP attribute as display text of entity - if (!isIdentityClaimType && result.ClaimTypeConfig.ShowClaimNameInDisplayText) - { - entityDisplayText += "(" + result.ClaimTypeConfig.ClaimTypeDisplayName + ") "; - } - entityDisplayText += prefixToAdd; - valueDisplayedInPermission = result.LDAPResults[result.ClaimTypeConfig.LDAPAttributeToShowAsDisplayText][0].ToString(); - entityDisplayText += valueDisplayedInPermission; - } - else - { // AttributeHelper is set to use its actual LDAP attribute as display text of entity - if (!isIdentityClaimType) - { - valueDisplayedInPermission = claimValue.StartsWith(prefixToAdd) ? claimValue : prefixToAdd + claimValue; - if (result.ClaimTypeConfig.ShowClaimNameInDisplayText) - { - entityDisplayText += String.Format( - EntityDisplayText, - result.ClaimTypeConfig.ClaimTypeDisplayName, - valueDisplayedInPermission); - } - else - { - entityDisplayText = valueDisplayedInPermission; - } - } - else - { // Always specifically use LDAP attribute of identity claim type - entityDisplayText += prefixToAdd; - valueDisplayedInPermission = result.LDAPResults[IdentityClaimTypeConfig.LDAPAttribute][0].ToString(); - entityDisplayText += valueDisplayedInPermission; - } - } - - // Check if LDAP value that actually resolved this result should be included in the display text of the entity - if (displayLdapMatchForIdentityClaimType && result.LDAPResults.Contains(result.ClaimTypeConfig.LDAPAttribute) - && !String.Equals(valueDisplayedInPermission, claimValue, StringComparison.InvariantCultureIgnoreCase)) - { - entityDisplayText += String.Format(" ({0})", claimValue); - } - } - return entityDisplayText; - } - - /// - /// Create a PickerEntity of the input for the claim type specified in parameter - /// - /// Value of the entity - /// claim type of the entity - /// Did the original input contain a keyword? - /// - protected virtual PickerEntity CreatePickerEntityForSpecificClaimType(string input, ClaimTypeConfig ctConfig, bool inputHasKeyword) - { - List entities = CreatePickerEntityForSpecificClaimTypes( - input, - new List() - { - ctConfig, - }, - inputHasKeyword); - return entities == null ? null : entities.First(); - } - - /// - /// Create a PickerEntity of the input for each claim type specified in parameter - /// - /// Value of the entity - /// claim types of the entity - /// Did the original input contain a keyword? - /// - protected virtual List CreatePickerEntityForSpecificClaimTypes(string input, IEnumerable ctConfigs, bool inputHasKeyword) - { - List entities = new List(); - foreach (ClaimTypeConfig ctConfig in ctConfigs) - { - SPClaim claim = CreateClaim(ctConfig.ClaimType, input, ctConfig.ClaimValueType, inputHasKeyword); - PickerEntity pe = CreatePickerEntity(); - pe.Claim = claim; - pe.IsResolved = true; - pe.EntityType = ctConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; - pe.EntityGroupName = this.CurrentConfiguration.PickerEntityGroupNameProp; - pe.Description = String.Format(EntityOnMouseOver, ctConfig.LDAPAttribute, input); - - if (!String.IsNullOrEmpty(ctConfig.EntityDataKey)) - { - pe.EntityData[ctConfig.EntityDataKey] = pe.Claim.Value; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Set metadata '{ctConfig.EntityDataKey}' of new entity to '{pe.EntityData[ctConfig.EntityDataKey]}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - - ConsolidatedResult result = new ConsolidatedResult(); - result.ClaimTypeConfig = ctConfig; - result.Value = input; - bool isIdentityClaimType = SPClaimTypes.Equals(claim.ClaimType, IdentityClaimTypeConfig.ClaimType); - pe.DisplayText = FormatPermissionDisplayText(pe, isIdentityClaimType, result); - - entities.Add(pe); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created entity: display text: '{pe.DisplayText}', value: '{pe.Claim.Value}', claim type: '{pe.Claim.ClaimType}'.", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - return entities.Count > 0 ? entities : null; - } - - protected override void FillSchema(Microsoft.SharePoint.WebControls.SPProviderSchema schema) - { - // Not implemented - I didn't identify the purpose of this method - } - - public override string Name => ProviderInternalName; - public override bool SupportsEntityInformation => true; - public override bool SupportsHierarchy => true; - public override bool SupportsResolve => true; - public override bool SupportsSearch => true; - public override bool SupportsUserKey => true; - - /// - /// Return the identity claim type - /// - /// Identity claim type. Should not return null to prevent exceptions in SharePoint when users sign-in - public override string GetClaimTypeForUserKey() - { - // Elevation of privileges when calling LDAPCP.Initialize is very important to prevent issue https://github.com/Yvand/LDAPCP/issues/87 - // But calling SPSecurity.RunWithElevatedPrivileges here is not possible as it causes a StackOverflowException - Initialize(null, null); - - this.Lock_Config.EnterReadLock(); - try - { - if (SPTrust == null) - { - return String.Empty; - } - - return SPTrust.IdentityClaimTypeInformation.MappedClaimType; - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in GetClaimTypeForUserKey", TraceCategory.Rehydration, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - return null; - } - - /// - /// Return the user key (SPClaim with identity claim type) from the incoming entity - /// - /// SPClaim corresponding to the user key of the incoming entity. Should not return null to prevent exceptions in SharePoint when users sign-in - /// - protected override SPClaim GetUserKeyForEntity(SPClaim entity) - { - bool initSucceeded = false; - - // Elevation of privileges when calling LDAPCP.Initialize is very important to prevent issue https://github.com/Yvand/LDAPCP/issues/87 - // But doing elevation of privileges here causes issue https://github.com/Yvand/LDAPCP/issues/99 in some environments (I could not repro) - //SPSecurity.RunWithElevatedPrivileges(delegate () - //{ - initSucceeded = Initialize(null, null); - //}); - - this.Lock_Config.EnterReadLock(); - try - { - // If initialization failed but SPTrust is not null, rest of the method can be executed normally - // Otherwise return the entity - if (!initSucceeded && SPTrust == null) - { - return entity; - } - - // There are 2 scenarios: - // 1: OriginalIssuer is "SecurityTokenService": Value looks like "05.t|yvanhost|yvand@yvanhost.local", claim type is "http://schemas.microsoft.com/sharepoint/2009/08/claims/userid" and it must be decoded properly - // 2: OriginalIssuer is LDAPCP: in this case incoming entity is valid and returned as is - if (String.Equals(entity.OriginalIssuer, IssuerName, StringComparison.InvariantCultureIgnoreCase)) - { - return entity; - } - - SPClaimProviderManager cpm = SPClaimProviderManager.Local; - SPClaim curUser = SPClaimProviderManager.DecodeUserIdentifierClaim(entity); - - ClaimsProviderLogging.Log(String.Format("[{0}] Return user key for user \"{1}\"", ProviderInternalName, entity.Value), - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Rehydration); - return CreateClaim(SPTrust.IdentityClaimTypeInformation.MappedClaimType, curUser.Value, curUser.ValueType); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in GetUserKeyForEntity", TraceCategory.Rehydration, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - return null; - } - - protected override void FillDefaultLocalizedDisplayName(System.Globalization.CultureInfo culture, out string localizedName) - { - if (SPTrust != null) - { - localizedName = SPTrust.DisplayName; - } - else - { - base.FillDefaultLocalizedDisplayName(culture, out localizedName); - } - } - } - - public class ConsolidatedResult - { - public ClaimTypeConfig ClaimTypeConfig; - public PickerEntity PickerEntity; - public ResultPropertyCollection LDAPResults; - public int nbMatch = 0; - public string Value; - public string DomainName; - public string DomainFQDN; - //public string DEBUG; - } - - public class LDAPSearchResult - { - public SearchResult SearchResult; - public string DomainName; - public string DomainFQDN; - } - - public class ConsolidatedResultCollection : Collection - { - /// - /// Compare 2 results to not add duplicates - /// they are identical if they have the same claim type and same value - /// - /// LDAP result to compare - /// AttributeHelper that matches result - /// if true, don't consider 2 results as identical if they don't are in same domain. - /// - public bool Contains(LDAPSearchResult result, ClaimTypeConfig attribute, bool compareWithDomain) - { - foreach (var item in base.Items) - { - if (item.ClaimTypeConfig.ClaimType != attribute.ClaimType) { continue; } - - if (!item.LDAPResults.Contains(attribute.LDAPAttribute)) { continue; } - - // if compareWithDomain is true, don't consider 2 results as identical if they don't are in same domain - // Using same bool to compare both DomainName and DomainFQDN causes scenario below to potentially generate duplicates: - // result.DomainName == item.DomainName BUT result.DomainFQDN != item.DomainFQDN AND value of claim is created with DomainName token - // If so, compareWithDomain will be true and test below will be true so duplicates won't be check, even though it would be possible. - // But this would be so unlikely that this scenario can be ignored - if (compareWithDomain && ( - !String.Equals(item.DomainName, result.DomainName, StringComparison.InvariantCultureIgnoreCase) || - !String.Equals(item.DomainFQDN, result.DomainFQDN, StringComparison.InvariantCultureIgnoreCase) - )) - { - continue; // They don't are in same domain, so not identical, jump to next item - } - - if (String.Equals(item.LDAPResults[attribute.LDAPAttribute][0].ToString(), result.SearchResult.Properties[attribute.LDAPAttribute][0].ToString(), StringComparison.InvariantCultureIgnoreCase)) - { - item.nbMatch++; - return true; - } - } - return false; - } - } -} diff --git a/LDAPCP/LDAPCP.csproj b/LDAPCP/LDAPCP.csproj deleted file mode 100644 index ccf6651..0000000 --- a/LDAPCP/LDAPCP.csproj +++ /dev/null @@ -1,148 +0,0 @@ - - - - - Debug - AnyCPU - {66F4B88D-7D7E-4435-92DE-94810E2B8F9F} - Library - Properties - ldapcp - ldapcp - v4.6.2 - 19.0 - 512 - {C1CDDADD-2546-481F-9697-4EA41081F2FC};{14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - False - SAK - SAK - SAK - SAK - - SharePointCustomization - 14.1 - 15.0 - 40 - C:\YvanData\dev\LDAPCP\Backup1\ - 12.0 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - AnyCPU - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - true - - - LDAPCP.snk - - - - - - - - - - - - - references\SPS2013\Microsoft.SharePoint.dll - False - - - - - ClaimTypesConfig.ascx - ASPXCodeBehind - - - ClaimTypesConfig.ascx.cs - - - GlobalSettings.ascx - ASPXCodeBehind - - - GlobalSettings.ascx.cs - - - - - LDAPCP.feature - - - - - ASPXCodeBehind - - - - - - {f66ab842-cba9-4e8b-994a-082609fa04b9} - - - {64451824-37fb-4ecb-8c3c-2501826bd17e} - - - {44294c3e-7072-44ba-8a9e-4a2a2e6d3de8} - - - {6a2152a5-0941-4e3a-abee-d771c813bdce} - - - {48070593-e687-4f19-8920-d28f17368f37} - - - - {6ff99767-b95a-4143-8cd8-3978aa01fb80} - - - Package.package - - - - - - - - - - LDAPCP.Administration.feature - - - LDAPCP.feature - - - - - Designer - - - - - 11.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\gacutil.exe" /f /i "$(TargetPath)" - - \ No newline at end of file diff --git a/LDAPCP/LDAPCPConfig.cs b/LDAPCP/LDAPCPConfig.cs deleted file mode 100644 index 7d6d84c..0000000 --- a/LDAPCP/LDAPCPConfig.cs +++ /dev/null @@ -1,1375 +0,0 @@ -using Microsoft.SharePoint; -using Microsoft.SharePoint.Administration; -using Microsoft.SharePoint.Administration.Claims; -using Microsoft.SharePoint.WebControls; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.DirectoryServices; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Web; -using static ldapcp.ClaimsProviderLogging; -using WIF4_5 = System.Security.Claims; - -namespace ldapcp -{ - public interface ILDAPCPConfiguration - { - List LDAPConnectionsProp { get; set; } - ClaimTypeConfigCollection ClaimTypes { get; set; } - bool BypassLDAPLookup { get; set; } - bool AddWildcardAsPrefixOfInput { get; set; } - bool DisplayLdapMatchForIdentityClaimTypeProp { get; set; } - string PickerEntityGroupNameProp { get; set; } - bool FilterEnabledUsersOnlyProp { get; set; } - bool FilterSecurityGroupsOnlyProp { get; set; } - bool FilterExactMatchOnlyProp { get; set; } - int LDAPQueryTimeout { get; set; } - bool CompareResultsWithDomainNameProp { get; set; } - bool EnableAugmentation { get; set; } - string MainGroupClaimType { get; set; } - string EntityDisplayTextPrefix { get; set; } - string CustomData { get; set; } - int MaxSearchResultsCount { get; set; } - } - - public class ClaimsProviderConstants - { - public static string CONFIG_ID => "5D306A02-A262-48AC-8C44-BDB927620227"; - public static string CONFIG_NAME => "LdapcpConfig"; - public static string LDAPCPCONFIG_TOKENDOMAINNAME => "{domain}"; - public static string LDAPCPCONFIG_TOKENDOMAINFQDN => "{fqdn}"; - public static int LDAPCPCONFIG_TIMEOUT => 10; - public static string GroupClaimEntityType => SPClaimEntityTypes.FormsRole; - public static bool EnforceOnly1ClaimTypeForGroup => false; // In LDAPCP, multiple claim types can be used to create group permissions - public static string DefaultMainGroupClaimType => WIF4_5.ClaimTypes.Role; - public static string PUBLICSITEURL => "https://ldapcp.com"; - private static object Sync_SetClaimsProviderVersion = new object(); - private static string _ClaimsProviderVersion; - public static string ClaimsProviderVersion - { - get - { - if (!String.IsNullOrEmpty(_ClaimsProviderVersion)) - { - return _ClaimsProviderVersion; - } - - // Method FileVersionInfo.GetVersionInfo() may hang and block all LDAPCP threads, so it is read only 1 time - lock (Sync_SetClaimsProviderVersion) - { - if (!String.IsNullOrEmpty(_ClaimsProviderVersion)) - { - return _ClaimsProviderVersion; - } - - try - { - _ClaimsProviderVersion = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LDAPCP)).Location).FileVersion; - } - // If assembly was removed from the GAC, CLR throws a FileNotFoundException - catch (System.IO.FileNotFoundException) - { - // Current process will never detect if assembly is added to the GAC later, which is fine - _ClaimsProviderVersion = " "; - } - return _ClaimsProviderVersion; - } - } - } - - /// - /// Escape characters to use for special characters in LDAP filters, as documented in https://ldap.com/ldap-filters/ - /// - public static Dictionary SpecialCharacters - { - get => _SpecialCharacters; - set => _SpecialCharacters = value; - } - private static Dictionary _SpecialCharacters = new Dictionary - { - { @"\", @"\5C" }, // '\' must be the 1st to be evaluated because '\' is also present in the escape characters themselves - { "*", @"\2A" }, - { "(", @"\28" }, - { ")", @"\29" }, - }; - } - - public class LDAPCPConfig : SPPersistedObject, ILDAPCPConfiguration - { - /// - /// List of LDAP servers to query - /// - public List LDAPConnectionsProp - { - get => LDAPConnections; - set => LDAPConnections = value; - } - [Persisted] - private List LDAPConnections; - - /// - /// Configuration of claim types and their mapping with LDAP attribute/class - /// - public ClaimTypeConfigCollection ClaimTypes - { - get - { - if (_ClaimTypes == null) - { - _ClaimTypes = new ClaimTypeConfigCollection(ref this._ClaimTypesCollection); - } - //else - //{ - // _ClaimTypesCollection = _ClaimTypes.innerCol; - //} - return _ClaimTypes; - } - set - { - _ClaimTypes = value; - _ClaimTypesCollection = value == null ? null : value.innerCol; - } - } - [Persisted] - private Collection _ClaimTypesCollection; - - private ClaimTypeConfigCollection _ClaimTypes; - - /// - /// If true, LDAPCP will validate the input as is, with no LDAP query - /// - public bool BypassLDAPLookup - { - get => AlwaysResolveUserInput; - set => AlwaysResolveUserInput = value; - } - [Persisted] - private bool AlwaysResolveUserInput; - - /// - /// NOT RECOMMENDED: Change filter to query "*input*" instead of "input*". This may have a strong negative impact on performance - /// - public bool AddWildcardAsPrefixOfInput - { - get => AddWildcardInFrontOfQuery; - set => AddWildcardInFrontOfQuery = value; - } - [Persisted] - private bool AddWildcardInFrontOfQuery; - - public bool DisplayLdapMatchForIdentityClaimTypeProp - { - get => DisplayLdapMatchForIdentityClaimType; - set => DisplayLdapMatchForIdentityClaimType = value; - } - [Persisted] - private bool DisplayLdapMatchForIdentityClaimType; - - public string PickerEntityGroupNameProp - { - get => PickerEntityGroupName; - set => PickerEntityGroupName = value; - } - [Persisted] - private string PickerEntityGroupName; - - public bool FilterEnabledUsersOnlyProp - { - get => FilterEnabledUsersOnly; - set => FilterEnabledUsersOnly = value; - } - [Persisted] - private bool FilterEnabledUsersOnly; - - public bool FilterSecurityGroupsOnlyProp - { - get => FilterSecurityGroupsOnly; - set => FilterSecurityGroupsOnly = value; - } - [Persisted] - private bool FilterSecurityGroupsOnly; - - /// - /// If true, LDAPCP will only return results that match exactly the input - /// - public bool FilterExactMatchOnlyProp - { - get => FilterExactMatchOnly; - set => FilterExactMatchOnly = value; - } - [Persisted] - private bool FilterExactMatchOnly; - - /// - /// Timeout in seconds to wait for LDAP to return the result - /// - public int LDAPQueryTimeout - { - get => Timeout; - set => Timeout = value; - } - [Persisted] - private int Timeout; - - /// - /// Should we care about the LDAP netbios name to consider 2 results as identical - /// - public bool CompareResultsWithDomainNameProp - { - get => CompareResultsWithDomainName; - set => CompareResultsWithDomainName = value; - } - [Persisted] - private bool CompareResultsWithDomainName = false; - - /// - /// Set to true to enable augmentation. Property MainGroupClaimType must also be set for augmentation to work. - /// - public bool EnableAugmentation - { - get => AugmentationEnabled; - set => AugmentationEnabled = value; - } - [Persisted] - private bool AugmentationEnabled; - - /// - /// Set the claim type that LDAPCP will use to create claims with the group membership of users during augmentation - /// - public string MainGroupClaimType - { - get => AugmentationClaimType; - set => AugmentationClaimType = value; - } - [Persisted] - private string AugmentationClaimType; - - public string EntityDisplayTextPrefix - { - get => _EntityDisplayTextPrefix; - set => _EntityDisplayTextPrefix = value; - } - [Persisted] - private string _EntityDisplayTextPrefix; - - /// - /// Name of the SPTrustedLoginProvider where LDAPCP is enabled - /// - [Persisted] - public string SPTrustName; - - private SPTrustedLoginProvider _SPTrust; - private SPTrustedLoginProvider SPTrust - { - get - { - if (_SPTrust == null) - { - _SPTrust = SPSecurityTokenServiceManager.Local.TrustedLoginProviders.GetProviderByName(SPTrustName); - } - return _SPTrust; - } - } - - [Persisted] - private string ClaimsProviderVersion; - - /// - /// This property is not used by AzureCP and is available to developers for their own needs - /// - public string CustomData - { - get => _CustomData; - set => _CustomData = value; - } - [Persisted] - private string _CustomData; - - /// - /// Limit number of results returned to SharePoint during a search - /// - public int MaxSearchResultsCount - { - get => _MaxSearchResultsCount; - set => _MaxSearchResultsCount = value; - } - [Persisted] - private int _MaxSearchResultsCount = 30; // SharePoint sets maxCount to 30 in method FillSearch - - public LDAPCPConfig(string persistedObjectName, SPPersistedObject parent, string spTrustName) : base(persistedObjectName, parent) - { - this.SPTrustName = spTrustName; - } - - public LDAPCPConfig() { } - - /// - /// Override this method to allow more users to update the object. True specifies that more users can update the object; otherwise, false. The default value is false. - /// - /// - protected override bool HasAdditionalUpdateAccess() - { - return false; - } - - /// - /// Returns the configuration of LDAPCP - /// - /// - public static LDAPCPConfig GetConfiguration() - { - return GetConfiguration(ClaimsProviderConstants.CONFIG_NAME, String.Empty); - } - - /// - /// Returns the configuration of LDAPCP - /// - /// - /// - public static LDAPCPConfig GetConfiguration(string persistedObjectName) - { - return GetConfiguration(persistedObjectName, String.Empty); - } - - /// - /// Returns the configuration of LDAPCP - /// - /// Name of the configuration - /// Name of the SPTrustedLoginProvider using the claims provider - public static LDAPCPConfig GetConfiguration(string persistedObjectName, string spTrustName) - { - SPPersistedObject parent = SPFarm.Local; - try - { - LDAPCPConfig persistedObject = parent.GetChild(persistedObjectName); - if (persistedObject != null) - { - persistedObject.CheckAndCleanConfiguration(spTrustName); - return persistedObject; - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(String.Empty, $"while retrieving configuration '{persistedObjectName}'", TraceCategory.Configuration, ex); - } - return null; - } - - /// - /// Commit changes to configuration database - /// - public override void Update() - { - // In case ClaimTypes collection was modified, test if it is still valid before committed changes to database - try - { - ClaimTypeConfigCollection testUpdateCollection = new ClaimTypeConfigCollection(); - testUpdateCollection.SPTrust = this.SPTrust; - foreach (ClaimTypeConfig curCTConfig in this.ClaimTypes) - { - testUpdateCollection.Add(curCTConfig, false); - } - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Some changes made to list ClaimTypes are invalid and cannot be committed to configuration database. Inspect inner exception for more details about the error.", ex); - } - - base.Update(); - ClaimsProviderLogging.Log($"Configuration '{base.DisplayName}' was updated successfully to version {base.Version} in configuration database.", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); - } - - public static LDAPCPConfig ResetConfiguration(string persistedObjectName) - { - LDAPCPConfig previousConfig = GetConfiguration(persistedObjectName, String.Empty); - if (previousConfig == null) { return null; } - Guid configId = previousConfig.Id; - string spTrustName = previousConfig.SPTrustName; - DeleteConfiguration(persistedObjectName); - LDAPCPConfig newConfig = CreateConfiguration(configId.ToString(), persistedObjectName, spTrustName); - ClaimsProviderLogging.Log($"Configuration '{persistedObjectName}' was successfully reset to its default configuration", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - return newConfig; - } - - /// - /// Set properties of current configuration to their default values - /// - /// - public void ResetCurrentConfiguration() - { - LDAPCPConfig defaultConfig = ReturnDefaultConfiguration(this.SPTrustName) as LDAPCPConfig; - ApplyConfiguration(defaultConfig); - CheckAndCleanConfiguration(String.Empty); - } - - /// - /// Apply configuration in parameter to current object. It does not copy SharePoint base class members - /// - /// - public void ApplyConfiguration(LDAPCPConfig configToApply) - { - // Copy non-inherited public properties - PropertyInfo[] propertiesToCopy = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (PropertyInfo property in propertiesToCopy) - { - if (property.CanWrite) - { - object value = property.GetValue(configToApply); - if (value != null) - { - property.SetValue(this, value); - } - } - } - - // Member SPTrustName is not exposed through a property, so it must be set explicitly - this.SPTrustName = configToApply.SPTrustName; - } - - /// - /// Returns a copy of the current object. This copy does not have any member of the base SharePoint base class set - /// - /// - public LDAPCPConfig CopyConfiguration() - { - // Cannot use reflection here to copy object because of the calls to methods CopyConfiguration() on some properties - LDAPCPConfig copy = new LDAPCPConfig(); - copy.SPTrustName = this.SPTrustName; - copy.LDAPConnectionsProp = new List(); - foreach (LDAPConnection currentCoco in this.LDAPConnectionsProp) - { - copy.LDAPConnectionsProp.Add(currentCoco.CopyConfiguration()); - } - copy.ClaimTypes = new ClaimTypeConfigCollection(); - foreach (ClaimTypeConfig currentObject in this.ClaimTypes) - { - copy.ClaimTypes.Add(currentObject.CopyConfiguration(), false); - } - copy.BypassLDAPLookup = this.BypassLDAPLookup; - copy.AddWildcardAsPrefixOfInput = this.AddWildcardAsPrefixOfInput; - copy.DisplayLdapMatchForIdentityClaimTypeProp = this.DisplayLdapMatchForIdentityClaimTypeProp; - copy.PickerEntityGroupNameProp = this.PickerEntityGroupNameProp; - copy.FilterEnabledUsersOnlyProp = this.FilterEnabledUsersOnlyProp; - copy.FilterSecurityGroupsOnlyProp = this.FilterSecurityGroupsOnlyProp; - copy.FilterExactMatchOnlyProp = this.FilterExactMatchOnlyProp; - copy.LDAPQueryTimeout = this.LDAPQueryTimeout; - copy.CompareResultsWithDomainNameProp = this.CompareResultsWithDomainNameProp; - copy.EnableAugmentation = this.EnableAugmentation; - copy.MainGroupClaimType = this.MainGroupClaimType; - copy.EntityDisplayTextPrefix = this.EntityDisplayTextPrefix; - copy.CustomData = this.CustomData; - copy.MaxSearchResultsCount = this.MaxSearchResultsCount; - return copy; - } - - public void ResetClaimTypesList() - { - ClaimTypes.Clear(); - ClaimTypes = ReturnDefaultClaimTypesConfig(this.SPTrustName); - MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - ClaimsProviderLogging.Log($"Claim types list of configuration '{Name}' was successfully reset to default configuration", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - } - - /// - /// If LDAPCP is associated with a SPTrustedLoginProvider, create its configuration with default settings and save it into configuration database. If it already exists, it will be replaced. - /// - /// - public static LDAPCPConfig CreateDefaultConfiguration() - { - SPTrustedLoginProvider spTrust = LDAPCP.GetSPTrustAssociatedWithCP(LDAPCP._ProviderInternalName); - if (spTrust == null) - { - return null; - } - else - { - return CreateConfiguration(ClaimsProviderConstants.CONFIG_ID, ClaimsProviderConstants.CONFIG_NAME, spTrust.Name); - } - } - - /// - /// Create a persisted object with default configuration of LDAPCP. If it already exists, it will be deleted. - /// - /// GUID of the configuration, stored as a persisted object into SharePoint configuration database - /// Name of the configuration, stored as a persisted object into SharePoint configuration database - /// Name of the SPTrustedLoginProvider that claims provider is associated with - /// - public static LDAPCPConfig CreateConfiguration(string persistedObjectID, string persistedObjectName, string spTrustName) - { - if (String.IsNullOrEmpty(spTrustName)) - { - throw new ArgumentNullException("spTrustName"); - } - - // Ensure it doesn't already exists and delete it if so - LDAPCPConfig existingConfig = LDAPCPConfig.GetConfiguration(persistedObjectName, String.Empty); - if (existingConfig != null) - { - DeleteConfiguration(persistedObjectName); - } - - ClaimsProviderLogging.Log($"Creating configuration '{persistedObjectName}' with Id {persistedObjectID}...", TraceSeverity.VerboseEx, EventSeverity.Error, TraceCategory.Core); - LDAPCPConfig PersistedObject = new LDAPCPConfig(persistedObjectName, SPFarm.Local, spTrustName); - PersistedObject.ResetCurrentConfiguration(); - PersistedObject.Id = new Guid(persistedObjectID); - PersistedObject.Update(); - ClaimsProviderLogging.Log($"Created configuration '{persistedObjectName}' with Id {PersistedObject.Id}", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); - return PersistedObject; - } - - public static ILDAPCPConfiguration ReturnDefaultConfiguration(string spTrustName) - { - LDAPCPConfig defaultConfig = new LDAPCPConfig(); - defaultConfig.SPTrustName = spTrustName; - defaultConfig.LDAPConnections = ReturnDefaultLDAPConnection(); - defaultConfig.ClaimTypes = ReturnDefaultClaimTypesConfig(spTrustName); - defaultConfig.PickerEntityGroupNameProp = "Results"; - defaultConfig.BypassLDAPLookup = false; - defaultConfig.AddWildcardAsPrefixOfInput = false; - defaultConfig.FilterEnabledUsersOnlyProp = false; - defaultConfig.FilterSecurityGroupsOnlyProp = false; - defaultConfig.FilterExactMatchOnlyProp = false; - defaultConfig.LDAPQueryTimeout = ClaimsProviderConstants.LDAPCPCONFIG_TIMEOUT; - defaultConfig.EnableAugmentation = false; - defaultConfig.MainGroupClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; - return defaultConfig; - } - - /// - /// Generate and return default claim types configuration list - /// - /// - public static ClaimTypeConfigCollection ReturnDefaultClaimTypesConfig(string spTrustName) - { - if (String.IsNullOrWhiteSpace(spTrustName)) - { - throw new ArgumentNullException("spTrustName cannot be null."); - } - - SPTrustedLoginProvider spTrust = SPSecurityTokenServiceManager.Local.TrustedLoginProviders.GetProviderByName(spTrustName); - if (spTrust == null) - { - ClaimsProviderLogging.Log($"SPTrustedLoginProvider '{spTrustName}' was not found ", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - return null; - } - - ClaimTypeConfigCollection newCTConfigCollection = new ClaimTypeConfigCollection - { - // Claim types most liekly to be set as identity claim types - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "mail", ClaimType = WIF4_5.ClaimTypes.Email, EntityDataKey = PeopleEditorEntityDataKeys.Email}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "userPrincipalName", ClaimType = WIF4_5.ClaimTypes.Upn}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "sAMAccountName", ClaimType = WIF4_5.ClaimTypes.WindowsAccountName, AdditionalLDAPFilter = "(!(objectClass=computer))"}, - - // Additional properties to find user and create entity with the identity claim type (UseMainClaimTypeOfDirectoryObject=true) - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "displayName", UseMainClaimTypeOfDirectoryObject = true, EntityDataKey = PeopleEditorEntityDataKeys.DisplayName}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "cn", UseMainClaimTypeOfDirectoryObject = true, AdditionalLDAPFilter = "(!(objectClass=computer))"}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "sn", UseMainClaimTypeOfDirectoryObject = true}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute = "givenName", UseMainClaimTypeOfDirectoryObject = true}, // First name - - // Additional properties to populate metadata of entity created: no claim type set, EntityDataKey is set and UseMainClaimTypeOfDirectoryObject = false (default value) - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute="physicalDeliveryOfficeName", EntityDataKey = PeopleEditorEntityDataKeys.Location}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute="title", EntityDataKey = PeopleEditorEntityDataKeys.JobTitle}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute="msRTCSIP-PrimaryUserAddress", EntityDataKey = PeopleEditorEntityDataKeys.SIPAddress}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, LDAPClass = "user", LDAPAttribute="telephoneNumber", EntityDataKey = PeopleEditorEntityDataKeys.WorkPhone}, - - // Group - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, LDAPClass = "group", LDAPAttribute="sAMAccountName", ClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType, ClaimValuePrefix = @"{fqdn}\"}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, LDAPClass = "group", LDAPAttribute="displayName", UseMainClaimTypeOfDirectoryObject = true, EntityDataKey = PeopleEditorEntityDataKeys.DisplayName}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, LDAPClass = "user", LDAPAttribute="primaryGroupID", ClaimType = WIF4_5.ClaimTypes.PrimaryGroupSid, SupportsWildcard = false}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, LDAPClass = "group", LDAPAttribute="mail", EntityDataKey = PeopleEditorEntityDataKeys.Email}, - }; - newCTConfigCollection.SPTrust = spTrust; - return newCTConfigCollection; - } - - /// - /// Return default LDAP connection list - /// - /// - public static List ReturnDefaultLDAPConnection() - { - return new List - { - new LDAPConnection{ - UseSPServerConnectionToAD = true, - EnableAugmentation = true, - } - }; - } - - /// - /// Delete persisted object from configuration database - /// - /// Name of persisted object to delete - public static void DeleteConfiguration(string persistedObjectName) - { - LDAPCPConfig config = LDAPCPConfig.GetConfiguration(persistedObjectName, String.Empty); - if (config == null) - { - ClaimsProviderLogging.Log($"Configuration '{persistedObjectName}' was not found in configuration database", TraceSeverity.Medium, EventSeverity.Error, TraceCategory.Core); - return; - } - config.Delete(); - ClaimsProviderLogging.Log($"Configuration '{persistedObjectName}' was successfully deleted from configuration database", TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - } - - /// - /// Check if current configuration is compatible with current version of AzureCP, and fix it if not. If object comes from configuration database, changes are committed in configuration database - /// - /// Name of the SPTrust if it changed, null or empty string otherwise - /// Bollean indicates whether the configuration was updated in configuration database - public bool CheckAndCleanConfiguration(string spTrustName) - { - // ClaimsProviderConstants.ClaimsProviderVersion can be null if assembly was removed from GAC - if (String.IsNullOrEmpty(ClaimsProviderConstants.ClaimsProviderVersion)) - { - return false; - } - - bool configUpdated = false; - - if (!String.IsNullOrEmpty(spTrustName) && !String.Equals(this.SPTrustName, spTrustName, StringComparison.InvariantCultureIgnoreCase)) - { - ClaimsProviderLogging.Log($"Updated property SPTrustName from \"{this.SPTrustName}\" to \"{spTrustName}\" in configuration \"{base.DisplayName}\".", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); - this.SPTrustName = spTrustName; - configUpdated = true; - } - - if (!String.Equals(this.ClaimsProviderVersion, ClaimsProviderConstants.ClaimsProviderVersion, StringComparison.InvariantCultureIgnoreCase)) - { - // Detect if current assembly has a version different than AzureCPConfig.ClaimsProviderVersion. If so, config needs a sanity check - ClaimsProviderLogging.Log($"Updated property ClaimsProviderVersion from \"{this.ClaimsProviderVersion}\" to \"{ClaimsProviderConstants.ClaimsProviderVersion}\" in configuration \"{base.DisplayName}\".", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); - this.ClaimsProviderVersion = ClaimsProviderConstants.ClaimsProviderVersion; - configUpdated = true; - } - else if (!String.IsNullOrEmpty(this.SPTrustName)) - { - // ClaimTypeConfigCollection.SPTrust is not persisted so it should always be set explicitely - // Done in "else if" to not set this.ClaimTypes.SPTrust if we are not sure that this.ClaimTypes is in a good state - this.ClaimTypes.SPTrust = this.SPTrust; - } - - // Either claims provider was associated to a new SPTrustedLoginProvider - // Or version of the current assembly changed (upgrade) - // So let's do a sanity check of the configuration - if (configUpdated) - { - try - { - // If LDAPCP is updated from a version < v10, this.ClaimTypes.Count will throw a NullReferenceException - int testClaimTypeCollection = this.ClaimTypes.Count; - } - catch (NullReferenceException) - { - this.ClaimTypes = LDAPCPConfig.ReturnDefaultClaimTypesConfig(this.SPTrustName); - configUpdated = true; - } - - if (this.LDAPConnectionsProp == null) - { - // LDAPConnections was introduced in v2.1 (SP2013). if LDAPCP is updated from an earlier version, LDAPConnections doesn't exist yet - this.LDAPConnectionsProp = ReturnDefaultLDAPConnection(); - configUpdated = true; - } - - if (!String.IsNullOrEmpty(this.SPTrustName)) - { - this.ClaimTypes.SPTrust = this.SPTrust; - } - - // Changed in v11: - // Added property SPTrust / SPTrustName (SPTrustName must be set before this function runs to avoid deleting identity claim type) - // Adding 2 times a ClaimTypeConfig with the same EntityType and same LDAPClass/LDAPAttribute now throws an InvalidOperationException - // But this was possible before, so list this.ClaimTypes must be checked to be sure we are not in this scenario, and cleaned if so - foreach (DirectoryObjectType entityType in Enum.GetValues(typeof(DirectoryObjectType))) - { - var duplicatedPropertiesList = this.ClaimTypes.Where(x => x.EntityType == entityType) // Check 1 EntityType - .GroupBy(x => new - { // Group by LDAPClass/LDAPAttribute - x.LDAPClass, - x.LDAPAttribute - }) - .Select(x => new - { - LDAPProperties = x.Key, - ObjectCount = x.Count() // For each LDAPClass/LDAPAttribute, how many items found - }) - .Where(x => x.ObjectCount > 1); // Keep only LDAPClass/LDAPAttribute found more than 1 time (for a given EntityType) - foreach (var duplicatedProperty in duplicatedPropertiesList) - { - ClaimTypeConfig ctConfigToDelete = null; - if (SPTrust != null && entityType == DirectoryObjectType.User) - { - ctConfigToDelete = this.ClaimTypes.FirstOrDefault(x => x.LDAPClass == duplicatedProperty.LDAPProperties.LDAPClass && x.LDAPAttribute == duplicatedProperty.LDAPProperties.LDAPAttribute && x.EntityType == entityType && !String.Equals(x.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)); - } - else if (entityType == DirectoryObjectType.Group && !String.IsNullOrEmpty(this.MainGroupClaimType)) - { - ctConfigToDelete = this.ClaimTypes.FirstOrDefault(x => x.LDAPClass == duplicatedProperty.LDAPProperties.LDAPClass && x.LDAPAttribute == duplicatedProperty.LDAPProperties.LDAPAttribute && x.EntityType == entityType && !String.Equals(x.ClaimType, this.MainGroupClaimType, StringComparison.InvariantCultureIgnoreCase)); - } - else - { - ctConfigToDelete = this.ClaimTypes.FirstOrDefault(x => x.LDAPClass == duplicatedProperty.LDAPProperties.LDAPClass && x.LDAPAttribute == duplicatedProperty.LDAPProperties.LDAPAttribute && x.EntityType == entityType); - } - - this.ClaimTypes.Remove(ctConfigToDelete); - ClaimsProviderLogging.Log($"Removed claim type '{ctConfigToDelete.ClaimType}' from claim types configuration list because it duplicates LDAP attribute {ctConfigToDelete.LDAPAttribute} and LDAP class {ctConfigToDelete.LDAPClass}", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - } - } - - if (Version > 0) - { - try - { - // SPContext may be null if code does not run in a SharePoint process (e.g. in unit tests process) - if (SPContext.Current != null) - { - SPContext.Current.Web.AllowUnsafeUpdates = true; - } - this.Update(); - ClaimsProviderLogging.Log($"Configuration '{this.Name}' was upgraded in configuration database and some settings were updated or reset to their default configuration", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - } - catch (Exception) - { - // It may fail if current user doesn't have permission to update the object in configuration database - ClaimsProviderLogging.Log($"Configuration '{this.Name}' was upgraded locally, but changes could not be applied in configuration database. Please visit admin pages in central administration to upgrade configuration globally.", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - } - finally - { - if (SPContext.Current != null) - { - SPContext.Current.Web.AllowUnsafeUpdates = false; - } - } - } - } - return configUpdated; - } - } - - public class LDAPConnection : SPAutoSerializingObject - { - public Guid Identifier - { - get => Id; - set => Id = value; - } - [Persisted] - private Guid Id = Guid.NewGuid(); - - /// - /// LDAP Path of the connection LDAP://contoso.local:port/DC=contoso,DC=local - /// - public string LDAPPath // Also required as a property for ASP.NET server side controls in admin pages. - { - get => Path; - set => Path = value; - } - [Persisted] - private string Path; - - public string LDAPUsername - { - get => Username; - set => Username = value; - } - [Persisted] - private string Username; - - public string LDAPPassword - { - get => Password; - set => Password = value; - } - [Persisted] - private string Password; - - public string AdditionalMetadata - { - get => Metadata; - set => Metadata = value; - } - [Persisted] - private string Metadata; - - public AuthenticationTypes AuthenticationSettings - { - get => AuthenticationTypes; - set => AuthenticationTypes = value; - } - [Persisted] - private AuthenticationTypes AuthenticationTypes; - - public bool UseSPServerConnectionToAD - { - get => UserServerDirectoryEntry; - set => UserServerDirectoryEntry = value; - } - [Persisted] - private bool UserServerDirectoryEntry; - - /// - /// If true: this LDAPConnection will be be used for augmentation - /// - public bool EnableAugmentation // Also required as a property for ASP.NET server side controls in admin pages. - { - get => AugmentationEnabled; - set => AugmentationEnabled = value; - } - [Persisted] - private bool AugmentationEnabled; - - /// - /// If true: get group membership with UserPrincipal.GetAuthorizationGroups() - /// If false: get group membership with LDAP queries - /// - public bool GetGroupMembershipUsingDotNetHelpers // Also required as a property for ASP.NET server side controls in admin pages. - { - get => GetGroupMembershipAsADDomain; - set => GetGroupMembershipAsADDomain = value; - } - [Persisted] - private bool GetGroupMembershipAsADDomain = true; - - /// - /// Contains the name of LDAP attributes where membership of users is stored - /// - public string[] GroupMembershipLDAPAttributes - { - get => GroupMembershipAttributes; - set => GroupMembershipAttributes = value; - } - [Persisted] - private string[] GroupMembershipAttributes = new string[] { "memberOf", "uniquememberof" }; - - /// - /// DirectoryEntry used to make LDAP queries - /// - public DirectoryEntry Directory - { - get => _Directory; - set => _Directory = value; - } - private DirectoryEntry _Directory; - - /// - /// LDAP filter - /// - public string Filter - { - get => _Filter; - set => _Filter = value; - } - private string _Filter; - - /// - /// Domain name, for example "contoso" - /// - public string DomainName - { - get => _DomainName; - set => _DomainName = value; - } - private string _DomainName; - - /// - /// Fully qualified domain name, for example "contoso.local" - /// - public string DomainFQDN - { - get => _DomainFQDN; - set => _DomainFQDN = value; - } - private string _DomainFQDN; - - /// - /// Root container to connect to, for example "DC=contoso,DC=local" - /// - public string RootContainer - { - get => _RootContainer; - set => _RootContainer = value; - } - private string _RootContainer; - - public LDAPConnection() - { - } - - /// - /// Returns a copy of the current object. This copy does not have any member of the base SharePoint base class set - /// - /// - internal LDAPConnection CopyConfiguration() - { - LDAPConnection copy = new LDAPConnection(); - // Copy non-inherited public properties - PropertyInfo[] propertiesToCopy = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (PropertyInfo property in propertiesToCopy) - { - if (property.CanWrite) - { - object value = property.GetValue(this); - if (value != null) - property.SetValue(copy, value); - } - } - return copy; - } - } - - /// - /// Contains information about current operation - /// - public class OperationContext - { - public static string RegexDomainFromFullAccountName => "(.*)\\\\.*"; - public static string RegexFullDomainFromEmail => ".*@(.*)"; - - /// - /// Indicates what kind of operation SharePoint is requesting - /// - public OperationType OperationType - { - get => _OperationType; - set => _OperationType = value; - } - private OperationType _OperationType; - - /// - /// Set only if request is a validation or an augmentation, to the incoming entity provided by SharePoint - /// - public SPClaim IncomingEntity - { - get => _IncomingEntity; - set => _IncomingEntity = value; - } - private SPClaim _IncomingEntity; - - /// - /// User submitting the query in the poeple picker, retrieved from HttpContext. Can be null - /// - public SPClaim UserInHttpContext - { - get => _UserInHttpContext; - set => _UserInHttpContext = value; - } - private SPClaim _UserInHttpContext; - - /// - /// Uri provided by SharePoint - /// - public Uri UriContext - { - get => _UriContext; - set => _UriContext = value; - } - private Uri _UriContext; - - /// - /// EntityTypes expected by SharePoint in the entities returned - /// - public DirectoryObjectType[] DirectoryObjectTypes - { - get => _DirectoryObjectTypes; - set => _DirectoryObjectTypes = value; - } - private DirectoryObjectType[] _DirectoryObjectTypes; - - public string HierarchyNodeID - { - get => _HierarchyNodeID; - set => _HierarchyNodeID = value; - } - private string _HierarchyNodeID; - - public int MaxCount - { - get => _MaxCount; - set => _MaxCount = value; - } - private int _MaxCount; - - /// - /// If search: it contains the raw input. If validation: it contains the incoming SPClaim value processed to be searchable against LDAP servers (domain tokens removed). LDAP special characters are NOT escaped - /// - public string Input - { - get => _Input; - set => _Input = value; - } - private string _Input; - - public bool InputHasKeyword - { - get => _InputHasKeyword; - set => _InputHasKeyword = value; - } - private bool _InputHasKeyword; - - /// - /// Indicates if search operation should return only results that exactly match the Input - /// - public bool ExactSearch - { - get => _ExactSearch; - set => _ExactSearch = value; - } - private bool _ExactSearch; - - /// - /// Set only if request is a validation or an augmentation, to the ClaimTypeConfig that matches the ClaimType of the incoming entity - /// - public ClaimTypeConfig IncomingEntityClaimTypeConfig - { - get => _IncomingEntityClaimTypeConfig; - set => _IncomingEntityClaimTypeConfig = value; - } - private ClaimTypeConfig _IncomingEntityClaimTypeConfig; - - /// - /// Contains the relevant list of ClaimTypeConfig for every type of request. In case of validation or augmentation, it will contain only 1 item. - /// - public List CurrentClaimTypeConfigList - { - get => _CurrentClaimTypeConfigList; - set => _CurrentClaimTypeConfigList = value; - } - private List _CurrentClaimTypeConfigList; - - public OperationContext(ILDAPCPConfiguration currentConfiguration, OperationType currentRequestType, List processedClaimTypeConfigList, string input, SPClaim incomingEntity, Uri context, string[] entityTypes, string hierarchyNodeID, int maxCount) - { - this.OperationType = currentRequestType; - this.Input = input; - this.IncomingEntity = incomingEntity; - this.UriContext = context; - this.HierarchyNodeID = hierarchyNodeID; - this.MaxCount = maxCount; - - if (entityTypes != null) - { - List aadEntityTypes = new List(); - if (entityTypes.Contains(SPClaimEntityTypes.User)) - { - aadEntityTypes.Add(DirectoryObjectType.User); - } - if (entityTypes.Contains(ClaimsProviderConstants.GroupClaimEntityType)) - { - aadEntityTypes.Add(DirectoryObjectType.Group); - } - this.DirectoryObjectTypes = aadEntityTypes.ToArray(); - } - - HttpContext httpctx = HttpContext.Current; - if (httpctx != null) - { - WIF4_5.ClaimsPrincipal cp = httpctx.User as WIF4_5.ClaimsPrincipal; - if (cp != null) - { - if (SPClaimProviderManager.IsEncodedClaim(cp.Identity.Name)) - { - this.UserInHttpContext = SPClaimProviderManager.Local.DecodeClaimFromFormsSuffix(cp.Identity.Name); - } - else - { - // This code is reached only when called from central administration: current user is always a Windows user - this.UserInHttpContext = SPClaimProviderManager.Local.ConvertIdentifierToClaim(cp.Identity.Name, SPIdentifierTypes.WindowsSamAccountName); - } - } - } - - if (currentRequestType == OperationType.Validation) - { - this.InitializeValidation(processedClaimTypeConfigList); - } - else if (currentRequestType == OperationType.Search) - { - this.InitializeSearch(processedClaimTypeConfigList, currentConfiguration.FilterExactMatchOnlyProp); - } - else if (currentRequestType == OperationType.Augmentation) - { - this.InitializeAugmentation(processedClaimTypeConfigList); - } - } - - /// - /// Validation is when SharePoint expects exactly 1 PickerEntity from the incoming SPClaim - /// - /// - protected void InitializeValidation(List processedClaimTypeConfigList) - { - if (this.IncomingEntity == null) { throw new ArgumentNullException("IncomingEntity"); } - this.IncomingEntityClaimTypeConfig = processedClaimTypeConfigList.FirstOrDefault(x => - String.Equals(x.ClaimType, this.IncomingEntity.ClaimType, StringComparison.InvariantCultureIgnoreCase) && - !x.UseMainClaimTypeOfDirectoryObject); - - if (this.IncomingEntityClaimTypeConfig == null) - { - ClaimsProviderLogging.Log($"[{LDAPCP._ProviderInternalName}] Unable to validate entity \"{this.IncomingEntity.Value}\" because its claim type \"{this.IncomingEntity.ClaimType}\" was not found in the ClaimTypes list of current configuration.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); - throw new InvalidOperationException($"[{LDAPCP._ProviderInternalName}] Unable validate entity \"{this.IncomingEntity.Value}\" because its claim type \"{this.IncomingEntity.ClaimType}\" was not found in the ClaimTypes list of current configuration."); - } - - // CurrentClaimTypeConfigList must also be set - this.CurrentClaimTypeConfigList = new List(1); - this.CurrentClaimTypeConfigList.Add(this.IncomingEntityClaimTypeConfig); - this.ExactSearch = true; - this.Input = (!String.IsNullOrEmpty(IncomingEntityClaimTypeConfig.ClaimValuePrefix) && this.IncomingEntity.Value.StartsWith(IncomingEntityClaimTypeConfig.ClaimValuePrefix, StringComparison.InvariantCultureIgnoreCase)) ? - this.IncomingEntity.Value.Substring(IncomingEntityClaimTypeConfig.ClaimValuePrefix.Length) : this.IncomingEntity.Value; - - // When working with domain tokens remove the domain part of the input so it can be found in AD - if (IncomingEntityClaimTypeConfig.ClaimValuePrefix != null && ( - IncomingEntityClaimTypeConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINNAME) || - IncomingEntityClaimTypeConfig.ClaimValuePrefix.Contains(ClaimsProviderConstants.LDAPCPCONFIG_TOKENDOMAINFQDN) - )) - { - this.Input = GetAccountFromFullAccountName(this.Input); - } - - this.InputHasKeyword = (!String.IsNullOrEmpty(IncomingEntityClaimTypeConfig.ClaimValuePrefix) && !IncomingEntity.Value.StartsWith(IncomingEntityClaimTypeConfig.ClaimValuePrefix, StringComparison.InvariantCultureIgnoreCase) && IncomingEntityClaimTypeConfig.DoNotAddClaimValuePrefixIfBypassLookup) ? true : false; - } - - /// - /// Search is when SharePoint expects a list of any PickerEntity that match input provided - /// - /// - protected void InitializeSearch(List processedClaimTypeConfigList, bool exactSearch) - { - this.ExactSearch = exactSearch; - if (!String.IsNullOrEmpty(this.HierarchyNodeID)) - { - // Restrict search to ClaimType currently selected in the hierarchy (may return multiple results if identity claim type) - CurrentClaimTypeConfigList = processedClaimTypeConfigList.FindAll(x => - String.Equals(x.ClaimType, this.HierarchyNodeID, StringComparison.InvariantCultureIgnoreCase) && - this.DirectoryObjectTypes.Contains(x.EntityType)); - } - else - { - // List.FindAll returns an empty list if no result found: http://msdn.microsoft.com/en-us/library/fh1w7y8z(v=vs.110).aspx - CurrentClaimTypeConfigList = processedClaimTypeConfigList.FindAll(x => this.DirectoryObjectTypes.Contains(x.EntityType)); - } - } - - protected void InitializeAugmentation(List processedClaimTypeConfigList) - { - if (this.IncomingEntity == null) { throw new ArgumentNullException("IncomingEntity"); } - this.IncomingEntityClaimTypeConfig = processedClaimTypeConfigList.FirstOrDefault(x => - String.Equals(x.ClaimType, this.IncomingEntity.ClaimType, StringComparison.InvariantCultureIgnoreCase) && - !x.UseMainClaimTypeOfDirectoryObject); - - if (this.IncomingEntityClaimTypeConfig == null) - { - ClaimsProviderLogging.Log($"[{LDAPCP._ProviderInternalName}] Unable to augment entity \"{this.IncomingEntity.Value}\" because its claim type \"{this.IncomingEntity.ClaimType}\" was not found in the ClaimTypes list of current configuration.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); - throw new InvalidOperationException($"[{LDAPCP._ProviderInternalName}] Unable to augment entity \"{this.IncomingEntity.Value}\" because its claim type \"{this.IncomingEntity.ClaimType}\" was not found in the ClaimTypes list of current configuration."); - } - } - - /// - /// Returns the account from "domain\account" - /// - /// e.g. "contoso.local\account" - /// account - public static string GetAccountFromFullAccountName(string fullAccountName) - { - if (fullAccountName.Contains(@"\")) - { - return fullAccountName.Split(new char[] { '\\' }, 2)[1]; - } - else - { - return fullAccountName; - } - } - - /// - /// Returns the domain from "domain\account" - /// - /// e.g. "contoso.local\account" - /// e.g. "contoso.local" - public static string GetDomainFromFullAccountName(string fullAccountName) - { - if (fullAccountName.Contains(@"\")) - { - return fullAccountName.Split(new char[] { '\\' }, 2)[0]; - } - else - { - return fullAccountName; - } - } - - /// - /// Return the domain FQDN from the given email - /// - /// e.g. yvand@contoso.local - /// e.g. contoso.local - public static string GetFQDNFromEmail(string email) - { - return Regex.Replace(email, RegexFullDomainFromEmail, "$1", RegexOptions.None); - } - - public static string GetFirstSubString(string value, string separator) - { - int stop = value.IndexOf(separator); - return (stop > -1) ? value.Substring(0, stop) : string.Empty; - } - - /// - /// Return the domain name from the domain FQDN - /// - /// Fully qualified domain name - /// Domain name - public static string GetDomainName(string domainFQDN) - { - string domainName = String.Empty; - if (domainFQDN.Contains(".")) - { - domainName = domainFQDN.Split(new char[] { '.' })[0]; - } - return domainName; - } - - /// - /// Extract domain name information from the distinguishedName supplied - /// - /// distinguishedName to use to extract domain name information - /// Domain name - /// Fully qualified domain name - public static void GetDomainInformation(string distinguishedName, out string domainName, out string domainFQDN) - { - StringBuilder sbDomainFQDN = new StringBuilder(); - domainName = String.Empty; - // String search in distinguishedName should not be case sensitive - https://github.com/Yvand/LDAPCP/issues/147 - if (distinguishedName.IndexOf("DC=", StringComparison.InvariantCultureIgnoreCase) >= 0) - { - int start = distinguishedName.IndexOf("DC=", StringComparison.InvariantCultureIgnoreCase); - string[] dnSplitted = distinguishedName.Substring(start).ToLower().Split(new string[] { "dc=" }, StringSplitOptions.RemoveEmptyEntries); - bool setDomainName = true; - foreach (string dc in dnSplitted) - { - sbDomainFQDN.Append(dc.Replace(',', '.')); - if (setDomainName) - { - domainName = dc.Trim(new char[] { ',' }); - setDomainName = false; - } - } - } - domainFQDN = sbDomainFQDN.ToString(); - } - - /// - /// Query LDAP server to retrieve domain name information - /// - /// LDAP Server to query - /// Domain name - /// Fully qualified domain name - public static bool GetDomainInformation(DirectoryEntry directory, out string domaindistinguishedName, out string domainName, out string domainFQDN) - { - bool success = false; - domaindistinguishedName = String.Empty; - domainName = String.Empty; - domainFQDN = String.Empty; - - try - { -#if DEBUG - directory.AuthenticationType = AuthenticationTypes.None; - ClaimsProviderLogging.LogDebug($"Hardcoded property DirectoryEntry.AuthenticationType to {directory.AuthenticationType} for \"{directory.Path}\""); -#endif - - // Method PropertyCollection.Contains("distinguishedName") does a LDAP bind - // In AD LDS: property "distinguishedName" = "CN=LDSInstance2,DC=ADLDS,DC=local", properties "name" and "cn" = "LDSInstance2" - if (directory.Properties.Contains("distinguishedName")) - { - domaindistinguishedName = directory.Properties["distinguishedName"].Value.ToString(); - GetDomainInformation(domaindistinguishedName, out domainName, out domainFQDN); - } - else if (directory.Properties.Contains("name")) - { - domainName = directory.Properties["name"].Value.ToString(); - } - else if (directory.Properties.Contains("cn")) - { - // Tivoli stores domain name in property "cn" (properties "distinguishedName" and "name" don't exist) - domainName = directory.Properties["cn"].Value.ToString(); - } - - success = true; - } - catch (DirectoryServicesCOMException ex) - { - ClaimsProviderLogging.LogException("", $"while getting domain names information for LDAP connection {directory.Path} (DirectoryServicesCOMException)", TraceCategory.Configuration, ex); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException("", $"while getting domain names information for LDAP connection {directory.Path} (Exception)", TraceCategory.Configuration, ex); - } - - return success; - } - - /// - /// Return the value from a distinguished name, or an empty string if not found. - /// - /// e.g. "CN=group1,CN=Users,DC=contoso,DC=local" - /// e.g. "group1", or an empty string if not found - public static string GetValueFromDistinguishedName(string distinguishedNameValue) - { - int equalsIndex = distinguishedNameValue.IndexOf("=", 1); - int commaIndex = distinguishedNameValue.IndexOf(",", 1); - if (equalsIndex != -1 && commaIndex != -1) - { - return distinguishedNameValue.Substring(equalsIndex + 1, commaIndex - equalsIndex - 1); - } - else - { - return String.Empty; - } - } - - public static string EscapeSpecialCharacters(string stringWithSpecialChars) - { - string result = stringWithSpecialChars; - foreach (KeyValuePair kvp in ClaimsProviderConstants.SpecialCharacters) - { - result = result.Replace(kvp.Key, kvp.Value); - } - return result; - } - - //public static string UnescapeSpecialCharacters(string stringWithEscapedChars) - //{ - // string result = stringWithEscapedChars; - // foreach (KeyValuePair kvp in ClaimsProviderConstants.SpecialCharacters) - // { - // result = result.Replace(kvp.Value, kvp.Key); - // } - // return result; - //} - } - - public enum DirectoryObjectType - { - User, - Group - } - - public enum OperationType - { - Search, - Validation, - Augmentation, - } -} diff --git a/LDAPCP/Layouts/SharePointProjectItem.spdata b/LDAPCP/Layouts/SharePointProjectItem.spdata deleted file mode 100644 index 80296dd..0000000 --- a/LDAPCP/Layouts/SharePointProjectItem.spdata +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/LDAPCP/LdapcpUserControl.cs b/LDAPCP/LdapcpUserControl.cs deleted file mode 100644 index f1ea030..0000000 --- a/LDAPCP/LdapcpUserControl.cs +++ /dev/null @@ -1,228 +0,0 @@ -using Microsoft.SharePoint; -using Microsoft.SharePoint.Administration; -using Microsoft.SharePoint.Administration.Claims; -using Microsoft.SharePoint.Utilities; -using Microsoft.SharePoint.WebControls; -using System; -using System.Linq; -using System.Web.UI; -using static ldapcp.ClaimsProviderLogging; - -namespace ldapcp.ControlTemplates -{ - public abstract class LdapcpUserControl : UserControl - { - /// - /// This member must be set in the markup code, with the name of the claims provider - /// - public string ClaimsProviderName; - - /// - /// This member must be set in the markup code, with the name of the persisted object that holds the configuration - /// - public string PersistedObjectName; - - private Guid _PersistedObjectID; - /// - /// This property must be set in the markup code, with the GUID of the persisted object that holds the configuration - /// - public string PersistedObjectID - { - get - { - return (this._PersistedObjectID == null || this._PersistedObjectID == Guid.Empty) ? String.Empty : this._PersistedObjectID.ToString(); - } - set - { - this._PersistedObjectID = new Guid(value); - } - } - - private ILDAPCPConfiguration _PersistedObject; - protected LDAPCPConfig PersistedObject - { - get - { - SPSecurity.RunWithElevatedPrivileges(delegate () - { - if (_PersistedObject == null) _PersistedObject = LDAPCPConfig.GetConfiguration(PersistedObjectName, this.CurrentTrustedLoginProvider.Name); - if (_PersistedObject == null) - { - SPContext.Current.Web.AllowUnsafeUpdates = true; - _PersistedObject = LDAPCPConfig.CreateConfiguration(this.PersistedObjectID, this.PersistedObjectName, this.CurrentTrustedLoginProvider.Name); - SPContext.Current.Web.AllowUnsafeUpdates = false; - } - }); - return _PersistedObject as LDAPCPConfig; - } - //set { _PersistedObject = value; } - } - - protected SPTrustedLoginProvider CurrentTrustedLoginProvider; - protected ClaimTypeConfig IdentityCTConfig; - protected ConfigStatus Status; - - protected long PersistedObjectVersion - { - get - { - if (ViewState[ViewStatePersistedObjectVersionKey] == null) - { - ViewState.Add(ViewStatePersistedObjectVersionKey, PersistedObject.Version); - } - return (long)ViewState[ViewStatePersistedObjectVersionKey]; - } - set { ViewState[ViewStatePersistedObjectVersionKey] = value; } - } - - protected string MostImportantError - { - get - { - if (Status == ConfigStatus.AllGood) return String.Empty; - - if ((Status & ConfigStatus.NoSPTrustAssociation) == ConfigStatus.NoSPTrustAssociation) - { - return String.Format(TextErrorNoSPTrustAssociation, SPEncode.HtmlEncode(ClaimsProviderName)); - } - - if ((Status & ConfigStatus.PersistedObjectNotFound) == ConfigStatus.PersistedObjectNotFound) - { - return TextErrorPersistedObjectNotFound; - } - - if ((Status & ConfigStatus.NoIdentityClaimType) == ConfigStatus.NoIdentityClaimType) - { - return String.Format(TextErrorNoIdentityClaimType, CurrentTrustedLoginProvider.DisplayName, CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType); - } - - if ((Status & ConfigStatus.PersistedObjectStale) == ConfigStatus.PersistedObjectStale) - { - return TextErrorPersistedObjectStale; - } - - if ((Status & ConfigStatus.ClaimsProviderNamePropNotSet) == ConfigStatus.ClaimsProviderNamePropNotSet) - { - return TextErrorClaimsProviderNameNotSet; - } - - if ((Status & ConfigStatus.PersistedObjectNamePropNotSet) == ConfigStatus.PersistedObjectNamePropNotSet) - { - return TextErrorPersistedObjectNameNotSet; - } - - if ((Status & ConfigStatus.PersistedObjectIDPropNotSet) == ConfigStatus.PersistedObjectIDPropNotSet) - { - return TextErrorPersistedObjectIDNotSet; - } - - return String.Empty; - } - } - - protected static readonly string ViewStatePersistedObjectVersionKey = "PersistedObjectVersion"; - protected static readonly string TextErrorPersistedObjectNotFound = "PersistedObject cannot be found."; - protected static readonly string TextErrorPersistedObjectStale = "Modifications were not applied because the persisted object was modified after this page was loaded. Please refresh the page and try again."; - protected static readonly string TextErrorNoSPTrustAssociation = "{0} is currently not associated with any TrustedLoginProvider, which is required to create entities.
Visit ldapcp.com for more information.
Refresh this page once '{0}' is associated with a TrustedLoginProvider."; - protected static readonly string TextErrorNoIdentityClaimType = "The TrustedLoginProvider {0} is set with identity claim type '{1}', but is not set in claim types configuration list.
Please visit claim types configuration page to add it."; - protected static readonly string TextErrorClaimsProviderNameNotSet = "The attribute 'ClaimsProviderName' must be set in the user control."; - protected static readonly string TextErrorPersistedObjectNameNotSet = "The attribute 'PersistedObjectName' must be set in the user control."; - protected static readonly string TextErrorPersistedObjectIDNotSet = "The attribute 'PersistedObjectID' must be set in the user control."; - - /// - /// Ensures configuration is valid to proceed - /// - /// - public virtual ConfigStatus ValidatePrerequisite() - { - if (!this.IsPostBack) - { - // DataBind() must be called to bind attributes that are set as "<%# #>"in .aspx - // But only during initial page load, otherwise it would reset bindings in other controls like SPGridView - DataBind(); - ViewState.Add("ClaimsProviderName", ClaimsProviderName); - ViewState.Add("PersistedObjectName", PersistedObjectName); - ViewState.Add("PersistedObjectID", PersistedObjectID); - } - else - { - ClaimsProviderName = ViewState["ClaimsProviderName"].ToString(); - PersistedObjectName = ViewState["PersistedObjectName"].ToString(); - PersistedObjectID = ViewState["PersistedObjectID"].ToString(); - } - - Status = ConfigStatus.AllGood; - if (String.IsNullOrEmpty(ClaimsProviderName)) { Status |= ConfigStatus.ClaimsProviderNamePropNotSet; } - if (String.IsNullOrEmpty(PersistedObjectName)) { Status |= ConfigStatus.PersistedObjectNamePropNotSet; } - if (String.IsNullOrEmpty(PersistedObjectID)) { Status |= ConfigStatus.PersistedObjectIDPropNotSet; } - if (Status != ConfigStatus.AllGood) - { - ClaimsProviderLogging.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); - // Should not go further if those requirements are not met - return Status; - } - - if (CurrentTrustedLoginProvider == null) - { - CurrentTrustedLoginProvider = LDAPCP.GetSPTrustAssociatedWithCP(this.ClaimsProviderName); - if (CurrentTrustedLoginProvider == null) - { - Status |= ConfigStatus.NoSPTrustAssociation; - return Status; - } - } - - if (PersistedObject == null) - { - Status |= ConfigStatus.PersistedObjectNotFound; - } - - if (Status != ConfigStatus.AllGood) - { - ClaimsProviderLogging.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); - // Should not go further if those requirements are not met - return Status; - } - - PersistedObject.CheckAndCleanConfiguration(CurrentTrustedLoginProvider.Name); - PersistedObject.ClaimTypes.SPTrust = CurrentTrustedLoginProvider; - if (IdentityCTConfig == null && Status == ConfigStatus.AllGood) - { - IdentityCTConfig = this.IdentityCTConfig = PersistedObject.ClaimTypes.FirstOrDefault(x => String.Equals(CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase) && !x.UseMainClaimTypeOfDirectoryObject); - if (IdentityCTConfig == null) - { - Status |= ConfigStatus.NoIdentityClaimType; - } - } - if (PersistedObjectVersion != PersistedObject.Version) - { - Status |= ConfigStatus.PersistedObjectStale; - } - - if (Status != ConfigStatus.AllGood) - { - ClaimsProviderLogging.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); - } - return Status; - } - - public virtual void CommitChanges() - { - PersistedObject.Update(); - PersistedObjectVersion = PersistedObject.Version; - } - } - - [Flags] - public enum ConfigStatus - { - AllGood = 0x0, - PersistedObjectNotFound = 0x1, - NoSPTrustAssociation = 0x2, - NoIdentityClaimType = 0x4, - PersistedObjectStale = 0x8, - ClaimsProviderNamePropNotSet = 0x10, - PersistedObjectNamePropNotSet = 0x20, - PersistedObjectIDPropNotSet = 0x40 - }; -} diff --git a/LDAPCP/Package/Package.package b/LDAPCP/Package/Package.package deleted file mode 100644 index e189b7e..0000000 --- a/LDAPCP/Package/Package.package +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Yvand.LDAPCPSE.Tests/AugmentationTests.cs b/Yvand.LDAPCPSE.Tests/AugmentationTests.cs new file mode 100644 index 0000000..5f7997a --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/AugmentationTests.cs @@ -0,0 +1,80 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yvand.LdapClaimsProvider.Tests +{ + public class AugmentUsingLdapQueryTestss : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + Settings.LdapConnections[0].GetGroupMembershipUsingDotNetHelpers = false; + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), nameof(ValidateEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public virtual void TestAugmentationOperation(ValidateEntityData registrationData) + { + TestAugmentationOperation(registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + +#if DEBUG + [TestCase("testLdapcpseUser_001@contoso.local", true, @"contoso.local\testLdapcpseGroup_2")] + public void TestAugmentationOperationGroupRecursive(string claimValue, bool isMemberOfTrustedGroup, string groupValue) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, groupValue); + } +#endif + } + + public class AugmentUsingLdapQueryAndSidAsGroupValueTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + Settings.LdapConnections[0].GetGroupMembershipUsingDotNetHelpers = false; + base.Settings.ClaimTypes.UpdateGroupIdentifier("group", "objectSid"); + base.Settings.ClaimTypes.GetIdentifierConfiguration(Configuration.DirectoryObjectType.Group).ClaimValueLeadingToken = String.Empty; + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupSid); + } + +#if DEBUG + [TestCase("testLdapcpseUser_001@contoso.local", true, @"S-1-5-21-2647467245-1611586658-188888215-110602")] // testLdapcpseGroup_2 + public void TestAugmentationOperationGroupRecursive(string claimValue, bool isMemberOfTrustedGroup, string groupValue) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, groupValue); + } +#endif + } +} diff --git a/Yvand.LDAPCPSE.Tests/BasicConfigurationTests.cs b/Yvand.LDAPCPSE.Tests/BasicConfigurationTests.cs new file mode 100644 index 0000000..cfe9d91 --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/BasicConfigurationTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; + +namespace Yvand.LdapClaimsProvider.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + internal class BasicConfigurationTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + + [Test, TestCaseSource(typeof(SearchEntityDataSource), nameof(SearchEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public void TestSearch(SearchEntityData registrationData) + { + base.TestSearchOperation(registrationData.Input, registrationData.SearchResultCount, registrationData.SearchResultSingleEntityClaimValue); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), nameof(ValidateEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [MaxTime(UnitTestsHelper.MaxTime)] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public void TestValidation(ValidateEntityData registrationData) + { + base.TestValidationOperation(registrationData); + } + + /// + /// Tests if the augmentation works as expected. + /// + /// + [Test, TestCaseSource(typeof(ValidateEntityDataSource), nameof(ValidateEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public virtual void TestAugmentationOperation(ValidateEntityData registrationData) + { + TestAugmentationOperation(registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + +#if DEBUG + ////[TestCaseSource(typeof(SearchEntityDataSourceCollection))] + //public void DEBUG_SearchEntitiesFromCollection(string inputValue, string expectedCount, string expectedClaimValue) + //{ + // if (!TestSearchTest) { return; } + + // TestSearchOperation(inputValue, Convert.ToInt32(expectedCount), expectedClaimValue); + //} + + //[TestCase(@"group\ch", 1, @"contoso.local\group\chartest")] + [TestCase(@"test_special)", 1, @"test_special_char@contoso.local")] + //[TestCase(@"group\ch", 1, @"group\chartest")] + [TestCase(@"testLdapcpseUser_001", 1, @"testLdapcpseUser_001@contoso.local")] + public void TestSearch(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + } + + //[TestCase("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", @"contoso.local\group\chartest", true)] + //[TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", @"test)char@contoso.local", true)] + //[TestCase("http://yvand.com/customType1", @"group\chartest", true)] + [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", @"yvan", false)] + public void TestValidation(string claimType, string claimValue, bool shouldValidate) + { + base.TestValidationOperation(claimType, claimValue, shouldValidate); + } +#endif + } +} diff --git a/Yvand.LDAPCPSE.Tests/BypassDirectoryTests.cs b/Yvand.LDAPCPSE.Tests/BypassDirectoryTests.cs new file mode 100644 index 0000000..34c8f36 --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/BypassDirectoryTests.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using Yvand.LdapClaimsProvider.Configuration; + +namespace Yvand.LdapClaimsProvider.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class BypassDirectoryOnClaimTypesTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.User).LeadingKeywordToBypassDirectory = "bypass-user:"; + Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.Group).LeadingKeywordToBypassDirectory = "bypass-group:"; + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + + [TestCase("bypass-user:externalUser@contoso.com", 1, "externalUser@contoso.com")] + [TestCase("externalUser@contoso.com", 0, "")] + [TestCase("bypass-user:", 0, "")] + [TestCase(@"bypass-group:domain\groupValue", 1, @"domain\groupValue")] + [TestCase(@"domain\groupValue", 0, "")] + [TestCase("bypass-group:", 0, "")] + public void TestBypassDirectoryByClaimType(string inputValue, int expectedCount, string expectedClaimValue) + { + TestSearchOperation(inputValue, expectedCount, expectedClaimValue); + + if (expectedCount > 0) + { + TestValidationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, expectedClaimValue, true); + } + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), nameof(ValidateEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public virtual void TestAugmentationOperation(ValidateEntityData registrationData) + { + TestAugmentationOperation(registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + } + + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class BypassDirectoryGloballyTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + Settings.AlwaysResolveUserInput = true; + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + + [Test] + public void TestBypassDirectoryGlobally() + { + TestSearchOperation(UnitTestsHelper.RandomClaimValue, 2, UnitTestsHelper.RandomClaimValue); + TestValidationOperation(base.UserIdentifierClaimType, UnitTestsHelper.RandomClaimValue, true); + TestValidationOperation(base.GroupIdentifierClaimType, UnitTestsHelper.RandomClaimValue, true); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), nameof(ValidateEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public virtual void TestAugmentationOperation(ValidateEntityData registrationData) + { + TestAugmentationOperation(registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + } +} diff --git a/Yvand.LDAPCPSE.Tests/ClaimsProviderTestsBase.cs b/Yvand.LDAPCPSE.Tests/ClaimsProviderTestsBase.cs new file mode 100644 index 0000000..88a1fe6 --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/ClaimsProviderTestsBase.cs @@ -0,0 +1,234 @@ +using Microsoft.SharePoint.Administration.Claims; +using Microsoft.SharePoint.WebControls; +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Text; +using Yvand.LdapClaimsProvider.Configuration; + +namespace Yvand.LdapClaimsProvider.Tests +{ + public class ClaimsProviderTestsBase + { + /// + /// Configures whether the configuration applied is valid, and whether the claims provider should be able to use it + /// + protected bool ConfigurationShouldBeValid = true; + + protected string UserIdentifierClaimType + { + get + { + return Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.User).ClaimType; + } + } + + protected string GroupIdentifierClaimType + { + get + { + return Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.Group).ClaimType; + } + } + + protected LdapProviderSettings Settings = new LdapProviderSettings(); + + /// + /// Initialize settings + /// + [OneTimeSetUp] + protected virtual void InitializeSettings() + { + Settings = new LdapProviderSettings(); + Settings.ClaimTypes = LdapProviderSettings.ReturnDefaultClaimTypesConfig(UnitTestsHelper.ClaimsProvider.Name); + +#if DEBUG + Settings.Timeout = 99999; +#endif + + string json = File.ReadAllText(UnitTestsHelper.AzureTenantsJsonFile); + List azureTenants = JsonConvert.DeserializeObject>(json); + Settings.LdapConnections = azureTenants; + Settings.EnableAugmentation = true; + + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Initialized default settings."); + } + + /// + /// Override this method and decorate it with [Test] if the settings applied in the inherited class should be tested + /// + public virtual void CheckSettingsTest() + { + UnitTestsHelper.PersistedConfiguration.ApplySettings(Settings, false); + if (ConfigurationShouldBeValid) + { + Assert.DoesNotThrow(() => UnitTestsHelper.PersistedConfiguration.ValidateConfiguration(), "ValidateLocalConfiguration should NOT throw a InvalidOperationException because the configuration is valid"); + } + else + { + Assert.Throws(() => UnitTestsHelper.PersistedConfiguration.ValidateConfiguration(), "ValidateLocalConfiguration should throw a InvalidOperationException because the configuration is invalid"); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Invalid configuration: {JsonConvert.SerializeObject(Settings, Formatting.None)}"); + } + } + + /// + /// Applies the to the configuration object and save it in the configuration database + /// + protected void ApplySettings() + { + UnitTestsHelper.PersistedConfiguration.ApplySettings(Settings, true); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Updated configuration: {JsonConvert.SerializeObject(Settings, Formatting.None)}"); + } + + [OneTimeTearDown] + protected void Cleanup() + { + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Cleanup."); + } + + /// + /// Start search operation on a specific claims provider + /// + /// + /// How many entities are expected to be returned. Set to Int32.MaxValue if exact number is unknown but greater than 0 + /// + protected void TestSearchOperation(string inputValue, int expectedCount, string expectedClaimValue) + { + try + { + Stopwatch timer = new Stopwatch(); + timer.Start(); + var entityTypes = new[] { "User", "SecGroup", "SharePointGroup", "System", "FormsRole" }; + + SPProviderHierarchyTree providerResults = UnitTestsHelper.ClaimsProvider.Search(UnitTestsHelper.TestSiteCollUri, entityTypes, inputValue, null, 30); + List entities = new List(); + foreach (var children in providerResults.Children) + { + entities.AddRange(children.EntityData); + } + VerifySearchTest(entities, inputValue, expectedCount, expectedClaimValue); + + entities = UnitTestsHelper.ClaimsProvider.Resolve(UnitTestsHelper.TestSiteCollUri, entityTypes, inputValue).ToList(); + VerifySearchTest(entities, inputValue, expectedCount, expectedClaimValue); + timer.Stop(); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestSearchOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: inputValue: '{inputValue}', expectedCount: '{expectedCount}', expectedClaimValue: '{expectedClaimValue}'."); + } + catch (Exception ex) + { + Trace.TraceError($"{DateTime.Now:s} [{this.GetType().Name}] TestSearchOperation failed with exception '{ex.GetType()}', message '{ex.Message}'. Parameters: inputValue: '{inputValue}', expectedCount: '{expectedCount}', expectedClaimValue: '{expectedClaimValue}'."); + } + } + + private void VerifySearchTest(List entities, string input, int expectedCount, string expectedClaimValue) + { + bool entityValueFound = false; + StringBuilder detailedLog = new StringBuilder($"It returned {entities.Count} entities: "); + string entityLogPattern = "entity \"{0}\", claim type: \"{1}\"; "; + foreach (PickerEntity entity in entities) + { + detailedLog.AppendLine(String.Format(entityLogPattern, entity.Claim.Value, entity.Claim.ClaimType)); + if (String.Equals(expectedClaimValue, entity.Claim.Value, StringComparison.InvariantCultureIgnoreCase)) + { + entityValueFound = true; + } + } + + if (!String.IsNullOrWhiteSpace(expectedClaimValue) && !entityValueFound && expectedCount > 0) + { + Assert.Fail($"Input \"{input}\" returned no entity with claim value \"{expectedClaimValue}\". {detailedLog}"); + } + + if (expectedCount == Int32.MaxValue) + { + expectedCount = entities.Count; + } + + Assert.That(entities.Count, Is.EqualTo(expectedCount), $"Input \"{input}\" should have returned {expectedCount} entities, but it returned {entities.Count} instead. {detailedLog}"); + } + + protected void TestValidationOperation(ValidateEntityData registrationData) + { + bool shouldValidate = registrationData.ShouldValidate; + string claimType = registrationData.EntityType == ResultEntityType.User ? + UserIdentifierClaimType : + GroupIdentifierClaimType; + + TestValidationOperation(claimType, registrationData.ClaimValue, shouldValidate); + } + + protected void TestValidationOperation(string claimType, string claimValue, bool shouldValidate) //(SPClaim inputClaim, bool shouldValidate, string expectedClaimValue) + { + SPClaim inputClaim = new SPClaim(claimType, claimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); + try + { + Stopwatch timer = new Stopwatch(); + timer.Start(); + var entityTypes = new[] { "User" }; + + PickerEntity[] entities = UnitTestsHelper.ClaimsProvider.Resolve(UnitTestsHelper.TestSiteCollUri, entityTypes, inputClaim); + + int expectedCount = shouldValidate ? 1 : 0; + Assert.That(entities.Length, Is.EqualTo(expectedCount), $"Validation of entity \"{inputClaim.Value}\" should have returned {expectedCount} entity, but it returned {entities.Length} instead."); + if (shouldValidate) + { + Assert.That(entities[0].Claim.Value, Is.EqualTo(claimValue).IgnoreCase, $"Validation of entity \"{inputClaim.Value}\" should have returned value \"{claimValue}\", but it returned \"{entities[0].Claim.Value}\" instead."); + } + timer.Stop(); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestValidationOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: inputClaim.Value: '{inputClaim.Value}', shouldValidate: '{shouldValidate}', expectedClaimValue: '{claimValue}'."); + } + catch (Exception ex) + { + Trace.TraceError($"{DateTime.Now:s} [{this.GetType().Name}] TestValidationOperation failed with exception '{ex.GetType()}', message '{ex.Message}'. Parameters: inputClaim.Value: '{inputClaim.Value}', shouldValidate: '{shouldValidate}', expectedClaimValue: '{claimValue}'."); + } + } + + /// + /// Tests if the augmentation works as expected. + /// + /// + /// + //[TestCase("FakeAccount", false)] + //[TestCase("yvand@contoso.local", true)] + protected void TestAugmentationOperation(string claimValue, bool shouldBeMemberOfTheGroupTested, string groupNameToTestInGroupMembership) + { + string claimType = UserIdentifierClaimType; + SPClaim groupClaimToTestInGroupMembership = new SPClaim(GroupIdentifierClaimType, groupNameToTestInGroupMembership, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); + try + { + Stopwatch timer = new Stopwatch(); + timer.Start(); + SPClaim inputClaim = new SPClaim(claimType, claimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); + Uri context = new Uri(UnitTestsHelper.TestSiteCollUri.AbsoluteUri); + + SPClaim[] groups = UnitTestsHelper.ClaimsProvider.GetClaimsForEntity(context, inputClaim); + + bool groupFound = false; + if (groups != null && groups.Contains(groupClaimToTestInGroupMembership)) + { + groupFound = true; + } + + if (shouldBeMemberOfTheGroupTested) + { + + Assert.That(groupFound, Is.True, $"Entity \"{claimValue}\" should be member of group \"{groupNameToTestInGroupMembership}\", but this group was not found in the claims returned by the claims provider."); + } + else + { + Assert.That(groupFound, Is.False, $"Entity \"{claimValue}\" should NOT be member of group \"{groupNameToTestInGroupMembership}\", but this group was found in the claims returned by the claims provider."); + } + timer.Stop(); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: claimType: '{claimType}', claimValue: '{claimValue}', isMemberOfTrustedGroup: '{shouldBeMemberOfTheGroupTested}'."); + } + catch (Exception ex) + { + Trace.TraceError($"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOperation failed with exception '{ex.GetType()}', message '{ex.Message}'. Parameters: claimType: '{claimType}', claimValue: '{claimValue}', isMemberOfTrustedGroup: '{shouldBeMemberOfTheGroupTested}'."); + } + } + } +} diff --git a/Yvand.LDAPCPSE.Tests/PrimaryGroupIDTests.cs b/Yvand.LDAPCPSE.Tests/PrimaryGroupIDTests.cs new file mode 100644 index 0000000..bf29f4e --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/PrimaryGroupIDTests.cs @@ -0,0 +1,56 @@ +using NUnit.Framework; +using Yvand.LdapClaimsProvider.Configuration; + +namespace Yvand.LdapClaimsProvider.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + internal class PrimaryGroupIdTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + ClaimTypeConfig ctConfigPgidAttribute = new ClaimTypeConfig + { + ClaimType = TestContext.Parameters["MultiPurposeCustomClaimType"], // WIF4_5.ClaimTypes.PrimaryGroupSid + ClaimTypeDisplayName = "primaryGroupID", + DirectoryObjectType = DirectoryObjectType.User, + SPEntityType = ClaimsProviderConstants.GroupClaimEntityType, + DirectoryObjectClass = "user", + DirectoryObjectAttribute = "primaryGroupID", + DirectoryObjectAttributeSupportsWildcard = false, + }; + Settings.ClaimTypes.Add(ctConfigPgidAttribute); + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + + [TestCase(@"513", 1, @"513")] + [TestCase(@"51", 0, @"")] + [TestCase(@"5133", 0, @"")] + public void TestPrimaryGroupIdClaimType(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + base.TestValidationOperation(TestContext.Parameters["MultiPurposeCustomClaimType"], expectedEntityClaimValue, expectedResultCount == 0 ? false : true); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), nameof(ValidateEntityDataSource.GetTestData), new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public virtual void TestAugmentationOperation(ValidateEntityData registrationData) + { + TestAugmentationOperation(registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupName); + } + } +} diff --git a/Yvand.LDAPCPSE.Tests/Properties/AssemblyInfo.cs b/Yvand.LDAPCPSE.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..09c4a2a --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Yvand.LDAPCPSE.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Yvand.LDAPCPSE.Tests")] +[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("90a764e3-68f7-4e55-b557-e46b98340216")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Yvand.LDAPCPSE.Tests/UnitTestsHelper.cs b/Yvand.LDAPCPSE.Tests/UnitTestsHelper.cs new file mode 100644 index 0000000..3c65641 --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/UnitTestsHelper.cs @@ -0,0 +1,274 @@ +using DataAccess; +using Microsoft.SharePoint; +using Microsoft.SharePoint.Administration; +using Microsoft.SharePoint.Administration.Claims; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Security.Claims; +using Yvand.LdapClaimsProvider.Configuration; + +namespace Yvand.LdapClaimsProvider.Tests +{ + [SetUpFixture] + public class UnitTestsHelper + { + public static readonly LDAPCPSE ClaimsProvider = new LDAPCPSE(TestContext.Parameters["ClaimsProviderName"]); + public static SPTrustedLoginProvider SPTrust => SPSecurityTokenServiceManager.Local.TrustedLoginProviders.FirstOrDefault(x => String.Equals(x.ClaimProviderName, TestContext.Parameters["ClaimsProviderName"], StringComparison.InvariantCultureIgnoreCase)); + public static Uri TestSiteCollUri; + public static string TestSiteRelativePath => $"/sites/{TestContext.Parameters["TestSiteCollectionName"]}"; + public const int MaxTime = 50000; + public static string FarmAdmin => TestContext.Parameters["FarmAdmin"]; + public static string DomainFqdn => TestContext.Parameters["DomainFqdn"]; + public static string Domain => TestContext.Parameters["Domain"]; +#if DEBUG + public const int TestRepeatCount = 1; +#else + public const int TestRepeatCount = 20; +#endif + + public static string RandomClaimType => "http://schemas.yvand.net/ws/claims/random"; + public static string RandomClaimValue => "IDoNotExist"; + public static string RandomDirectoryObjectClass => "randomClass"; + public static string RandomDirectoryObjectAttribute => "randomAttribute"; + + public static string ValidGroupClaimType => TestContext.Parameters["ValidGroupClaimType"]; + public static string ValidGroupName => TestContext.Parameters["ValidGroupName"]; + public const string ValidGroupSid = "S-1-5-21-2647467245-1611586658-188888215-11601"; //=> TestContext.Parameters["ValidTrustedGroupName"]; // CONTOSO\group1 + public const string ValidUserSid = "S-1-5-21-2647467245-1611586658-188888215-107206"; // => TestContext.Parameters["ValidUserSid"]; // CONTOSO\testLdapcpseUser_001 + + public static string AzureTenantsJsonFile => TestContext.Parameters["LdapConnections"]; + public static string DataFile_AllAccounts_Search => TestContext.Parameters["DataFile_AllAccounts_Search"]; + public static string DataFile_AllAccounts_Validate => TestContext.Parameters["DataFile_AllAccounts_Validate"]; + static TextWriterTraceListener Logger { get; set; } + public static LdapProviderConfiguration PersistedConfiguration; + private static ILdapProviderSettings OriginalSettings; + + + [OneTimeSetUp] + public static void InitializeSiteCollection() + { + Logger = new TextWriterTraceListener(TestContext.Parameters["TestLogFileName"]); + Trace.Listeners.Add(Logger); + Trace.AutoFlush = true; + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] Start integration tests of {ClaimsProvider.Name} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LDAPCPSE)).Location).FileVersion}."); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] DataFile_AllAccounts_Search: {DataFile_AllAccounts_Search}"); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] DataFile_AllAccounts_Validate: {DataFile_AllAccounts_Validate}"); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] TestSiteCollectionName: {TestContext.Parameters["TestSiteCollectionName"]}"); + + if (SPTrust == null) + { + Trace.TraceError($"{DateTime.Now.ToString("s")} [SETUP] SPTrust: is null"); + } + else + { + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] SPTrust: {SPTrust.Name}"); + } + + PersistedConfiguration = LDAPCPSE.GetConfiguration(true); + if (PersistedConfiguration != null) + { + OriginalSettings = PersistedConfiguration.Settings; + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Took a backup of the original settings"); + } + else + { + PersistedConfiguration = LDAPCPSE.CreateConfiguration(); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Persisted configuration not found, created it"); + } + +#if DEBUG + TestSiteCollUri = new Uri($"http://spsites{TestSiteRelativePath}"); + return; // Uncommented when debugging from unit tests +#endif + + var service = SPFarm.Local.Services.GetValue(String.Empty); + SPWebApplication wa = service.WebApplications.FirstOrDefault(x => + { + foreach (var iisSetting in x.IisSettings.Values) + { + foreach (SPAuthenticationProvider authenticationProviders in iisSetting.ClaimsAuthenticationProviders) + { + if (String.Equals(authenticationProviders.ClaimProviderName, LDAPCPSE.ClaimsProviderName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + return false; + }); + if (wa == null) + { + Trace.TraceError($"{DateTime.Now.ToString("s")} [SETUP] Web application was NOT found."); + return; + } + + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] Web application {wa.Name} found."); + Uri waRootAuthority = wa.AlternateUrls[0].Uri; + TestSiteCollUri = new Uri($"{waRootAuthority.GetLeftPart(UriPartial.Authority)}{TestSiteRelativePath}"); + SPClaimProviderManager claimMgr = SPClaimProviderManager.Local; + string encodedGroupClaim = claimMgr.EncodeClaim(new SPClaim(ValidGroupClaimType, ValidGroupName, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name))); + SPUserInfo groupInfo = new SPUserInfo { LoginName = encodedGroupClaim, Name = ValidGroupName }; + + FileVersionInfo spAssemblyVersion = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(SPSite)).Location); + string spSiteTemplate = "STS#3"; // modern team site template + if (spAssemblyVersion.FileBuildPart < 10000) + { + // If SharePoint 2016, must use the classic team site template + spSiteTemplate = "STS#0"; + } + + // The root site may not exist, but it must be present for tests to run + if (!SPSite.Exists(waRootAuthority)) + { + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] Creating root site collection {waRootAuthority.AbsoluteUri}..."); + SPSite spSite = wa.Sites.Add(waRootAuthority.AbsoluteUri, "root", "root", 1033, spSiteTemplate, FarmAdmin, String.Empty, String.Empty); + spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); + + SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; + membersGroup.AddUser(groupInfo.LoginName, groupInfo.Email, groupInfo.Name, groupInfo.Notes); + spSite.Dispose(); + } + + if (!SPSite.Exists(TestSiteCollUri)) + { + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] Creating site collection {TestSiteCollUri.AbsoluteUri} with template '{spSiteTemplate}'..."); + SPSite spSite = wa.Sites.Add(TestSiteCollUri.AbsoluteUri, LDAPCPSE.ClaimsProviderName, LDAPCPSE.ClaimsProviderName, 1033, spSiteTemplate, FarmAdmin, String.Empty, String.Empty); + spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); + + SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; + membersGroup.AddUser(groupInfo.LoginName, groupInfo.Email, groupInfo.Name, groupInfo.Notes); + spSite.Dispose(); + } + else + { + using (SPSite spSite = new SPSite(TestSiteCollUri.AbsoluteUri)) + { + SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; + membersGroup.AddUser(groupInfo.LoginName, groupInfo.Email, groupInfo.Name, groupInfo.Notes); + } + } + } + + [OneTimeTearDown] + public static void Cleanup() + { + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Cleanup."); + try + { + if (PersistedConfiguration != null && OriginalSettings != null) + { + PersistedConfiguration.ApplySettings(OriginalSettings, true); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Restored original settings of LDAPCPSE configuration"); + } + } + catch (Exception ex) + { + Trace.TraceError($"{DateTime.Now:s} [SETUP] Unexpected error while restoring the original settings of LDAPCPSE configuration: {ex.Message}"); + } + + Trace.TraceInformation($"{DateTime.Now.ToString("s")} [SETUP] Integration tests of {LDAPCPSE.ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LDAPCPSE)).Location).FileVersion} finished."); + Trace.Flush(); + if (Logger != null) + { + Logger.Dispose(); + } + } + } + + //public class SearchEntityDataSourceCollection : IEnumerable + //{ + // public IEnumerator GetEnumerator() + // { + // yield return new[] { "AADGroup1", "1", "5b0f6c56-c87f-44c3-9354-56cba03da433" }; + // yield return new[] { "AADGroupTes", "1", "99abdc91-e6e0-475c-a0ba-5014f91de853" }; + // } + //} + + public enum ResultEntityType + { + None, + Mixed, + User, + Group, + } + + public enum EntityDataSourceType + { + AllAccounts, + UPNB2BGuestAccounts + } + + public class SearchEntityData + { + public string Input; + public int SearchResultCount; + public string SearchResultSingleEntityClaimValue; + public ResultEntityType SearchResultEntityTypes; + public bool ExactMatch; + } + + public class SearchEntityDataSource + { + public static IEnumerable GetTestData(EntityDataSourceType entityDataSourceType) + { + string csvPath = UnitTestsHelper.DataFile_AllAccounts_Search; + DataTable dt = DataTable.New.ReadCsv(csvPath); + foreach (Row row in dt.Rows) + { + var registrationData = new SearchEntityData(); + registrationData.Input = row["Input"]; + registrationData.SearchResultCount = Convert.ToInt32(row["SearchResultCount"]); + registrationData.SearchResultSingleEntityClaimValue = row["SearchResultSingleEntityClaimValue"]; + registrationData.SearchResultEntityTypes = (ResultEntityType)Enum.Parse(typeof(ResultEntityType), row["SearchResultEntityTypes"]); + registrationData.ExactMatch = Convert.ToBoolean(row["ExactMatch"]); + yield return new TestCaseData(new object[] { registrationData }); + } + } + + //public class ReadCSV + //{ + // public void GetValue() + // { + // TextReader tr1 = new StreamReader(@"c:\pathtofile\filename", true); + + // var Data = tr1.ReadToEnd().Split('\n') + // .Where(l => l.Length > 0) //nonempty strings + // .Skip(1) // skip header + // .Select(s => s.Trim()) // delete whitespace + // .Select(l => l.Split(',')) // get arrays of values + // .Select(l => new { Field1 = l[0], Field2 = l[1], Field3 = l[2] }); + // } + //} + } + + public class ValidateEntityDataSource + { + public static IEnumerable GetTestData(EntityDataSourceType entityDataSourceType) + { + string csvPath = UnitTestsHelper.DataFile_AllAccounts_Validate; + DataTable dt = DataTable.New.ReadCsv(csvPath); + + foreach (Row row in dt.Rows) + { + var registrationData = new ValidateEntityData(); + registrationData.ClaimValue = row["ClaimValue"]; + registrationData.ShouldValidate = Convert.ToBoolean(row["ShouldValidate"]); + registrationData.IsMemberOfTrustedGroup = Convert.ToBoolean(row["IsMemberOfTrustedGroup"]); + registrationData.EntityType = (ResultEntityType)Enum.Parse(typeof(ResultEntityType), row["EntityType"]); + yield return new TestCaseData(new object[] { registrationData }); + } + } + } + + public class ValidateEntityData + { + public string ClaimValue; + public bool ShouldValidate; + public bool IsMemberOfTrustedGroup; + public ResultEntityType EntityType; + } +} \ No newline at end of file diff --git a/Yvand.LDAPCPSE.Tests/UseSidAttributeForPermissionTests.cs b/Yvand.LDAPCPSE.Tests/UseSidAttributeForPermissionTests.cs new file mode 100644 index 0000000..202e24c --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/UseSidAttributeForPermissionTests.cs @@ -0,0 +1,160 @@ +using NUnit.Framework; +using System; +using System.Diagnostics; +using Yvand.LdapClaimsProvider.Configuration; + +namespace Yvand.LdapClaimsProvider.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class UseSidAttributeAsUserIdentifierPermissionTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + base.Settings.ClaimTypes.UpdateUserIdentifier("user", "objectSid"); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Initialized custom settings."); + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + +#if DEBUG + [TestCase(UnitTestsHelper.ValidUserSid, 1, UnitTestsHelper.ValidUserSid)] + [TestCase(@"testLdapcpseUser_001", 1, UnitTestsHelper.ValidUserSid)] + [TestCase(@"S-1-5-21-0000000000-1611586658-188888215-107206", 0, @"")] + public void TestSidAttribute(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + base.TestValidationOperation(base.UserIdentifierClaimType, expectedEntityClaimValue, expectedResultCount == 0 ? false : true); + } +#endif + } + + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class UseSidAsSearchAttributeForUserIdentifierTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + ClaimTypeConfig ctConfigPgidAttribute = new ClaimTypeConfig + { + ClaimType = String.Empty, + IsAdditionalLdapSearchAttribute = true, + DirectoryObjectType = DirectoryObjectType.User, + DirectoryObjectClass = "user", + DirectoryObjectAttribute = "objectSid", + DirectoryObjectAttributeSupportsWildcard = false, + SPEntityDataKey = "User", + }; + base.Settings.ClaimTypes.Add(ctConfigPgidAttribute); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Initialized custom settings."); + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + +#if DEBUG + [TestCase(UnitTestsHelper.ValidUserSid, 1, @"testLdapcpseUser_001@contoso.local")] + [TestCase(@"testLdapcpseUser_001", 1, @"testLdapcpseUser_001@contoso.local")] + public void TestSidAttribute(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + base.TestValidationOperation(base.UserIdentifierClaimType, expectedEntityClaimValue, expectedResultCount == 0 ? false : true); + } +#endif + } + + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class UseSidAttributeForUserPermissionTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + ClaimTypeConfig ctConfigPgidAttribute = new ClaimTypeConfig + { + ClaimType = TestContext.Parameters["MultiPurposeCustomClaimType"], // WIF4_5.ClaimTypes.PrimaryGroupSid + ClaimTypeDisplayName = "SID", + DirectoryObjectType = DirectoryObjectType.User, + DirectoryObjectClass = "user", + DirectoryObjectAttribute = "objectSid", + DirectoryObjectAttributeSupportsWildcard = false, + }; + base.Settings.ClaimTypes.Add(ctConfigPgidAttribute); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Initialized custom settings."); + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + +#if DEBUG + [TestCase(UnitTestsHelper.ValidUserSid, 1, UnitTestsHelper.ValidUserSid)] + [TestCase(@"S-1-5-21-0000000000-1611586658-188888215-107206", 0, @"")] + public void TestSidAttribute(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + base.TestValidationOperation(TestContext.Parameters["MultiPurposeCustomClaimType"], inputValue, expectedResultCount == 0 ? false : true); + } +#endif + } + + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class UseSidAttributeAsGroupIdentifierTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + base.Settings.ClaimTypes.UpdateGroupIdentifier("group", "objectSid"); + base.Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.Group).ClaimValueLeadingToken = String.Empty; + ClaimTypeConfig ctConfigPgidAttribute = new ClaimTypeConfig + { + ClaimType = String.Empty, + IsAdditionalLdapSearchAttribute = true, + DirectoryObjectType = DirectoryObjectType.Group, + DirectoryObjectClass = "group", + DirectoryObjectAttribute = "sAMAccountName", + }; + base.Settings.ClaimTypes.Add(ctConfigPgidAttribute); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Initialized custom settings."); + base.ApplySettings(); + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + +#if DEBUG + [TestCase(UnitTestsHelper.ValidGroupSid, 1, UnitTestsHelper.ValidGroupSid)] + [TestCase("group1", 1, UnitTestsHelper.ValidGroupSid)] + public void TestGroupSidAttribute(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + base.TestValidationOperation(base.GroupIdentifierClaimType, expectedEntityClaimValue, expectedResultCount == 0 ? false : true); + } + + [TestCase("FakeAccount", false)] + [TestCase("yvand@contoso.local", true)] + public void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup) + { + base.TestAugmentationOperation(claimValue, isMemberOfTrustedGroup, UnitTestsHelper.ValidGroupSid); + } +#endif + } +} diff --git a/Yvand.LDAPCPSE.Tests/WrongConfigTests.cs b/Yvand.LDAPCPSE.Tests/WrongConfigTests.cs new file mode 100644 index 0000000..2141176 --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/WrongConfigTests.cs @@ -0,0 +1,107 @@ +using NUnit.Framework; +using System; +using Yvand.LdapClaimsProvider.Configuration; + +namespace Yvand.LdapClaimsProvider.Tests +{ + [TestFixture] + public class WrongConfigNoIdentityClaimTypeTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + ClaimTypeConfig randomClaimTypeConfig = new ClaimTypeConfig + { + ClaimType = UnitTestsHelper.RandomClaimType, + DirectoryObjectClass = "user", + DirectoryObjectAttribute = UnitTestsHelper.RandomDirectoryObjectAttribute, + }; + base.Settings.ClaimTypes = new ClaimTypeConfigCollection(UnitTestsHelper.SPTrust) { randomClaimTypeConfig }; + ConfigurationShouldBeValid = false; + } + + [Test] + public override void CheckSettingsTest() + { + base.CheckSettingsTest(); + } + } + + [TestFixture] + public class WrongConfigMultipleGroupClaimTypesTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + } + + [Test] + public void TestAddSecondGroupClaimType() + { + ClaimTypeConfig newGroupClaimTypeConfig = new ClaimTypeConfig + { + ClaimType = UnitTestsHelper.RandomClaimType, + DirectoryObjectType = DirectoryObjectType.Group, + DirectoryObjectClass = "group", + DirectoryObjectAttribute = UnitTestsHelper.RandomDirectoryObjectAttribute, + }; + Assert.Throws(() => base.Settings.ClaimTypes.Add(newGroupClaimTypeConfig), "ClaimTypes.Add should throw a InvalidOperationException because the configuration is invalid"); + } + } + + [TestFixture] + public class WrongUpdatesOnClaimTypesTests : ClaimsProviderTestsBase + { + protected override void InitializeSettings() + { + base.InitializeSettings(); + } + + [Test] + public void TryAddingWrongClaimTypeConfigTest() + { + ClaimTypeConfig ctConfig = new ClaimTypeConfig(); + + // Add a ClaimTypeConfig with a claim type already set should throw exception InvalidOperationException + ctConfig.ClaimType = UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType; + ctConfig.DirectoryObjectClass = UnitTestsHelper.RandomDirectoryObjectClass; + ctConfig.DirectoryObjectAttribute = UnitTestsHelper.RandomDirectoryObjectAttribute; + Assert.Throws(() => base.Settings.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with a claim type already set should throw exception InvalidOperationException with this message: \"Claim type '{UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType}' already exists in the collection\""); + + // Add a ClaimTypeConfig with IsAdditionalLdapSearchAttribute = false (default value) and LDAPAttribute / DirectoryObjectClass not set should throw an InvalidOperationException + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.DirectoryObjectClass = String.Empty; + ctConfig.DirectoryObjectAttribute = String.Empty; + Assert.Throws(() => base.Settings.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with IsAdditionalLdapSearchAttribute = false (default value) and LDAPAttribute / DirectoryObjectClass not set should throw exception InvalidOperationException with this message: \"Property LDAPAttribute and DirectoryObjectClass are required\""); + + // Add a ClaimTypeConfig with IsAdditionalLdapSearchAttribute = true and ClaimType set should throw an InvalidOperationException + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.DirectoryObjectClass = UnitTestsHelper.RandomDirectoryObjectClass; + ctConfig.DirectoryObjectAttribute = UnitTestsHelper.RandomDirectoryObjectAttribute; + ctConfig.IsAdditionalLdapSearchAttribute = true; + Assert.Throws(() => base.Settings.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with IsAdditionalLdapSearchAttribute = true and ClaimType set should throw exception InvalidOperationException with this message: \"No claim type should be set if IsAdditionalLdapSearchAttribute is set to true\""); + + // Add a ClaimTypeConfig with EntityType 'Group' should should throw an InvalidOperationException + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.DirectoryObjectAttribute = UnitTestsHelper.RandomDirectoryObjectAttribute; + ctConfig.DirectoryObjectClass = UnitTestsHelper.RandomDirectoryObjectClass; + ctConfig.DirectoryObjectType = DirectoryObjectType.Group; + ctConfig.IsAdditionalLdapSearchAttribute = false; + Assert.Throws(() => base.Settings.ClaimTypes.Add(ctConfig), "Add a ClaimTypeConfig with EntityType 'Group' should throw an InvalidOperationException"); + + // Add a valid ClaimTypeConfig should succeed + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.DirectoryObjectAttribute = UnitTestsHelper.RandomDirectoryObjectAttribute; + ctConfig.DirectoryObjectClass = UnitTestsHelper.RandomDirectoryObjectClass; + ctConfig.DirectoryObjectType = DirectoryObjectType.User; + ctConfig.IsAdditionalLdapSearchAttribute = false; + Assert.DoesNotThrow(() => base.Settings.ClaimTypes.Add(ctConfig), $"Add a valid ClaimTypeConfig should succeed"); + + // Add a ClaimTypeConfig twice should throw an InvalidOperationException + Assert.Throws(() => base.Settings.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with a claim type already set should throw exception InvalidOperationException with this message: \"Claim type '{UnitTestsHelper.RandomClaimType}' already exists in the collection\""); + + // Delete the ClaimTypeConfig by calling method ClaimTypeConfigCollection.Remove(ClaimTypeConfig) should succeed + Assert.That(base.Settings.ClaimTypes.Remove(ctConfig), Is.True, $"Delete the ClaimTypeConfig by calling method ClaimTypeConfigCollection.Remove(ClaimTypeConfig) should succeed"); + } + } +} diff --git a/LDAPCP.Tests/LDAPCP.Tests.csproj b/Yvand.LDAPCPSE.Tests/Yvand.LDAPCPSE.Tests.csproj similarity index 55% rename from LDAPCP.Tests/LDAPCP.Tests.csproj rename to Yvand.LDAPCPSE.Tests/Yvand.LDAPCPSE.Tests.csproj index 7977055..2e306aa 100644 --- a/LDAPCP.Tests/LDAPCP.Tests.csproj +++ b/Yvand.LDAPCPSE.Tests/Yvand.LDAPCPSE.Tests.csproj @@ -1,24 +1,19 @@  - + + Debug AnyCPU - {4FA40F51-787E-4287-9120-B5F4FE8C3579} + {90A764E3-68F7-4E55-B557-E46B98340216} Library Properties - LDAPCP.Tests - LDAPCP.Tests + Yvand.LdapClaimsProvider.Tests + Yvand.LDAPCPSE.Tests v4.8 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 15.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest + true - true @@ -28,6 +23,7 @@ DEBUG;TRACE prompt 4 + x64 pdbonly @@ -38,24 +34,35 @@ 4 - + False - ..\LDAPCP\references\SPS2013\Microsoft.SharePoint.dll - False + ..\Yvand.LDAPCPSE\references\SPSE\Microsoft.SharePoint.dll - + + + + + + - - - - - - + + + + + + + + + + + {66f4b88d-7d7e-4435-92de-94810e2b8f9f} + Yvand.LDAPCPSE + @@ -65,24 +72,14 @@ 13.0.3 - 3.13.3 + 4.0.1 - 4.4.2 - runtime; build; native; contentfiles; analyzers; buildtransitive - all + 4.5.0 - - - {66f4b88d-7d7e-4435-92de-94810e2b8f9f} - LDAPCP - False - - - \ No newline at end of file diff --git a/Yvand.LDAPCPSE.Tests/local.runsettings b/Yvand.LDAPCPSE.Tests/local.runsettings new file mode 100644 index 0000000..1f715c3 --- /dev/null +++ b/Yvand.LDAPCPSE.Tests/local.runsettings @@ -0,0 +1,23 @@ + + + + x64 + + + + + + + + + + + + + + + + + + + diff --git a/LDAPCP.sln b/Yvand.LDAPCPSE.sln similarity index 65% rename from LDAPCP.sln rename to Yvand.LDAPCPSE.sln index 1af56db..36baaaa 100644 --- a/LDAPCP.sln +++ b/Yvand.LDAPCPSE.sln @@ -3,12 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34302.85 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LDAPCP", "LDAPCP\LDAPCP.csproj", "{66F4B88D-7D7E-4435-92DE-94810E2B8F9F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yvand.LDAPCPSE", "Yvand.LDAPCPSE\Yvand.LDAPCPSE.csproj", "{66F4B88D-7D7E-4435-92DE-94810E2B8F9F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LDAPCP.Tests", "LDAPCP.Tests\LDAPCP.Tests.csproj", "{4FA40F51-787E-4287-9120-B5F4FE8C3579}" - ProjectSection(ProjectDependencies) = postProject - {66F4B88D-7D7E-4435-92DE-94810E2B8F9F} = {66F4B88D-7D7E-4435-92DE-94810E2B8F9F} - EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yvand.LDAPCPSE.Tests", "Yvand.LDAPCPSE.Tests\Yvand.LDAPCPSE.Tests.csproj", "{90A764E3-68F7-4E55-B557-E46B98340216}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,10 +19,10 @@ Global {66F4B88D-7D7E-4435-92DE-94810E2B8F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {66F4B88D-7D7E-4435-92DE-94810E2B8F9F}.Release|Any CPU.Build.0 = Release|Any CPU {66F4B88D-7D7E-4435-92DE-94810E2B8F9F}.Release|Any CPU.Deploy.0 = Release|Any CPU - {4FA40F51-787E-4287-9120-B5F4FE8C3579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4FA40F51-787E-4287-9120-B5F4FE8C3579}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FA40F51-787E-4287-9120-B5F4FE8C3579}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4FA40F51-787E-4287-9120-B5F4FE8C3579}.Release|Any CPU.Build.0 = Release|Any CPU + {90A764E3-68F7-4E55-B557-E46B98340216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90A764E3-68F7-4E55-B557-E46B98340216}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90A764E3-68F7-4E55-B557-E46B98340216}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90A764E3-68F7-4E55-B557-E46B98340216}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LDAPCP/Features/LDAPClaimProvider/LDAPClaimProvider.EventReceiver.cs b/Yvand.LDAPCPSE/Features/LDAPClaimProvider/LDAPClaimProvider.EventReceiver.cs similarity index 100% rename from LDAPCP/Features/LDAPClaimProvider/LDAPClaimProvider.EventReceiver.cs rename to Yvand.LDAPCPSE/Features/LDAPClaimProvider/LDAPClaimProvider.EventReceiver.cs diff --git a/LDAPCP/Features/LDAPClaimProvider/LDAPClaimProvider.Template.xml b/Yvand.LDAPCPSE/Features/LDAPClaimProvider/LDAPClaimProvider.Template.xml similarity index 100% rename from LDAPCP/Features/LDAPClaimProvider/LDAPClaimProvider.Template.xml rename to Yvand.LDAPCPSE/Features/LDAPClaimProvider/LDAPClaimProvider.Template.xml diff --git a/LDAPCP/Features/LDAPClaimProvider/LDAPClaimProvider.feature b/Yvand.LDAPCPSE/Features/LDAPClaimProvider/LDAPClaimProvider.feature similarity index 100% rename from LDAPCP/Features/LDAPClaimProvider/LDAPClaimProvider.feature rename to Yvand.LDAPCPSE/Features/LDAPClaimProvider/LDAPClaimProvider.feature diff --git a/LDAPCP/Features/LdapcpAdministration/LdapcpAdministration.Template.xml b/Yvand.LDAPCPSE/Features/LdapcpAdministration/LdapcpAdministration.Template.xml similarity index 100% rename from LDAPCP/Features/LdapcpAdministration/LdapcpAdministration.Template.xml rename to Yvand.LDAPCPSE/Features/LdapcpAdministration/LdapcpAdministration.Template.xml diff --git a/LDAPCP/Features/LdapcpAdministration/LdapcpAdministration.feature b/Yvand.LDAPCPSE/Features/LdapcpAdministration/LdapcpAdministration.feature similarity index 100% rename from LDAPCP/Features/LdapcpAdministration/LdapcpAdministration.feature rename to Yvand.LDAPCPSE/Features/LdapcpAdministration/LdapcpAdministration.feature diff --git a/LDAPCP/Features/LDAPCP.Administration/LDAPCP.Administration.Template.xml b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE.Administration/Yvand.LDAPCPSE.Administration.Template.xml similarity index 100% rename from LDAPCP/Features/LDAPCP.Administration/LDAPCP.Administration.Template.xml rename to Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE.Administration/Yvand.LDAPCPSE.Administration.Template.xml diff --git a/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE.Administration/Yvand.LDAPCPSE.Administration.feature b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE.Administration/Yvand.LDAPCPSE.Administration.feature new file mode 100644 index 0000000..fc7dcf9 --- /dev/null +++ b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE.Administration/Yvand.LDAPCPSE.Administration.feature @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LDAPCP/Features/LDAPCP/LDAPCP.EventReceiver.cs b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.EventReceiver.cs similarity index 52% rename from LDAPCP/Features/LDAPCP/LDAPCP.EventReceiver.cs rename to Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.EventReceiver.cs index dee0dd1..5606d8e 100644 --- a/LDAPCP/Features/LDAPCP/LDAPCP.EventReceiver.cs +++ b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.EventReceiver.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; -namespace ldapcp +namespace Yvand.LdapClaimsProvider { /// /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade. @@ -14,15 +14,15 @@ namespace ldapcp /// The GUID attached to this class may be used during packaging and should not be modified. /// [Guid("91e8e631-b3be-4d05-84c4-8653bddac278")] - public class LDAPCPEventReceiver : SPClaimProviderFeatureReceiver + public class LDAPCPSEEventReceiver : SPClaimProviderFeatureReceiver { - public override string ClaimProviderAssembly => typeof(LDAPCP).Assembly.FullName; + public override string ClaimProviderAssembly => typeof(LDAPCPSE).Assembly.FullName; - public override string ClaimProviderDescription => LDAPCP._ProviderInternalName; + public override string ClaimProviderDescription => LDAPCPSE.ClaimsProviderName; - public override string ClaimProviderDisplayName => LDAPCP._ProviderInternalName; + public override string ClaimProviderDisplayName => LDAPCPSE.ClaimsProviderName; - public override string ClaimProviderType => typeof(LDAPCP).FullName; + public override string ClaimProviderType => typeof(LDAPCPSE).FullName; public override void FeatureActivated(SPFeatureReceiverProperties properties) { @@ -38,21 +38,21 @@ private void ExecBaseFeatureActivated(SPFeatureReceiverProperties properties) { try { - ClaimsProviderLogging svc = ClaimsProviderLogging.Local; - ClaimsProviderLogging.Log($"[{LDAPCP._ProviderInternalName}] Activating farm-scoped feature for claims provider \"{LDAPCP._ProviderInternalName}\"", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - LDAPCPConfig existingConfig = LDAPCPConfig.GetConfiguration(ClaimsProviderConstants.CONFIG_NAME); - if (existingConfig == null) - { - LDAPCPConfig.CreateDefaultConfiguration(); - } - else - { - ClaimsProviderLogging.Log($"[{LDAPCP._ProviderInternalName}] Use configuration \"{existingConfig.Name}\" found in the configuration database", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - } + Logger svc = Logger.Local; + Logger.Log($"[{LDAPCPSE.ClaimsProviderName}] Activating farm-scoped feature for claims provider \"{LDAPCPSE.ClaimsProviderName}\"", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + //LDAPCPConfig existingConfig = LDAPCPConfig.GetConfiguration(ClaimsProviderConstants.CONFIG_NAME); + //if (existingConfig == null) + //{ + // LDAPCPConfig.CreateDefaultConfiguration(); + //} + //else + //{ + // Logger.Log($"[{LDAPCPSE.ClaimsProviderName}] Use configuration \"{existingConfig.Name}\" found in the configuration database", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + //} } catch (Exception ex) { - ClaimsProviderLogging.LogException(LDAPCP._ProviderInternalName, $"activating farm-scoped feature for {LDAPCP._ProviderInternalName}", ClaimsProviderLogging.TraceCategory.Configuration, ex); + Logger.LogException((string)LDAPCPSE.ClaimsProviderName, $"activating farm-scoped feature for claims provider \"{LDAPCPSE.ClaimsProviderName}\"", TraceCategory.Configuration, ex); } }); } @@ -63,13 +63,13 @@ public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { try { - ClaimsProviderLogging.Log($"[{LDAPCP._ProviderInternalName}] Uninstalling farm-scoped feature for claims provider \"{LDAPCP._ProviderInternalName}\": Deleting configuration from the farm", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - LDAPCPConfig.DeleteConfiguration(ClaimsProviderConstants.CONFIG_NAME); - ClaimsProviderLogging.Unregister(); + Logger.Log($"[{LDAPCPSE.ClaimsProviderName}] Uninstalling farm-scoped feature for claims provider \"{LDAPCPSE.ClaimsProviderName}\": Deleting configuration from the farm", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + //LDAPCPConfig.DeleteConfiguration(ClaimsProviderConstants.CONFIG_NAME); + Logger.Unregister(); } catch (Exception ex) { - ClaimsProviderLogging.LogException(LDAPCP._ProviderInternalName, $"deactivating farm-scoped feature for claims provider \"{LDAPCP._ProviderInternalName}\"", ClaimsProviderLogging.TraceCategory.Configuration, ex); + Logger.LogException(LDAPCPSE.ClaimsProviderName, $"uninstalling farm-scoped feature for claims provider \"{LDAPCPSE.ClaimsProviderName}\"", TraceCategory.Configuration, ex); } }); } @@ -80,12 +80,12 @@ public override void FeatureDeactivating(SPFeatureReceiverProperties properties) { try { - ClaimsProviderLogging.Log($"[{LDAPCP._ProviderInternalName}] Deactivating farm-scoped feature for claims provider \"{LDAPCP._ProviderInternalName}\": Removing claims provider from the farm (but not its configuration)", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - base.RemoveClaimProvider(LDAPCP._ProviderInternalName); + Logger.Log($"[{LDAPCPSE.ClaimsProviderName}] Deactivating farm-scoped feature for claims provider \"{LDAPCPSE.ClaimsProviderName}\": Removing claims provider from the farm (but not its configuration)", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + base.RemoveClaimProvider(LDAPCPSE.ClaimsProviderName); } catch (Exception ex) { - ClaimsProviderLogging.LogException(LDAPCP._ProviderInternalName, $"deactivating farm-scoped feature for claims provider \"{LDAPCP._ProviderInternalName}\"", ClaimsProviderLogging.TraceCategory.Configuration, ex); + Logger.LogException(LDAPCPSE.ClaimsProviderName, $"deactivating farm-scoped feature for claims provider \"{LDAPCPSE.ClaimsProviderName}\"", TraceCategory.Configuration, ex); } }); } diff --git a/LDAPCP/Features/LDAPCP/LDAPCP.Template.xml b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.Template.xml similarity index 100% rename from LDAPCP/Features/LDAPCP/LDAPCP.Template.xml rename to Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.Template.xml diff --git a/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.feature b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.feature new file mode 100644 index 0000000..45fe126 --- /dev/null +++ b/Yvand.LDAPCPSE/Features/Yvand.LDAPCPSE/Yvand.LDAPCPSE.feature @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/LDAPCP/LDAPCP.AdminLinks/Elements.xml b/Yvand.LDAPCPSE/LDAPCPSE.Administration.Links/Elements.xml similarity index 52% rename from LDAPCP/LDAPCP.AdminLinks/Elements.xml rename to Yvand.LDAPCPSE/LDAPCPSE.Administration.Links/Elements.xml index 8cc3681..badb8c0 100644 --- a/LDAPCP/LDAPCP.AdminLinks/Elements.xml +++ b/Yvand.LDAPCPSE/LDAPCPSE.Administration.Links/Elements.xml @@ -1,37 +1,37 @@  + Title = "LDAPCP Second Edition - Claims provider for LDAP and Active Directory" + ImageUrl="/_layouts/15/LDAPCPSE/LDAPCP_logo_small.png"> - + - + - + diff --git a/LDAPCP/LDAPCP.AdminLinks/SharePointProjectItem.spdata b/Yvand.LDAPCPSE/LDAPCPSE.Administration.Links/SharePointProjectItem.spdata similarity index 77% rename from LDAPCP/LDAPCP.AdminLinks/SharePointProjectItem.spdata rename to Yvand.LDAPCPSE/LDAPCPSE.Administration.Links/SharePointProjectItem.spdata index 472c521..fdecb7c 100644 --- a/LDAPCP/LDAPCP.AdminLinks/SharePointProjectItem.spdata +++ b/Yvand.LDAPCPSE/LDAPCPSE.Administration.Links/SharePointProjectItem.spdata @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/LDAPCP/LdapcpAdminLinks/Elements.xml b/Yvand.LDAPCPSE/LdapcpAdminLinks/Elements.xml similarity index 100% rename from LDAPCP/LdapcpAdminLinks/Elements.xml rename to Yvand.LDAPCPSE/LdapcpAdminLinks/Elements.xml diff --git a/LDAPCP/LdapcpAdminLinks/SharePointProjectItem.spdata b/Yvand.LDAPCPSE/LdapcpAdminLinks/SharePointProjectItem.spdata similarity index 100% rename from LDAPCP/LdapcpAdminLinks/SharePointProjectItem.spdata rename to Yvand.LDAPCPSE/LdapcpAdminLinks/SharePointProjectItem.spdata diff --git a/LDAPCP/Package/Package.Template.xml b/Yvand.LDAPCPSE/Package/Package.Template.xml similarity index 100% rename from LDAPCP/Package/Package.Template.xml rename to Yvand.LDAPCPSE/Package/Package.Template.xml diff --git a/Yvand.LDAPCPSE/Package/Package.package b/Yvand.LDAPCPSE/Package/Package.package new file mode 100644 index 0000000..58d14e5 --- /dev/null +++ b/Yvand.LDAPCPSE/Package/Package.package @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/LDAPCP/Properties/AssemblyInfo.cs b/Yvand.LDAPCPSE/Properties/AssemblyInfo.cs similarity index 100% rename from LDAPCP/Properties/AssemblyInfo.cs rename to Yvand.LDAPCPSE/Properties/AssemblyInfo.cs diff --git a/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx new file mode 100644 index 0000000..fd87511 --- /dev/null +++ b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx @@ -0,0 +1,582 @@ +<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="GlobalSettings.ascx.cs" Inherits="Yvand.LdapClaimsProvider.Administration.GlobalSettingsUserControl" %> +<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> +<%@ Register TagPrefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> +<%@ Register TagPrefix="wssuc" TagName="InputFormSection" Src="~/_controltemplates/InputFormSection.ascx" %> +<%@ Register TagPrefix="wssuc" TagName="InputFormControl" Src="~/_controltemplates/InputFormControl.ascx" %> +<%@ Register TagPrefix="wssuc" TagName="ButtonSection" Src="~/_controltemplates/ButtonSection.ascx" %> +<%@ Register TagPrefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ "<%= UserIdentifierEncodedValuePrefix %>" +
+
+ +
+
+ +
+ +
+ + + + + + + + +
+
+ +
+ "<%= GroupIdentifierEncodedValuePrefix %>" +
+
+ +
+
+ +
+ +

+ +

+
+
+ + + + + + + + + + +
+
+ +
+ +
+ + + + + + + + +
+
+ +
+
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + +
+
+ Settings for new LDAP connection +
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
  7. +
    Select the authentication type to use (optional):
    + +
  8. +
+
+
+
+
+
+
+

+ + +

+
+
+
+ Settings that uniquely identify a user +
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
+
+
+ Additional settings +
    + <%--
  1. + + + + + + + +
    +
  2. --%> +
  3. + + +
  4. +
  5. + + +
  6. +
  7. + + +
  8. +
  9. + + +
  10. +
+
+ +
+
+
+
+ Settings that uniquely identify a group +
    +
  1. + + + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
+
+
+ Additional settings +
    +
  1. + + +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
  7. + + +
  8. +
+
+
+
+ +
+ + + + +
+ + LDAP Server "": + + +
+
+
+
+
+
+
diff --git a/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx.cs b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx.cs similarity index 51% rename from LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx.cs rename to Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx.cs index 36a0ad2..9fc2a16 100644 --- a/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx.cs +++ b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx.cs @@ -6,46 +6,22 @@ using System.DirectoryServices; using System.Linq; using System.Web.UI.WebControls; -using static ldapcp.ClaimsProviderLogging; +using Yvand.LdapClaimsProvider.Configuration; +using AuthenticationTypes = System.DirectoryServices.AuthenticationTypes; +using DirectoryConnection = Yvand.LdapClaimsProvider.Configuration.DirectoryConnection; -namespace ldapcp.ControlTemplates +namespace Yvand.LdapClaimsProvider.Administration { - public partial class GlobalSettings : LdapcpUserControl + public partial class GlobalSettingsUserControl : LDAPCPSEUserControl { - protected bool ShowValidateSection - { - get { return ValidateSection.Visible; } - set - { - ValidateTopSection.Visible = value; - ValidateSection.Visible = value; - } - } - - protected bool ShowCurrentLdapConnectionSection - { - get { return CurrentLdapConnectionSection.Visible; } - set { CurrentLdapConnectionSection.Visible = value; } - } - - protected bool ShowNewLdapConnectionSection - { - get { return NewLdapConnectionSection.Visible; } - set { NewLdapConnectionSection.Visible = value; } - } - - protected bool ShowAugmentationSection - { - get { return AugmentationSection.Visible; } - set { AugmentationSection.Visible = value; } - } + public new string UserIdentifierEncodedValuePrefix = String.Empty; // This must be a member to be accessible from marup code, it cannot be a property + public new string GroupIdentifierEncodedValuePrefix = String.Empty; // This must be a member to be accessible from marup code, it cannot be a property - readonly string TextErrorNoGroupClaimType = "There is no claim type associated with an entity type 'FormsRole' or 'SecurityGroup'."; - readonly string TextErrorLDAPFieldsMissing = "Some mandatory fields are missing."; - readonly string TextErrorTestLdapConnection = "Unable to connect to LDAP for following reason:
{0}
It may be expected if w3wp process of central admin has intentionally no access to LDAP server."; readonly string TextConnectionSuccessful = "Connection successful."; + readonly string TextSummaryPersistedObjectInformation = "Found configuration '{0}' v{1} (Persisted Object ID: '{2}')"; readonly string TextSharePointDomain = "Connect to SharePoint domain"; - readonly string TextUpdateAdditionalLdapFilterOk = "LDAP filter was successfully applied to all LDAP attributes of class 'user'."; + readonly string TextErrorLDAPFieldsMissing = "Some mandatory fields are missing."; + readonly string TextErrorTestLdapConnection = "Unable to connect to LDAP for following reason:
{0}
It may be expected if w3wp process of central admin has intentionally no access to LDAP server."; protected void Page_Load(object sender, EventArgs e) { @@ -62,7 +38,6 @@ protected void Initialize() ViewState["IsDefaultADConnectionCreated"] = false; ViewState["ForceCheckCustomLdapConnection"] = false; - // Check prerequisite if (ValidatePrerequisite() != ConfigStatus.AllGood) { this.LabelErrorMessage.Text = base.MostImportantError; @@ -71,12 +46,15 @@ protected void Initialize() return; } - PopulateLdapConnectionGrid(); + LabelMessage.Text = String.Format(TextSummaryPersistedObjectInformation, Configuration.Name, Configuration.Version, Configuration.Id); + UserIdentifierEncodedValuePrefix = base.UserIdentifierEncodedValuePrefix; + GroupIdentifierEncodedValuePrefix = base.GroupIdentifierEncodedValuePrefix; + PopulateConnectionsGrid(); if (!this.IsPostBack) { PopulateCblAuthenticationTypes(); + PopulateFields(); InitializeAugmentation(); - InitializeGeneralSettings(); } // Handle password storage in ViewState @@ -93,53 +71,36 @@ protected void Initialize() private void InitializeAugmentation() { - IEnumerable potentialGroupClaimTypes = PersistedObject.ClaimTypes.Where(x => x.EntityType == DirectoryObjectType.Group && !x.UseMainClaimTypeOfDirectoryObject); - if (potentialGroupClaimTypes == null || potentialGroupClaimTypes.Count() == 0) - { - LabelErrorMessage.Text = TextErrorNoGroupClaimType; - return; - } - - foreach (var potentialGroup in potentialGroupClaimTypes) - { - DdlClaimTypes.Items.Add(potentialGroup.ClaimType); - } - - ChkEnableAugmentation.Checked = PersistedObject.EnableAugmentation; - - if (!String.IsNullOrEmpty(PersistedObject.MainGroupClaimType) && DdlClaimTypes.Items.FindByValue(PersistedObject.MainGroupClaimType) != null) - { - DdlClaimTypes.SelectedValue = PersistedObject.MainGroupClaimType; - } + ChkEnableAugmentation.Checked = Settings.EnableAugmentation; // Initialize grid for LDAP connections - var spDomainCoco = PersistedObject.LDAPConnectionsProp.FirstOrDefault(x => x.UseSPServerConnectionToAD); + var spDomainCoco = Settings.LdapConnections.FirstOrDefault(x => x.UseDefaultADConnection); if (spDomainCoco != null) { - spDomainCoco.LDAPPath = TextSharePointDomain; + spDomainCoco.LdapPath = TextSharePointDomain; } - GridLdapConnections.DataSource = PersistedObject.LDAPConnectionsProp; + GridLdapConnections.DataSource = Settings.LdapConnections; GridLdapConnections.DataKeyNames = new string[] { "Identifier" }; GridLdapConnections.DataBind(); } - void PopulateLdapConnectionGrid() + void PopulateConnectionsGrid() { - if (PersistedObject.LDAPConnectionsProp != null) + if (Settings.LdapConnections != null) { PropertyCollectionBinder pcb = new PropertyCollectionBinder(); - foreach (LDAPConnection coco in PersistedObject.LDAPConnectionsProp) + foreach (DirectoryConnection ldapConnection in Settings.LdapConnections) { - if (coco.UseSPServerConnectionToAD) + if (ldapConnection.UseDefaultADConnection) { ViewState["IsDefaultADConnectionCreated"] = true; - pcb.AddRow(coco.Identifier, TextSharePointDomain, "Process account"); + pcb.AddRow(ldapConnection.Identifier, TextSharePointDomain, "Process account"); } else { - pcb.AddRow(coco.Identifier, coco.LDAPPath, coco.LDAPUsername); + pcb.AddRow(ldapConnection.Identifier, ldapConnection.LdapPath, ldapConnection.Username); } } pcb.BindGrid(grdLDAPConnections); @@ -175,152 +136,220 @@ protected static Dictionary ParseEnumTypeAuthenticationTypes() return list; } - private void InitializeGeneralSettings() + private void PopulateFields() { - this.ChkIdentityShowAdditionalAttribute.Checked = PersistedObject.DisplayLdapMatchForIdentityClaimTypeProp; - if (String.IsNullOrEmpty(IdentityCTConfig.LDAPAttributeToShowAsDisplayText)) - { - this.RbIdentityDefault.Checked = true; - } - else + // User identifier settings + this.lblUserIdClaimType.Text = Settings.ClaimTypes.IdentityClaim.ClaimType; + this.TxtUserIdLdapClass.Text = Settings.ClaimTypes.IdentityClaim.DirectoryObjectClass; + this.TxtUserIdLdapAttribute.Text = Settings.ClaimTypes.IdentityClaim.DirectoryObjectAttribute; + this.TxtUserIdDisplayTextAttribute.Text = Settings.ClaimTypes.IdentityClaim.DirectoryObjectAttributeForDisplayText; + this.TxtUserIdAdditionalLdapAttributes.Text = String.Join(",", Settings.ClaimTypes.GetSearchAttributesForEntity(DirectoryObjectType.User)); + this.TxtUserIdLeadingToken.Text = Settings.ClaimTypes.IdentityClaim.ClaimValueLeadingToken; + this.TxtUserIdAdditionalLdapFilter.Text = Settings.ClaimTypes.IdentityClaim.DirectoryObjectAdditionalFilter; + + // Group identifier settings + var possibleGroupClaimTypes = Utils.GetNonWellKnownUserClaimTypesFromTrust(base.ClaimsProviderName); + foreach (string possibleGroupClaimType in possibleGroupClaimTypes) + { + ListItem possibleGroupClaimTypeItem = new ListItem(possibleGroupClaimType); + this.DdlGroupClaimType.Items.Add(possibleGroupClaimTypeItem); + } + + ClaimTypeConfig groupCtc = Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.Group); + if (groupCtc != null) + { + this.DdlGroupClaimType.SelectedValue = groupCtc.ClaimType; + this.TxtGroupLdapClass.Text = groupCtc.DirectoryObjectClass; + this.TxtGroupLdapAttribute.Text = groupCtc.DirectoryObjectAttribute; + this.TxtGroupDisplayTextAttribute.Text = groupCtc.DirectoryObjectAttributeForDisplayText; + this.TxtGroupAdditionalLdapAttributes.Text = String.Join(",", Settings.ClaimTypes.GetSearchAttributesForEntity(DirectoryObjectType.Group)); + this.TxtGroupLeadingToken.Text = groupCtc.ClaimValueLeadingToken; + this.TxtGroupAdditionalLdapFilter.Text = groupCtc.DirectoryObjectAdditionalFilter; + } + + // Other settings + this.ChkAlwaysResolveUserInput.Checked = Settings.AlwaysResolveUserInput; + this.ChkFilterEnabledUsersOnly.Checked = Settings.FilterEnabledUsersOnly; + this.ChkFilterSecurityGroupsOnly.Checked = Settings.FilterSecurityGroupsOnly; + this.ChkFilterExactMatchOnly.Checked = Settings.FilterExactMatchOnly; + this.txtTimeout.Text = Settings.Timeout.ToString(); + } + + /// + /// Parse checkbox list CblAuthenticationTypes to find authentication modes selected + /// + /// + AuthenticationTypes GetSelectedAuthenticationTypes(bool ClearSelection) + { + AuthenticationTypes authNTypes = 0; + foreach (ListItem item in this.CblAuthenticationTypes.Items) { - this.RbIdentityCustomLDAP.Checked = true; - this.TxtLdapAttributeToDisplay.Text = IdentityCTConfig.LDAPAttributeToShowAsDisplayText; + if (!item.Selected) { continue; } + int selectedType; + if (!Int32.TryParse(item.Value, out selectedType)) { continue; } + authNTypes += selectedType; + if (ClearSelection) + { + item.Selected = false; + } } - this.TxtUserIdentifierLDAPClass.Text = IdentityCTConfig.LDAPClass; - this.TxtUserIdentifierLDAPAttribute.Text = IdentityCTConfig.LDAPAttribute; - - this.ChkAlwaysResolveUserInput.Checked = PersistedObject.BypassLDAPLookup; - this.ChkFilterEnabledUsersOnly.Checked = PersistedObject.FilterEnabledUsersOnlyProp; - this.ChkFilterSecurityGroupsOnly.Checked = PersistedObject.FilterSecurityGroupsOnlyProp; - this.ChkFilterExactMatchOnly.Checked = PersistedObject.FilterExactMatchOnlyProp; - this.txtTimeout.Text = PersistedObject.LDAPQueryTimeout.ToString(); - // Deprecated options that are not shown anymore in LDAPCP configuration page - //this.ChkAddWildcardInFront.Checked = PersistedObject.AddWildcardInFrontOfQueryProp; - //this.TxtPickerEntityGroupName.Text = PersistedObject.PickerEntityGroupNameProp; + return authNTypes; } - protected bool UpdateConfiguration(bool commitChanges) + protected void grdLDAPConnections_RowDeleting(object sender, GridViewDeleteEventArgs e) { - if (ValidatePrerequisite() != ConfigStatus.AllGood) { return false; } - UpdateGeneralSettings(); - UpdateAugmentationSettings(); - if (commitChanges) + if (ValidatePrerequisite() != ConfigStatus.AllGood) { return; } + if (Settings.LdapConnections == null) { return; } + + GridViewRow rowToDelete = grdLDAPConnections.Rows[e.RowIndex]; + Guid Id = new Guid(rowToDelete.Cells[0].Text); + DirectoryConnection tenantToRemove = Settings.LdapConnections.FirstOrDefault(x => x.Identifier == Id); + if (tenantToRemove != null) { + Settings.LdapConnections.Remove(tenantToRemove); CommitChanges(); + Logger.Log($"Microsoft Entra ID tenant '{tenantToRemove.LdapPath}' was successfully removed from configuration '{ConfigurationName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); + LabelMessage.Text = String.Format(TextSummaryPersistedObjectInformation, Configuration.Name, Configuration.Version, Configuration.Id); + PopulateConnectionsGrid(); + InitializeAugmentation(); } - return true; } - private void UpdateGeneralSettings() + protected bool UpdateConfiguration(bool commitChanges) { - // Handle identity claim type - PersistedObject.DisplayLdapMatchForIdentityClaimTypeProp = this.ChkIdentityShowAdditionalAttribute.Checked; - if (this.RbIdentityCustomLDAP.Checked) + if (ValidatePrerequisite() != ConfigStatus.AllGood) { return false; } + + // User identifier settings + Settings.ClaimTypes.IdentityClaim.DirectoryObjectClass = this.TxtUserIdLdapClass.Text; + Settings.ClaimTypes.IdentityClaim.DirectoryObjectAttribute = this.TxtUserIdLdapAttribute.Text; + Settings.ClaimTypes.IdentityClaim.DirectoryObjectAttributeForDisplayText = this.TxtUserIdDisplayTextAttribute.Text; + Settings.ClaimTypes.SetSearchAttributesForEntity(this.TxtUserIdAdditionalLdapAttributes.Text, Settings.ClaimTypes.IdentityClaim.DirectoryObjectClass, DirectoryObjectType.User); + Settings.ClaimTypes.IdentityClaim.ClaimValueLeadingToken = this.TxtUserIdLeadingToken.Text; + Settings.ClaimTypes.SetAdditionalLdapFilterForEntity(this.TxtUserIdAdditionalLdapAttributes.Text, DirectoryObjectType.User); + + // Group identifier settings + ClaimTypeConfig groupConfig = Settings.ClaimTypes.GetIdentifierConfiguration(DirectoryObjectType.Group); + bool newGroupConfigObject = false; + if (groupConfig == null) { - IdentityCTConfig.LDAPAttributeToShowAsDisplayText = this.TxtLdapAttributeToDisplay.Text; + groupConfig = new ClaimTypeConfig { DirectoryObjectType = DirectoryObjectType.Group }; + newGroupConfigObject = true; } - else + groupConfig.ClaimType = this.DdlGroupClaimType.SelectedValue; + groupConfig.DirectoryObjectClass = this.TxtGroupLdapClass.Text; + groupConfig.DirectoryObjectAttribute = this.TxtGroupLdapAttribute.Text; + groupConfig.DirectoryObjectAttributeForDisplayText = this.TxtGroupDisplayTextAttribute.Text; + Settings.ClaimTypes.SetSearchAttributesForEntity(this.TxtGroupAdditionalLdapAttributes.Text, groupConfig.DirectoryObjectClass, DirectoryObjectType.Group); + groupConfig.ClaimValueLeadingToken = this.TxtGroupLeadingToken.Text; + Settings.ClaimTypes.SetAdditionalLdapFilterForEntity(this.TxtGroupAdditionalLdapFilter.Text, DirectoryObjectType.Group); + if (newGroupConfigObject) { - IdentityCTConfig.LDAPAttributeToShowAsDisplayText = String.Empty; + Settings.ClaimTypes.Add(groupConfig); } - if (!String.IsNullOrWhiteSpace(TxtUserIdentifierLDAPClass.Text) && !String.IsNullOrWhiteSpace(TxtUserIdentifierLDAPAttribute.Text)) + // Augmentation settings + foreach (GridViewRow item in GridLdapConnections.Rows) { - PersistedObject.ClaimTypes.UpdateUserIdentifier(TxtUserIdentifierLDAPClass.Text, TxtUserIdentifierLDAPAttribute.Text); + CheckBox chkAugEn = (CheckBox)item.FindControl("ChkAugmentationEnableOnCoco"); + CheckBox chkIsADDomain = (CheckBox)item.FindControl("ChkGetGroupMembershipAsADDomain"); + TextBox txtId = (TextBox)item.FindControl("IdPropHidden"); + + DirectoryConnection ldapConnection = Settings.LdapConnections.First(x => x.Identifier == new Guid(txtId.Text)); + ldapConnection.EnableAugmentation = chkAugEn.Checked; + ldapConnection.GetGroupMembershipUsingDotNetHelpers = chkIsADDomain.Checked; } - PersistedObject.BypassLDAPLookup = this.ChkAlwaysResolveUserInput.Checked; - PersistedObject.FilterEnabledUsersOnlyProp = this.ChkFilterEnabledUsersOnly.Checked; - PersistedObject.FilterSecurityGroupsOnlyProp = this.ChkFilterSecurityGroupsOnly.Checked; - PersistedObject.FilterExactMatchOnlyProp = this.ChkFilterExactMatchOnly.Checked; - PersistedObject.EnableAugmentation = ChkEnableAugmentation.Checked; - PersistedObject.MainGroupClaimType = DdlClaimTypes.SelectedValue.Equals("none", StringComparison.InvariantCultureIgnoreCase) ? String.Empty : DdlClaimTypes.SelectedValue; - // Deprecated options that are not shown anymore in LDAPCP configuration page - //PersistedObject.AddWildcardInFrontOfQuery = this.ChkAddWildcardInFront.Checked; - //PersistedObject.PickerEntityGroupName = this.TxtPickerEntityGroupName.Text; + Settings.AlwaysResolveUserInput = this.ChkAlwaysResolveUserInput.Checked; + Settings.FilterEnabledUsersOnly = this.ChkFilterEnabledUsersOnly.Checked; + Settings.FilterSecurityGroupsOnly = this.ChkFilterSecurityGroupsOnly.Checked; + Settings.FilterExactMatchOnly = this.ChkFilterExactMatchOnly.Checked; + Settings.EnableAugmentation = this.ChkEnableAugmentation.Checked; int timeOut; if (!Int32.TryParse(this.txtTimeout.Text, out timeOut) || timeOut < 0) { - timeOut = ClaimsProviderConstants.LDAPCPCONFIG_TIMEOUT; //set to default if unable to parse + timeOut = ClaimsProviderConstants.DEFAULT_TIMEOUT; //set to default if unable to parse } - PersistedObject.LDAPQueryTimeout = timeOut; - } + Settings.Timeout = timeOut; - private void UpdateAugmentationSettings() - { - foreach (GridViewRow item in GridLdapConnections.Rows) + foreach (var userAttr in this.Settings.ClaimTypes.Where(x => x.DirectoryObjectType == DirectoryObjectType.User)) { - CheckBox chkAugEn = (CheckBox)item.FindControl("ChkAugmentationEnableOnCoco"); - CheckBox chkIsADDomain = (CheckBox)item.FindControl("ChkGetGroupMembershipAsADDomain"); - TextBox txtId = (TextBox)item.FindControl("IdPropHidden"); - - var coco = PersistedObject.LDAPConnectionsProp.First(x => x.Identifier == new Guid(txtId.Text)); - coco.EnableAugmentation = chkAugEn.Checked; - coco.GetGroupMembershipUsingDotNetHelpers = chkIsADDomain.Checked; + userAttr.DirectoryObjectAdditionalFilter = this.TxtUserIdAdditionalLdapFilter.Text; } + + if (commitChanges) { CommitChanges(); } + return true; } - protected void BtnOK_Click(Object sender, EventArgs e) + protected void BtnTestLdapConnection_Click(Object sender, EventArgs e) { - if (ValidatePrerequisite() != ConfigStatus.AllGood) + this.ValidateLdapConnection(); + } + + protected void ValidateLdapConnection() + { + ViewState["ForceCheckCustomLdapConnection"] = true; + if (this.TxtLdapConnectionString.Text == String.Empty || this.TxtLdapPassword.Text == String.Empty || this.TxtLdapUsername.Text == String.Empty) { + this.LabelErrorTestLdapConnection.Text = TextErrorLDAPFieldsMissing; return; } - if (UpdateConfiguration(true)) + + DirectoryEntry de = null; + DirectorySearcher deSearch = new DirectorySearcher(); + try { - Response.Redirect("/Security.aspx", false); + AuthenticationTypes authNTypes = GetSelectedAuthenticationTypes(false); + de = new DirectoryEntry(this.TxtLdapConnectionString.Text, this.TxtLdapUsername.Text, this.TxtLdapPassword.Text, authNTypes); + deSearch.SearchRoot = de; + deSearch.FindOne(); + this.LabelTestLdapConnectionOK.Text = TextConnectionSuccessful; } - else + catch (Exception ex) { - LabelErrorMessage.Text = base.MostImportantError; + Logger.LogException(ClaimsProviderName, "while testing LDAP connection", TraceCategory.Configuration, ex); + this.LabelErrorTestLdapConnection.Text = String.Format(TextErrorTestLdapConnection, ex.Message); + } + finally + { + if (deSearch != null) { deSearch.Dispose(); } + if (de != null) { de.Dispose(); } } - } - - protected void BtnResetLDAPCPConfig_Click(Object sender, EventArgs e) - { - ResetConfiguration(); - } - - protected virtual void ResetConfiguration() - { - LDAPCPConfig.DeleteConfiguration(PersistedObjectName); - Response.Redirect(Request.RawUrl, false); - } - protected void BtnUpdateAdditionalUserLdapFilter_Click(Object sender, EventArgs e) - { - UpdateAdditionalUserLdapFilter(); + // Required to set radio buttons of LDAP connections correctly in UI + PopulateConnectionsGrid(); } - void UpdateAdditionalUserLdapFilter() + protected void BtnOK_Click(Object sender, EventArgs e) { - if (PersistedObject == null) { return; } - foreach (var userAttr in this.PersistedObject.ClaimTypes.Where(x => x.EntityType == DirectoryObjectType.User)) + if (ValidatePrerequisite() != ConfigStatus.AllGood) { return; } + if (UpdateConfiguration(true)) { - userAttr.AdditionalLDAPFilter = this.TxtAdditionalUserLdapFilter.Text; + Response.Redirect("/Security.aspx", false); + } + else + { + LabelErrorMessage.Text = base.MostImportantError; } - this.CommitChanges(); - LabelUpdateAdditionalLdapFilterOk.Text = this.TextUpdateAdditionalLdapFilterOk; } - protected void BtnTestLdapConnection_Click(Object sender, EventArgs e) + protected void BtnResetConfig_Click(Object sender, EventArgs e) { - this.ValidateLdapConnection(); + LdapProviderConfiguration.DeleteGlobalConfiguration(ConfigurationID); + Response.Redirect(Request.RawUrl, false); } protected void BtnAddLdapConnection_Click(object sender, EventArgs e) { - AddLdapConnection(); + AddTenantConnection(); } /// - /// Add new LDAP connection to collection in persisted object + /// Add new Microsoft Entra ID tenant in persisted object /// - void AddLdapConnection() + void AddTenantConnection() { - if (ValidatePrerequisite() != ConfigStatus.AllGood) return; + if (ValidatePrerequisite() != ConfigStatus.AllGood) { return; } if (this.RbUseCustomConnection.Checked && (this.TxtLdapConnectionString.Text == String.Empty || this.TxtLdapUsername.Text == String.Empty || this.TxtLdapPassword.Text == String.Empty)) { @@ -330,10 +359,10 @@ void AddLdapConnection() if (this.RbUseServerDomain.Checked) { - PersistedObject.LDAPConnectionsProp.Add( - new LDAPConnection + Settings.LdapConnections.Add( + new DirectoryConnection { - UseSPServerConnectionToAD = true , + UseDefaultADConnection = true, EnableAugmentation = true, } ); @@ -341,23 +370,22 @@ void AddLdapConnection() else { AuthenticationTypes authNType = GetSelectedAuthenticationTypes(true); - PersistedObject.LDAPConnectionsProp.Add( - new LDAPConnection + Settings.LdapConnections.Add( + new DirectoryConnection { - UseSPServerConnectionToAD = false, - LDAPPath = this.TxtLdapConnectionString.Text, - LDAPUsername = this.TxtLdapUsername.Text, - LDAPPassword = this.TxtLdapPassword.Text, - AuthenticationSettings = authNType, + UseDefaultADConnection = false, + LdapPath = this.TxtLdapConnectionString.Text, + Username = this.TxtLdapUsername.Text, + Password = this.TxtLdapPassword.Text, + AuthenticationType = authNType, EnableAugmentation = true, } ); } CommitChanges(); - ClaimsProviderLogging.Log($"LDAP server '{this.TxtLdapConnectionString.Text}' was successfully added in configuration '{PersistedObjectName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); - - PopulateLdapConnectionGrid(); + Logger.Log($"LDAP server '{this.TxtLdapConnectionString.Text}' was successfully added to configuration '{ConfigurationName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); + PopulateConnectionsGrid(); InitializeAugmentation(); ViewState["LDAPpwd"] = String.Empty; this.TxtLdapPassword.Attributes.Remove("value"); @@ -365,79 +393,6 @@ void AddLdapConnection() this.TxtLdapUsername.Text = String.Empty; this.TxtLdapPassword.Text = String.Empty; } - - protected void ValidateLdapConnection() - { - ViewState["ForceCheckCustomLdapConnection"] = true; - if (this.TxtLdapConnectionString.Text == String.Empty || this.TxtLdapPassword.Text == String.Empty || this.TxtLdapUsername.Text == String.Empty) - { - this.LabelErrorTestLdapConnection.Text = TextErrorLDAPFieldsMissing; - return; - } - - DirectoryEntry de = null; - DirectorySearcher deSearch = new DirectorySearcher(); - try - { - AuthenticationTypes authNTypes = GetSelectedAuthenticationTypes(false); - de = new DirectoryEntry(this.TxtLdapConnectionString.Text, this.TxtLdapUsername.Text, this.TxtLdapPassword.Text, authNTypes); - deSearch.SearchRoot = de; - deSearch.FindOne(); - this.LabelTestLdapConnectionOK.Text = TextConnectionSuccessful; - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ClaimsProviderName, "while testing LDAP connection", TraceCategory.Configuration, ex); - this.LabelErrorTestLdapConnection.Text = String.Format(TextErrorTestLdapConnection, ex.Message); - } - finally - { - if (deSearch != null) { deSearch.Dispose(); } - if (de != null) { de.Dispose(); } - } - - // Required to set radio buttons of LDAP connections correctly in UI - PopulateLdapConnectionGrid(); - } - - protected void grdLDAPConnections_RowDeleting(object sender, GridViewDeleteEventArgs e) - { - if (ValidatePrerequisite() != ConfigStatus.AllGood) { return; } - if (PersistedObject.LDAPConnectionsProp == null) { return; } - - GridViewRow rowToDelete = grdLDAPConnections.Rows[e.RowIndex]; - Guid Id = new Guid(rowToDelete.Cells[0].Text); - LDAPConnection connectionToRemove = PersistedObject.LDAPConnectionsProp.FirstOrDefault(x => x.Identifier == Id); - if (connectionToRemove != null) - { - PersistedObject.LDAPConnectionsProp.Remove(connectionToRemove); - CommitChanges(); - ClaimsProviderLogging.Log($"LDAP server '{connectionToRemove.Directory}' was successfully removed from configuration '{PersistedObjectName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); - InitializeAugmentation(); - PopulateLdapConnectionGrid(); - } - } - - /// - /// Parse checkbox list CblAuthenticationTypes to find authentication modes selected - /// - /// - AuthenticationTypes GetSelectedAuthenticationTypes(bool ClearSelection) - { - AuthenticationTypes authNTypes = 0; - foreach (ListItem item in this.CblAuthenticationTypes.Items) - { - if (!item.Selected) { continue; } - int selectedType; - if (!Int32.TryParse(item.Value, out selectedType)) { continue; } - authNTypes += selectedType; - if (ClearSelection) - { - item.Selected = false; - } - } - return authNTypes; - } } public class PropertyCollectionBinder diff --git a/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx.designer.cs b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx.designer.cs similarity index 79% rename from LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx.designer.cs rename to Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx.designer.cs index ae1e908..e95ee63 100644 --- a/LDAPCP/ADMIN/LDAPCP/GlobalSettings.ascx.designer.cs +++ b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.ascx.designer.cs @@ -7,11 +7,22 @@ // //------------------------------------------------------------------------------ -namespace ldapcp.ControlTemplates { - - - public partial class GlobalSettings { - +namespace Yvand.LdapClaimsProvider.Administration +{ + + + public partial class GlobalSettingsUserControl + { + + /// + /// LabelMessage control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Label LabelMessage; + /// /// LabelErrorMessage control. /// @@ -20,16 +31,16 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Label LabelErrorMessage; - + /// - /// ValidateTopSection control. + /// ValSummary control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.UserControl ValidateTopSection; - + protected global::System.Web.UI.WebControls.ValidationSummary ValSummary; + /// /// BtnOKTop control. /// @@ -38,7 +49,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Button BtnOKTop; - + /// /// CurrentLdapConnectionSection control. /// @@ -47,7 +58,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.UserControl CurrentLdapConnectionSection; - + /// /// grdLDAPConnections control. /// @@ -56,7 +67,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.SPGridView grdLDAPConnections; - + /// /// NewLdapConnectionSection control. /// @@ -65,7 +76,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.UserControl NewLdapConnectionSection; - + /// /// RbUseServerDomain control. /// @@ -74,7 +85,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.InputFormRadioButton RbUseServerDomain; - + /// /// RbUseCustomConnection control. /// @@ -83,7 +94,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.InputFormRadioButton RbUseCustomConnection; - + /// /// TxtLdapConnectionString control. /// @@ -92,7 +103,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtLdapConnectionString; - + /// /// TxtLdapUsername control. /// @@ -101,7 +112,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtLdapUsername; - + /// /// TxtLdapPassword control. /// @@ -110,7 +121,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtLdapPassword; - + /// /// CblAuthenticationTypes control. /// @@ -119,7 +130,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::Microsoft.SharePoint.WebControls.InputFormCheckBoxList CblAuthenticationTypes; - + /// /// BtnTestLdapConnection control. /// @@ -128,7 +139,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Button BtnTestLdapConnection; - + /// /// BtnAddLdapConnection control. /// @@ -137,7 +148,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Button BtnAddLdapConnection; - + /// /// LabelErrorTestLdapConnection control. /// @@ -146,7 +157,7 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Label LabelErrorTestLdapConnection; - + /// /// LabelTestLdapConnectionOK control. /// @@ -155,196 +166,223 @@ public partial class GlobalSettings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Label LabelTestLdapConnectionOK; - + /// - /// AugmentationSection control. + /// lblUserIdClaimType control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.UserControl AugmentationSection; - + protected global::Microsoft.SharePoint.WebControls.EncodedLiteral lblUserIdClaimType; + /// - /// Label1 control. + /// TxtUserIdLdapClass control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.Label Label1; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtUserIdLdapClass; + /// - /// ChkEnableAugmentation control. + /// TxtUserIdLdapAttribute control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.CheckBox ChkEnableAugmentation; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtUserIdLdapAttribute; + /// - /// DdlClaimTypes control. + /// TxtUserIdDisplayTextAttribute control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.DropDownList DdlClaimTypes; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtUserIdDisplayTextAttribute; + /// - /// GridLdapConnections control. + /// TxtUserIdAdditionalLdapAttributes control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::Microsoft.SharePoint.WebControls.SPGridView GridLdapConnections; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtUserIdAdditionalLdapAttributes; + /// - /// txtTimeout control. + /// TxtUserIdLeadingToken control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::Microsoft.SharePoint.WebControls.InputFormTextBox txtTimeout; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtUserIdLeadingToken; + + /// + /// TxtUserIdAdditionalLdapFilter control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtUserIdAdditionalLdapFilter; + + /// + /// AugmentationSection control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.UserControl AugmentationSection; + /// - /// TxtUserIdentifierLDAPClass control. + /// Label1 control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.TextBox TxtUserIdentifierLDAPClass; - + protected global::System.Web.UI.WebControls.Label Label1; + /// - /// TxtUserIdentifierLDAPAttribute control. + /// DdlGroupClaimType control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.TextBox TxtUserIdentifierLDAPAttribute; - + protected global::System.Web.UI.WebControls.DropDownList DdlGroupClaimType; + /// - /// RbIdentityDefault control. + /// TxtGroupLdapClass control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::Microsoft.SharePoint.WebControls.InputFormRadioButton RbIdentityDefault; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtGroupLdapClass; + /// - /// RbIdentityCustomLDAP control. + /// TxtGroupLdapAttribute control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::Microsoft.SharePoint.WebControls.InputFormRadioButton RbIdentityCustomLDAP; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtGroupLdapAttribute; + /// - /// TxtLdapAttributeToDisplay control. + /// TxtGroupDisplayTextAttribute control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtLdapAttributeToDisplay; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtGroupDisplayTextAttribute; + /// - /// ChkIdentityShowAdditionalAttribute control. + /// TxtGroupAdditionalLdapAttributes control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.CheckBox ChkIdentityShowAdditionalAttribute; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtGroupAdditionalLdapAttributes; + /// - /// ChkAlwaysResolveUserInput control. + /// TxtGroupLeadingToken control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.CheckBox ChkAlwaysResolveUserInput; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtGroupLeadingToken; + /// - /// ChkFilterEnabledUsersOnly control. + /// TxtGroupAdditionalLdapFilter control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.CheckBox ChkFilterEnabledUsersOnly; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtGroupAdditionalLdapFilter; + /// - /// ChkFilterSecurityGroupsOnly control. + /// ChkEnableAugmentation control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.CheckBox ChkFilterSecurityGroupsOnly; - + protected global::System.Web.UI.WebControls.CheckBox ChkEnableAugmentation; + /// - /// ChkFilterExactMatchOnly control. + /// GridLdapConnections control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.CheckBox ChkFilterExactMatchOnly; - + protected global::Microsoft.SharePoint.WebControls.SPGridView GridLdapConnections; + + /// + /// ChkFilterEnabledUsersOnly control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.CheckBox ChkFilterEnabledUsersOnly; + /// - /// TxtAdditionalUserLdapFilter control. + /// ChkFilterSecurityGroupsOnly control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::Microsoft.SharePoint.WebControls.InputFormTextBox TxtAdditionalUserLdapFilter; - + protected global::System.Web.UI.WebControls.CheckBox ChkFilterSecurityGroupsOnly; + /// - /// BtnUpdateAdditionalUserLdapFilter control. + /// txtTimeout control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.Button BtnUpdateAdditionalUserLdapFilter; - + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox txtTimeout; + /// - /// LabelUpdateAdditionalLdapFilterOk control. + /// ChkAlwaysResolveUserInput control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.Label LabelUpdateAdditionalLdapFilterOk; - + protected global::System.Web.UI.WebControls.CheckBox ChkAlwaysResolveUserInput; + /// - /// BtnResetLDAPCPConfig control. + /// ChkFilterExactMatchOnly control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.Button BtnResetLDAPCPConfig; - + protected global::System.Web.UI.WebControls.CheckBox ChkFilterExactMatchOnly; + /// - /// ValidateSection control. + /// BtnResetConfig control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.UserControl ValidateSection; - + protected global::System.Web.UI.WebControls.Button BtnResetConfig; + /// /// BtnOK control. /// diff --git a/LDAPCP/ADMIN/LDAPCP/LdapcpSettings.aspx b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.aspx similarity index 52% rename from LDAPCP/ADMIN/LDAPCP/LdapcpSettings.aspx rename to Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.aspx index e38e7c5..848b9a1 100644 --- a/LDAPCP/ADMIN/LDAPCP/LdapcpSettings.aspx +++ b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/GlobalSettings.aspx @@ -1,17 +1,18 @@ <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> <%@ Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" MasterPageFile="~/_admin/admin.master" %> -<%@ Register TagPrefix="Ldapcp" TagName="GlobalSettings" src="GlobalSettings.ascx" %> -<%@ Import Namespace="ldapcp" %> +<%@ Register TagPrefix="LDAPCP" TagName="GlobalSettings" src="GlobalSettings.ascx" %> +<%@ Import Namespace="Yvand.LdapClaimsProvider.Configuration" %> +<%@ Import Namespace="Yvand.LdapClaimsProvider" %> <%@ Import Namespace="System.Diagnostics" %> <%@ Import Namespace="System.Reflection" %> -LDAPCP Configuration +LDAPCP Second Edition - Configuration -<%= String.Format("LDAPCP {0} - LDAPCP.com", FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LDAPCP)).Location).FileVersion, ClaimsProviderConstants.PUBLICSITEURL) %> + <%= String.Format("LDAPCP Second Edition {0}", ClaimsProviderConstants.ClaimsProviderVersion, ClaimsProviderConstants.PUBLICSITEURL) %> - +
diff --git a/LDAPCP/ADMIN/LDAPCP/ClaimTypesConfig.ascx b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/PermsMappings.ascx similarity index 98% rename from LDAPCP/ADMIN/LDAPCP/ClaimTypesConfig.ascx rename to Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/PermsMappings.ascx index 221080c..4d54838 100644 --- a/LDAPCP/ADMIN/LDAPCP/ClaimTypesConfig.ascx +++ b/Yvand.LDAPCPSE/TEMPLATE/ADMIN/LDAPCPSE/PermsMappings.ascx @@ -1,8 +1,8 @@ <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ClaimTypesConfig.ascx.cs" Inherits="ldapcp.ControlTemplates.ClaimTypesConfigUserControl" %> +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="PermsMappings.ascx.cs" Inherits="Yvand.LdapClaimsProvider.Administration.PermsMappingsUserControl" %> <%@ Register TagPrefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> - +