diff --git a/AzureCP.Tests/AzureCP.Tests.csproj b/AzureCP.Tests/AzureCP.Tests.csproj index 2f2596ac..2cd139f7 100644 --- a/AzureCP.Tests/AzureCP.Tests.csproj +++ b/AzureCP.Tests/AzureCP.Tests.csproj @@ -6,8 +6,8 @@ {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88} Library Properties - AzureCP.Tests - AzureCP.Tests + Yvand.ClaimsProviders.Tests + AzureCPSE.Tests v4.8 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} @@ -47,17 +47,16 @@ - + + - - - - + + diff --git a/AzureCP.Tests/BackupCurrentConfig.cs b/AzureCP.Tests/BackupCurrentConfig.cs deleted file mode 100644 index 23abc2c4..00000000 --- a/AzureCP.Tests/BackupCurrentConfig.cs +++ /dev/null @@ -1,46 +0,0 @@ -using azurecp; -using NUnit.Framework; -using System; -using System.Diagnostics; - -namespace AzureCP.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 AzureCPConfig Config; - private AzureCPConfig BackupConfig; - - [OneTimeSetUp] - public void Init() - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} Start backup of current AzureCP configuration"); - Config = AzureCPConfig.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 = AzureCPConfig.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 AzureCP configuration"); - } - } -} diff --git a/AzureCP.Tests/BasicTests.cs b/AzureCP.Tests/BasicTests.cs new file mode 100644 index 00000000..08a5b974 --- /dev/null +++ b/AzureCP.Tests/BasicTests.cs @@ -0,0 +1,66 @@ +using NUnit.Framework; + +namespace Yvand.ClaimsProviders.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + internal class BasicTests : EntityTestsBase + { + [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void SearchEntities(SearchEntityData registrationData) + { + base.SearchEntities(registrationData); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [MaxTime(UnitTestsHelper.MaxTime)] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void ValidateClaim(ValidateEntityData registrationData) + { + base.ValidateClaim(registrationData); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void AugmentEntity(ValidateEntityData registrationData) + { + base.AugmentEntity(registrationData); + } + +#if DEBUG + ////[TestCaseSource(typeof(SearchEntityDataSourceCollection))] + //public void DEBUG_SearchEntitiesFromCollection(string inputValue, string expectedCount, string expectedClaimValue) + //{ + // if (!TestSearch) { return; } + + // TestSearchOperation(inputValue, Convert.ToInt32(expectedCount), expectedClaimValue); + //} + + [TestCase(@"AADGroup1130", 1, "e86ace87-37ba-4ee1-8087-ecd783728233")] + [TestCase(@"xyzguest", 0, "xyzGUEST@contoso.com")] + [TestCase(@"AzureGr}", 1, "ef7d18e6-5c4d-451a-9663-a976be81c91e")] + [TestCase(@"aad", 30, "")] + [TestCase(@"AADGroup", 30, "")] + public override void SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.SearchEntities(inputValue, expectedResultCount, expectedEntityClaimValue); + } + + [TestCase("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "ef7d18e6-5c4d-451a-9663-a976be81c91e", true)] + [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "FakeGuest@contoso.com", false)] + [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "FakeGuest.com#EXT#@XXX.onmicrosoft.com", false)] + [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "FakeGuest.com#EXT#@XXX.onmicrosoft.com", false)] + public override void ValidateClaim(string claimType, string claimValue, bool shouldValidate) + { + base.ValidateClaim(claimType, claimValue, shouldValidate); + } + + [TestCase("xydGUEST@FAKE.onmicrosoft.com", false)] + public override void AugmentEntity(string claimValue, bool shouldHavePermissions) + { + base.AugmentEntity(claimValue, shouldHavePermissions); + } +#endif + } +} diff --git a/AzureCP.Tests/CustomConfigTests.cs b/AzureCP.Tests/CustomConfigTests.cs index 2c24b2d5..c8901987 100644 --- a/AzureCP.Tests/CustomConfigTests.cs +++ b/AzureCP.Tests/CustomConfigTests.cs @@ -1,94 +1,78 @@ -using azurecp; -using Microsoft.SharePoint.Administration.Claims; +using Microsoft.SharePoint.Administration.Claims; using NUnit.Framework; -using System; -using System.Linq; using System.Security.Claims; +using Yvand.ClaimsProviders.AzureAD; +using Yvand.ClaimsProviders.Config; -namespace AzureCP.Tests +namespace Yvand.ClaimsProviders.Tests { - [TestFixture] - public class CustomConfigTests : BackupCurrentConfig + public class CustomConfigTestsBase : EntityTestsBase { public static string GroupsClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType; public override void InitializeConfiguration() { base.InitializeConfiguration(); - - // Extra initialization for current test class - Config.EnableAugmentation = true; - Config.ClaimTypes.GetByClaimType(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType).PrefixToBypassLookup = "bypass-user:"; - Config.ClaimTypes.GetByClaimType(UnitTestsHelper.TrustedGroupToAdd_ClaimType).PrefixToBypassLookup = "bypass-group:"; - Config.Update(); + Settings.EnableAugmentation = true; + Settings.ClaimTypes.GetByClaimType(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType).PrefixToBypassLookup = "bypass-user:"; + Settings.ClaimTypes.GetByClaimType(UnitTestsHelper.TrustedGroupToAdd_ClaimType).PrefixToBypassLookup = "bypass-group:"; + ClaimTypeConfig ctConfigExtensionAttribute = new ClaimTypeConfig + { + ClaimType = TestContext.Parameters["MultiPurposeCustomClaimType"], + ClaimTypeDisplayName = "extattr1", + EntityProperty = DirectoryObjectProperty.extensionAttribute1, + SharePointEntityType = "FormsRole", + }; + Settings.ClaimTypes.Add(ctConfigExtensionAttribute); + GlobalConfiguration.ApplySettings(Settings, true); } + } + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class CustomConfigTests : CustomConfigTestsBase + { [TestCase("bypass-user:externalUser@contoso.com", 1, "externalUser@contoso.com")] [TestCase("externalUser@contoso.com", 0, "")] [TestCase("bypass-user:", 0, "")] public void BypassLookupOnIdentityClaimTest(string inputValue, int expectedCount, string expectedClaimValue) { - UnitTestsHelper.TestSearchOperation(inputValue, expectedCount, expectedClaimValue); + 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); + 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) + [TestCase("val", 1, "value1")] // Extension attribute configuration + public override void SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) { - 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); - } + base.SearchEntities(inputValue, expectedResultCount, expectedEntityClaimValue); } [Test] [NonParallelizable] public void BypassServer() { - Config.AlwaysResolveUserInput = true; - Config.Update(); - + Settings.AlwaysResolveUserInput = true; + GlobalConfiguration.ApplySettings(Settings, true); try { - UnitTestsHelper.TestSearchOperation(UnitTestsHelper.RandomClaimValue, 2, UnitTestsHelper.RandomClaimValue); + TestSearchOperation(UnitTestsHelper.RandomClaimValue, 3, 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); + TestValidationOperation(inputClaim, true, UnitTestsHelper.RandomClaimValue); } finally { - Config.AlwaysResolveUserInput = false; - Config.Update(); + Settings.AlwaysResolveUserInput = false; + GlobalConfiguration.ApplySettings(Settings, true); } } - - //[Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] - ////[Repeat(UnitTestsHelper.TestRepeatCount)] - //public void RequireExactMatchDuringSearch(ValidateEntityData registrationData) - //{ - // Config.FilterExactMatchOnly = true; - // Config.Update(); - - // try - // { - // int expectedCount = registrationData.ShouldValidate ? 1 : 0; - // UnitTestsHelper.TestSearchOperation(registrationData.ClaimValue, expectedCount, registrationData.ClaimValue); - // } - // finally - // { - // Config.FilterExactMatchOnly = false; - // Config.Update(); - // } - //} } } diff --git a/AzureCP.Tests/CustomizeConfigTests.cs b/AzureCP.Tests/CustomizeConfigTests.cs new file mode 100644 index 00000000..87696043 --- /dev/null +++ b/AzureCP.Tests/CustomizeConfigTests.cs @@ -0,0 +1,171 @@ +using NUnit.Framework; +using System; +using System.Linq; +using Yvand.ClaimsProviders.Config; + +namespace Yvand.ClaimsProviders.Tests +{ + [TestFixture] + [NonParallelizable] + public class CustomizeConfigTests : EntityTestsBase + { + 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.EntityProperty = UnitTestsHelper.RandomObjectProperty; + Assert.Throws(() => 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 UseMainClaimTypeOfDirectoryObject = false (default value) and DirectoryObjectProperty not set should throw exception InvalidOperationException + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.EntityProperty = DirectoryObjectProperty.NotSet; + Assert.Throws(() => Settings.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = false (default value) and DirectoryObjectProperty not set should throw exception InvalidOperationException with this message: \"Property DirectoryObjectProperty is required\""); + + // Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = true and ClaimType set should throw exception InvalidOperationException + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.EntityProperty = UnitTestsHelper.RandomObjectProperty; + ctConfig.UseMainClaimTypeOfDirectoryObject = true; + Assert.Throws(() => Settings.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 throw exception InvalidOperationException since 1 already exists by default and AzureCP allows only 1 claim type for EntityType 'Group' + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.EntityProperty = UnitTestsHelper.RandomObjectProperty; + ctConfig.EntityType = DirectoryObjectType.Group; + ctConfig.UseMainClaimTypeOfDirectoryObject = false; + Assert.Throws(() => Settings.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with EntityType 'Group' should throw exception InvalidOperationException with this message: \"A claim type for EntityType 'Group' already exists in the collection\""); + + // Add a valid ClaimTypeConfig should succeed + ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; + ctConfig.EntityProperty = UnitTestsHelper.RandomObjectProperty; + ctConfig.EntityType = DirectoryObjectType.User; + ctConfig.UseMainClaimTypeOfDirectoryObject = false; + Assert.DoesNotThrow(() => Settings.ClaimTypes.Add(ctConfig), $"Add a valid ClaimTypeConfig should succeed"); + + // Add a ClaimTypeConfig twice should throw exception InvalidOperationException + Assert.Throws(() => 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.IsTrue(Settings.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(() => Settings.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 = Settings.ClaimTypes.FirstOrDefault(x => String.Equals(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); + Assert.Throws(() => Settings.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(() => GlobalConfiguration.ApplySettings(Settings, true), $"Modify identity claim type to set its EntityType to Group should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); + } + + [Test] + public void DuplicateClaimType() + { + var firstCTConfig = Settings.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, EntityProperty = UnitTestsHelper.RandomObjectProperty }; + Assert.Throws(() => Settings.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 + ClaimTypeConfig anotherCTConfig = Settings.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType) && !String.Equals(firstCTConfig.ClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); + anotherCTConfig.ClaimType = firstCTConfig.ClaimType; + Assert.Throws(() => GlobalConfiguration.ApplySettings(Settings, true), $"Modify an existing claim type to set a claim type already defined should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); + } + + [Test] + public void DuplicatePrefixToBypassLookup() + { + string prefixToBypassLookup = "test:"; + + // Set a duplicate PrefixToBypassLookup on 2 items already existing in the list should throw exception InvalidOperationException + Settings.ClaimTypes.Where(x => !String.IsNullOrEmpty(x.ClaimType)).Take(2).Select(x => x.PrefixToBypassLookup = prefixToBypassLookup).ToList(); + Assert.Throws(() => GlobalConfiguration.ApplySettings(Settings, true), $"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 = Settings.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType)); + firstCTConfig.PrefixToBypassLookup = prefixToBypassLookup; + ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = UnitTestsHelper.RandomClaimType, PrefixToBypassLookup = prefixToBypassLookup, EntityProperty = UnitTestsHelper.RandomObjectProperty }; + Assert.Throws(() => GlobalConfiguration.ApplySettings(Settings, true), $"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 + Settings.ClaimTypes.Where(x => x.EntityType == DirectoryObjectType.User).Take(2).Select(x => x.EntityDataKey = entityDataKey).ToList(); + Assert.Throws(() => GlobalConfiguration.ApplySettings(Settings, true), $"Duplicate EntityDataKey on 2 items already existing in the list should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); + + // Remove one of the duplicated EntityDataKey + Settings.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, EntityProperty = UnitTestsHelper.RandomObjectProperty }; + Assert.Throws(() => Settings.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 DuplicateDirectoryObjectProperty() + { + ClaimTypeConfig existingCTConfig = Settings.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType) && x.EntityType == DirectoryObjectType.User); + + // Create a new ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException + ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = UnitTestsHelper.RandomClaimType, EntityType = DirectoryObjectType.User, EntityProperty = existingCTConfig.EntityProperty }; + Assert.Throws(() => Settings.ClaimTypes.Add(ctConfig), $"Create a new ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException with this message: \"An item with property '{existingCTConfig.EntityProperty.ToString()}' already exists for the object type 'User'\""); + + // Add a valid ClaimTypeConfig should succeed (done for next test) + ctConfig.EntityProperty = UnitTestsHelper.RandomObjectProperty; + Assert.DoesNotThrow(() => Settings.ClaimTypes.Add(ctConfig), $"Add a valid ClaimTypeConfig should succeed"); + + // Update an existing ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException + ctConfig.EntityProperty = existingCTConfig.EntityProperty; + Assert.Throws(() => GlobalConfiguration.ApplySettings(Settings, true), $"Update an existing ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException with this message: \"{ConfigUpdateErrorMessage}\""); + + // Delete the ClaimTypeConfig should succeed + Assert.IsTrue(Settings.ClaimTypes.Remove(ctConfig), "Delete the ClaimTypeConfig should succeed"); + } + + [Test] + public void ModifyUserIdentifier() + { + IdentityClaimTypeConfig backupIdentityCTConfig = Settings.ClaimTypes.FirstOrDefault(x => x is IdentityClaimTypeConfig) as IdentityClaimTypeConfig; + backupIdentityCTConfig = backupIdentityCTConfig.CopyConfiguration() as IdentityClaimTypeConfig; + + // Member UserType + Assert.Throws(() => Settings.ClaimTypes.UpdateUserIdentifier(DirectoryObjectProperty.NotSet), $"Update user identifier with value NotSet should throw exception ArgumentNullException"); + + bool configUpdated = Settings.ClaimTypes.UpdateUserIdentifier(UnitTestsHelper.RandomObjectProperty); + Assert.IsTrue(configUpdated, $"Update user identifier with any DirectoryObjectProperty should succeed and return true"); + + configUpdated = Settings.ClaimTypes.UpdateUserIdentifier(backupIdentityCTConfig.EntityProperty); + Assert.IsTrue(configUpdated, $"Update user identifier with any DirectoryObjectProperty should succeed and return true"); + + configUpdated = Settings.ClaimTypes.UpdateUserIdentifier(backupIdentityCTConfig.EntityProperty); + Assert.IsFalse(configUpdated, $"Update user identifier with the same DirectoryObjectProperty should not change anything and return false"); + + // Guest UserType + Assert.Throws(() => Settings.ClaimTypes.UpdateIdentifierForGuestUsers(DirectoryObjectProperty.NotSet), $"Update user identifier of Guest UserType with value NotSet should throw exception ArgumentNullException"); + + configUpdated = Settings.ClaimTypes.UpdateIdentifierForGuestUsers(UnitTestsHelper.RandomObjectProperty); + Assert.IsTrue(configUpdated, $"Update user identifier of Guest UserType with any DirectoryObjectProperty should succeed and return true"); + + configUpdated = Settings.ClaimTypes.UpdateIdentifierForGuestUsers(backupIdentityCTConfig.DirectoryObjectPropertyForGuestUsers); + Assert.IsTrue(configUpdated, $"Update user identifier of Guest UserType with any DirectoryObjectProperty should succeed and return true"); + + configUpdated = Settings.ClaimTypes.UpdateIdentifierForGuestUsers(backupIdentityCTConfig.DirectoryObjectPropertyForGuestUsers); + Assert.IsFalse(configUpdated, $"Update user identifier of Guest UserType with the same DirectoryObjectProperty should not change anything and return false"); + } + } +} diff --git a/AzureCP.Tests/EntityTestsBase.cs b/AzureCP.Tests/EntityTestsBase.cs index 7ef4c070..08ad6a2b 100644 --- a/AzureCP.Tests/EntityTestsBase.cs +++ b/AzureCP.Tests/EntityTestsBase.cs @@ -1,53 +1,112 @@ 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.ClaimsProviders.Config; -namespace AzureCP.Tests +namespace Yvand.ClaimsProviders.Tests { - [TestFixture] - //[Parallelizable(ParallelScope.Children)] - public class EntityTestsBase : BackupCurrentConfig + public class EntityTestsBase { /// - /// Configure whether to run entity search tests. + /// Configures whether to run entity search tests. /// public virtual bool TestSearch => true; /// - /// Configure whether to run entity validation tests. + /// Configures whether to run entity validation tests. /// public virtual bool TestValidation => true; /// - /// Configure whether to run entity augmentation tests. + /// Configures whether to run entity augmentation tests. /// public virtual bool TestAugmentation => true; /// - /// Configure whether to exclude AAD Guest users from search and validation. This does not impact augmentation. + /// Configures whether to exclude AAD Guest users from search and validation. This does not impact augmentation. /// public virtual bool ExcludeGuestUsers => false; /// - /// Configure whether to exclude AAD Member users from search and validation. This does not impact augmentation. + /// Configures whether to exclude AAD Member users from search and validation. This does not impact augmentation. /// public virtual bool ExcludeMemberUsers => false; - public override void InitializeConfiguration() + /// + /// Configures whether the configuration applied is valid, and whether the claims provider should be able to use it + /// + public virtual bool ConfigurationIsValid => true; + + protected AADEntityProviderConfig GlobalConfiguration; + protected AADEntityProviderSettings Settings = new AADEntityProviderSettings(); + private static IAADSettings OriginalSettings; + + [OneTimeSetUp] + public void Init() + { + GlobalConfiguration = AzureCP.GetConfiguration(true); + if (GlobalConfiguration == null) + { + GlobalConfiguration = AzureCP.CreateConfiguration(); + } + else + { + Settings = (AADEntityProviderSettings)GlobalConfiguration.LocalSettings; + OriginalSettings = GlobalConfiguration.LocalSettings; + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Took a backup of the original settings"); + } + InitializeConfiguration(); + } + + /// + /// Initialize configuration + /// + public virtual void InitializeConfiguration() + { + Settings.ClaimTypes = AADEntityProviderSettings.ReturnDefaultClaimTypesConfig(UnitTestsHelper.ClaimsProvider.Name); + Settings.ProxyAddress = TestContext.Parameters["ProxyAddress"]; + +#if DEBUG + Settings.Timeout = 99999; +#endif + + string json = File.ReadAllText(UnitTestsHelper.AzureTenantsJsonFile); + List azureTenants = JsonConvert.DeserializeObject>(json); + Settings.AzureTenants = azureTenants; + foreach (AzureTenant tenant in azureTenants) + { + tenant.ExcludeMemberUsers = ExcludeMemberUsers; + tenant.ExcludeGuestUsers = ExcludeGuestUsers; + } + GlobalConfiguration.ApplySettings(Settings, true); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Set {Settings.AzureTenants.Count} Azure AD tenants to AzureCP configuration"); + } + + [OneTimeTearDown] + public void Cleanup() { - base.InitializeConfiguration(); - Config.EnableAugmentation = true; - foreach (var tenant in Config.AzureTenants) + try + { + if (OriginalSettings != null) + { + GlobalConfiguration.ApplySettings(OriginalSettings, true); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Restored original settings of AzureCP configuration"); + } + } + catch (Exception ex) { - tenant.ExcludeGuests = ExcludeGuestUsers; - tenant.ExcludeMembers = ExcludeMemberUsers; + Trace.TraceError($"{DateTime.Now.ToString("s")} Unexpected error while restoring the original settings of AzureCP configuration: {ex.Message}"); } - Config.Update(); } - [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] - [Repeat(UnitTestsHelper.TestRepeatCount)] public virtual void SearchEntities(SearchEntityData registrationData) { if (!TestSearch) @@ -55,94 +114,214 @@ public virtual void SearchEntities(SearchEntityData registrationData) return; } - // If current entry does not return only users, cannot reliably test number of results returned if guest and/or members should be excluded - if (!String.Equals(registrationData.ResultType, "User", StringComparison.InvariantCultureIgnoreCase) && + // If current entry does not return only users AND either guests or members are excluded, ExpectedResultCount cannot be determined so test cannot run + if (registrationData.SearchResultEntityTypes != ResultEntityType.User && (ExcludeGuestUsers || ExcludeMemberUsers)) { return; } - int expectedResultCount = registrationData.ExpectedResultCount; - if (ExcludeGuestUsers && String.Equals(registrationData.UserType, UnitTestsHelper.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) + int expectedResultCount = registrationData.SearchResultCount; + if (ExcludeGuestUsers && registrationData.SearchResultUserTypes == ResultUserType.Guest) { expectedResultCount = 0; } - if (ExcludeMemberUsers && String.Equals(registrationData.UserType, UnitTestsHelper.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) + if (ExcludeMemberUsers && registrationData.SearchResultUserTypes == ResultUserType.Member) { expectedResultCount = 0; } - UnitTestsHelper.TestSearchOperation(registrationData.Input, expectedResultCount, registrationData.ExpectedEntityClaimValue); + if (Settings.FilterExactMatchOnly == true) + { + expectedResultCount = registrationData.ExactMatch ? 1 : 0; + } + + TestSearchOperation(registrationData.Input, expectedResultCount, registrationData.SearchResultSingleEntityClaimValue); + } + + public virtual void SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + if (!TestSearch) { return; } + + TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); } - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] - [MaxTime(UnitTestsHelper.MaxTime)] - [Repeat(UnitTestsHelper.TestRepeatCount)] public virtual void ValidateClaim(ValidateEntityData registrationData) { if (!TestValidation) { return; } bool shouldValidate = registrationData.ShouldValidate; - if (ExcludeGuestUsers && String.Equals(registrationData.UserType, UnitTestsHelper.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) + if (ExcludeGuestUsers && registrationData.UserType == ResultUserType.Guest) { shouldValidate = false; } - if (ExcludeMemberUsers && String.Equals(registrationData.UserType, UnitTestsHelper.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) + if (ExcludeMemberUsers && registrationData.UserType == ResultUserType.Member) { shouldValidate = false; } - SPClaim inputClaim = new SPClaim(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, shouldValidate, registrationData.ClaimValue); + string claimType = registrationData.EntityType == ResultEntityType.User ? + UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType : + UnitTestsHelper.TrustedGroupToAdd_ClaimType; + + SPClaim inputClaim = new SPClaim(claimType, registrationData.ClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); + TestValidationOperation(inputClaim, shouldValidate, registrationData.ClaimValue); + } + + public virtual void 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)); + TestValidationOperation(inputClaim, shouldValidate, claimValue); } - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] - [Repeat(UnitTestsHelper.TestRepeatCount)] public virtual void AugmentEntity(ValidateEntityData registrationData) { if (!TestAugmentation) { return; } - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup); + TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, registrationData.ClaimValue, registrationData.IsMemberOfTrustedGroup); } -#if DEBUG - //[TestCaseSource(typeof(SearchEntityDataSourceCollection))] - public void DEBUG_SearchEntitiesFromCollection(string inputValue, string expectedCount, string expectedClaimValue) + public virtual void AugmentEntity(string claimValue, bool shouldHavePermissions) { - if (!TestSearch) { return; } + if (!TestAugmentation) { return; } - UnitTestsHelper.TestSearchOperation(inputValue, Convert.ToInt32(expectedCount), expectedClaimValue); + TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, claimValue, shouldHavePermissions); } - [TestCase(@"AADGroup1", 1, "30ef0958-c003-4667-a0ad-ef9783acaf25")] - [TestCase(@"xyzguest", 0, "xyzGUEST@contoso.com")] - [TestCase(@"AzureGr}", 1, "ef7d18e6-5c4d-451a-9663-a976be81c91e")] - public void DEBUG_SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + [Test] + public virtual void ValidateInitialization() { - if (!TestSearch) { return; } + if (ConfigurationIsValid) + { + Assert.IsNotNull(GlobalConfiguration.RefreshLocalSettingsIfNeeded(), "RefreshLocalConfigurationIfNeeded should return a valid configuration"); + Assert.IsTrue(UnitTestsHelper.ClaimsProvider.ValidateLocalConfiguration(null), "ValidateLocalConfiguration should return true because the configuration is valid"); + } + else + { + Assert.IsNull(GlobalConfiguration.RefreshLocalSettingsIfNeeded(), "RefreshLocalConfigurationIfNeeded should return null because the configuration is not valid"); + Assert.IsFalse(UnitTestsHelper.ClaimsProvider.ValidateLocalConfiguration(null), "ValidateLocalConfiguration should return false because the configuration is not valid"); + } + } + + /// + /// 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(); + var entityTypes = new[] { "User", "SecGroup", "SharePointGroup", "System", "FormsRole" }; - UnitTestsHelper.TestSearchOperation(inputValue, expectedResultCount, expectedEntityClaimValue); + 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.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}'."); + } } - //[TestCase("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "5b0f6c56-c87f-44c3-9354-56cba03da433", true)] - [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "FakeGuest@contoso.com", false)] - [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "FakeGuest.com#EXT#@XXX.onmicrosoft.com", false)] - [TestCase("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "FakeGuest.com#EXT#@XXX.onmicrosoft.com", false)] - public void DEBUG_ValidateClaim(string claimType, string claimValue, bool shouldValidate) + public static void VerifySearchTest(List entities, string input, int expectedCount, string expectedClaimValue) { - if (!TestValidation) { return; } + 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; + } + } - SPClaim inputClaim = new SPClaim(claimType, claimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); - UnitTestsHelper.TestValidationOperation(inputClaim, shouldValidate, claimValue); + if (!String.IsNullOrWhiteSpace(expectedClaimValue) && !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()}"); } - [TestCase("xydGUEST@FAKE.onmicrosoft.com", false)] - public void DEBUG_AugmentEntity(string claimValue, bool shouldHavePermissions) + public static void TestValidationOperation(SPClaim inputClaim, bool shouldValidate, string expectedClaimValue) { - if (!TestAugmentation) { return; } + try + { + Stopwatch timer = new Stopwatch(); + timer.Start(); + var entityTypes = new[] { "User" }; - UnitTestsHelper.TestAugmentationOperation(UnitTestsHelper.SPTrust.IdentityClaimTypeInformation.MappedClaimType, claimValue, shouldHavePermissions); + PickerEntity[] entities = UnitTestsHelper.ClaimsProvider.Resolve(UnitTestsHelper.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.TraceInformation($"{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 = UnitTestsHelper.ClaimsProvider.GetClaimsForEntity(context, inputClaim); + + bool groupFound = false; + if (groups != null && groups.Contains(UnitTestsHelper.TrustedGroup)) + { + groupFound = true; + } + + if (isMemberOfTrustedGroup) + { + Assert.IsTrue(groupFound, $"Entity \"{claimValue}\" should be member of group \"{UnitTestsHelper.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 \"{UnitTestsHelper.TrustedGroupToAdd_ClaimValue}\", but this group was found in the claims returned by the claims provider."); + } + timer.Stop(); + Trace.TraceInformation($"{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}'."); + } } -#endif } } diff --git a/AzureCP.Tests/ExcludeAUserTypeTests.cs b/AzureCP.Tests/ExcludeAUserTypeTests.cs new file mode 100644 index 00000000..15731399 --- /dev/null +++ b/AzureCP.Tests/ExcludeAUserTypeTests.cs @@ -0,0 +1,95 @@ +using NUnit.Framework; +using System.Runtime.CompilerServices; + +namespace Yvand.ClaimsProviders.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class ExcludeAllUserAccountsTests : EntityTestsBase + { + public override bool ExcludeGuestUsers => true; + public override bool ExcludeMemberUsers => true; + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void AugmentEntity(ValidateEntityData registrationData) + { + base.AugmentEntity(registrationData); + } + + [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void SearchEntities(SearchEntityData registrationData) + { + base.SearchEntities(registrationData); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [MaxTime(UnitTestsHelper.MaxTime)] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void ValidateClaim(ValidateEntityData registrationData) + { + base.ValidateClaim(registrationData); + } + } + + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class ExcludeGuestUserAccountsTests : EntityTestsBase + { + public override bool ExcludeGuestUsers => true; + public override bool ExcludeMemberUsers => false; + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void AugmentEntity(ValidateEntityData registrationData) + { + base.AugmentEntity(registrationData); + } + + [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void SearchEntities(SearchEntityData registrationData) + { + base.SearchEntities(registrationData); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [MaxTime(UnitTestsHelper.MaxTime)] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void ValidateClaim(ValidateEntityData registrationData) + { + base.ValidateClaim(registrationData); + } + } + + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class ExcludeMemberUserAccountsTests : EntityTestsBase + { + public override bool ExcludeGuestUsers => false; + public override bool ExcludeMemberUsers => true; + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void AugmentEntity(ValidateEntityData registrationData) + { + base.AugmentEntity(registrationData); + } + + [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void SearchEntities(SearchEntityData registrationData) + { + base.SearchEntities(registrationData); + } + + [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [MaxTime(UnitTestsHelper.MaxTime)] + [Repeat(UnitTestsHelper.TestRepeatCount)] + public override void ValidateClaim(ValidateEntityData registrationData) + { + base.ValidateClaim(registrationData); + } + } +} diff --git a/AzureCP.Tests/ExcludeAllUserAccountsTests.cs b/AzureCP.Tests/ExcludeAllUserAccountsTests.cs deleted file mode 100644 index e3a0f4c9..00000000 --- a/AzureCP.Tests/ExcludeAllUserAccountsTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NUnit.Framework; - -namespace AzureCP.Tests -{ - [TestFixture] - public class ExcludeAllUserAccountsTests : EntityTestsBase - { - public override bool ExcludeGuestUsers => false; - public override bool ExcludeMemberUsers => false; - } -} diff --git a/AzureCP.Tests/ExcludeGuestUserAccountsTests.cs b/AzureCP.Tests/ExcludeGuestUserAccountsTests.cs deleted file mode 100644 index e0c5ca17..00000000 --- a/AzureCP.Tests/ExcludeGuestUserAccountsTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NUnit.Framework; - -namespace AzureCP.Tests -{ - [TestFixture] - public class ExcludeGuestUserAccountsTests : EntityTestsBase - { - public override bool ExcludeGuestUsers => true; - public override bool ExcludeMemberUsers => false; - } -} diff --git a/AzureCP.Tests/ExcludeMemberUserAccountsTests.cs b/AzureCP.Tests/ExcludeMemberUserAccountsTests.cs deleted file mode 100644 index 7ec1d618..00000000 --- a/AzureCP.Tests/ExcludeMemberUserAccountsTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NUnit.Framework; - -namespace AzureCP.Tests -{ - [TestFixture] - public class ExcludeMemberUserAccountsTests : EntityTestsBase - { - public override bool ExcludeGuestUsers => false; - public override bool ExcludeMemberUsers => true; - } -} diff --git a/AzureCP.Tests/GuestAccountsUPNTests.cs b/AzureCP.Tests/GuestAccountsUPNTests.cs index f8641fb1..f533d301 100644 --- a/AzureCP.Tests/GuestAccountsUPNTests.cs +++ b/AzureCP.Tests/GuestAccountsUPNTests.cs @@ -1,12 +1,13 @@ -using azurecp; -using NUnit.Framework; +using NUnit.Framework; +using Yvand.ClaimsProviders.Config; -namespace AzureCP.Tests +namespace Yvand.ClaimsProviders.Tests { /// /// Test guest accounts when their identity claim is the UserPrincipalName /// [TestFixture] + [Parallelizable(ParallelScope.Children)] public class GuestAccountsUPNTests : EntityTestsBase { public override void InitializeConfiguration() @@ -14,9 +15,9 @@ public override void InitializeConfiguration() base.InitializeConfiguration(); // Extra initialization for current test class - Config.ClaimTypes.UpdateIdentifierForGuestUsers(AzureADObjectProperty.UserPrincipalName); - Config.EnableAugmentation = true; - Config.Update(); + Settings.ClaimTypes.UpdateIdentifierForGuestUsers(DirectoryObjectProperty.UserPrincipalName); + Settings.EnableAugmentation = true; + GlobalConfiguration.ApplySettings(Settings, true); } [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.UPNB2BGuestAccounts })] @@ -27,12 +28,11 @@ public override void SearchEntities(SearchEntityData registrationData) } [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.UPNB2BGuestAccounts })] - [MaxTime(UnitTestsHelper.MaxTime)] [Repeat(UnitTestsHelper.TestRepeatCount)] public override void ValidateClaim(ValidateEntityData registrationData) { base.ValidateClaim(registrationData); - } + } [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.UPNB2BGuestAccounts })] [Repeat(UnitTestsHelper.TestRepeatCount)] diff --git a/AzureCP.Tests/ModifyConfigTests.cs b/AzureCP.Tests/ModifyConfigTests.cs deleted file mode 100644 index 9252a38e..00000000 --- a/AzureCP.Tests/ModifyConfigTests.cs +++ /dev/null @@ -1,170 +0,0 @@ -using azurecp; -using NUnit.Framework; -using System; -using System.Linq; - -namespace AzureCP.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.DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty; - 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 DirectoryObjectProperty not set should throw exception InvalidOperationException - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.DirectoryObjectProperty = AzureADObjectProperty.NotSet; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = false (default value) and DirectoryObjectProperty not set should throw exception InvalidOperationException with this message: \"Property DirectoryObjectProperty is required\""); - - // Add a ClaimTypeConfig with UseMainClaimTypeOfDirectoryObject = true and ClaimType set should throw exception InvalidOperationException - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty; - 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 throw exception InvalidOperationException since 1 already exists by default and AzureCP allows only 1 claim type for EntityType 'Group' - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty; - ctConfig.EntityType = DirectoryObjectType.Group; - ctConfig.UseMainClaimTypeOfDirectoryObject = false; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Add a ClaimTypeConfig with EntityType 'Group' should throw exception InvalidOperationException with this message: \"A claim type for EntityType 'Group' already exists in the collection\""); - - // Add a valid ClaimTypeConfig should succeed - ctConfig.ClaimType = UnitTestsHelper.RandomClaimType; - ctConfig.DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty; - 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, DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty }; - 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 - ClaimTypeConfig anotherCTConfig = Config.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType) && !String.Equals(firstCTConfig.ClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); - 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}\""); - } - - [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, DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty }; - 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 => x.EntityType == DirectoryObjectType.User).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, DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty }; - 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 DuplicateDirectoryObjectProperty() - { - ClaimTypeConfig existingCTConfig = Config.ClaimTypes.FirstOrDefault(x => !String.IsNullOrEmpty(x.ClaimType) && x.EntityType == DirectoryObjectType.User); - - // Create a new ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException - ClaimTypeConfig ctConfig = new ClaimTypeConfig() { ClaimType = UnitTestsHelper.RandomClaimType, EntityType = DirectoryObjectType.User, DirectoryObjectProperty = existingCTConfig.DirectoryObjectProperty }; - Assert.Throws(() => Config.ClaimTypes.Add(ctConfig), $"Create a new ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException with this message: \"An item with property '{existingCTConfig.DirectoryObjectProperty.ToString()}' already exists for the object type 'User'\""); - - // Add a valid ClaimTypeConfig should succeed (done for next test) - ctConfig.DirectoryObjectProperty = UnitTestsHelper.RandomObjectProperty; - Assert.DoesNotThrow(() => Config.ClaimTypes.Add(ctConfig), $"Add a valid ClaimTypeConfig should succeed"); - - // Update an existing ClaimTypeConfig with a DirectoryObjectProperty already set should throw exception InvalidOperationException - ctConfig.DirectoryObjectProperty = existingCTConfig.DirectoryObjectProperty; - Assert.Throws(() => Config.Update(), $"Update an existing ClaimTypeConfig with a DirectoryObjectProperty 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() - { - IdentityClaimTypeConfig backupIdentityCTConfig = Config.ClaimTypes.FirstOrDefault(x => x is IdentityClaimTypeConfig) as IdentityClaimTypeConfig; - backupIdentityCTConfig = backupIdentityCTConfig.CopyConfiguration() as IdentityClaimTypeConfig; - - // Member UserType - Assert.Throws(() => Config.ClaimTypes.UpdateUserIdentifier(AzureADObjectProperty.NotSet), $"Update user identifier with value NotSet should throw exception ArgumentNullException"); - - bool configUpdated = Config.ClaimTypes.UpdateUserIdentifier(UnitTestsHelper.RandomObjectProperty); - Assert.IsTrue(configUpdated, $"Update user identifier with any AzureADObjectProperty should succeed and return true"); - - configUpdated = Config.ClaimTypes.UpdateUserIdentifier(backupIdentityCTConfig.DirectoryObjectProperty); - Assert.IsTrue(configUpdated, $"Update user identifier with any AzureADObjectProperty should succeed and return true"); - - configUpdated = Config.ClaimTypes.UpdateUserIdentifier(backupIdentityCTConfig.DirectoryObjectProperty); - Assert.IsFalse(configUpdated, $"Update user identifier with the same AzureADObjectProperty should not change anything and return false"); - - // Guest UserType - Assert.Throws(() => Config.ClaimTypes.UpdateIdentifierForGuestUsers(AzureADObjectProperty.NotSet), $"Update user identifier of Guest UserType with value NotSet should throw exception ArgumentNullException"); - - configUpdated = Config.ClaimTypes.UpdateIdentifierForGuestUsers(UnitTestsHelper.RandomObjectProperty); - Assert.IsTrue(configUpdated, $"Update user identifier of Guest UserType with any AzureADObjectProperty should succeed and return true"); - - configUpdated = Config.ClaimTypes.UpdateIdentifierForGuestUsers(backupIdentityCTConfig.DirectoryObjectPropertyForGuestUsers); - Assert.IsTrue(configUpdated, $"Update user identifier of Guest UserType with any AzureADObjectProperty should succeed and return true"); - - configUpdated = Config.ClaimTypes.UpdateIdentifierForGuestUsers(backupIdentityCTConfig.DirectoryObjectPropertyForGuestUsers); - Assert.IsFalse(configUpdated, $"Update user identifier of Guest UserType with the same AzureADObjectProperty should not change anything and return false"); - } - } -} diff --git a/AzureCP.Tests/RequireExactMatchTests.cs b/AzureCP.Tests/RequireExactMatchTests.cs index c02721ba..9b841854 100644 --- a/AzureCP.Tests/RequireExactMatchTests.cs +++ b/AzureCP.Tests/RequireExactMatchTests.cs @@ -1,46 +1,46 @@ using NUnit.Framework; -namespace AzureCP.Tests +namespace Yvand.ClaimsProviders.Tests { [TestFixture] - public class RequireExactMatchOnBaseConfigTests : BackupCurrentConfig + public class RequireExactMatchOnBaseConfigTests : EntityTestsBase { public override void InitializeConfiguration() { base.InitializeConfiguration(); - - // Extra initialization for current test class - Config.FilterExactMatchOnly = true; - Config.Update(); + Settings.FilterExactMatchOnly = true; + GlobalConfiguration.ApplySettings(Settings, true); } - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] [Repeat(UnitTestsHelper.TestRepeatCount)] - public void RequireExactMatchDuringSearch(ValidateEntityData registrationData) + public override void SearchEntities(SearchEntityData registrationData) { - int expectedCount = registrationData.ShouldValidate ? 1 : 0; - UnitTestsHelper.TestSearchOperation(registrationData.ClaimValue, expectedCount, registrationData.ClaimValue); + base.SearchEntities(registrationData); + } + + [TestCase(@"aadgroup1143", 1, "3f4b724c-125d-47b4-b989-195b29417d6e")] + public override void SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.SearchEntities(inputValue, expectedResultCount, expectedEntityClaimValue); } } [TestFixture] - public class RequireExactMatchOnCustomConfigTests : CustomConfigTests + public class RequireExactMatchOnCustomConfigTests : CustomConfigTestsBase { public override void InitializeConfiguration() { base.InitializeConfiguration(); - - // Extra initialization for current test class - Config.FilterExactMatchOnly = true; - Config.Update(); + Settings.FilterExactMatchOnly = true; + GlobalConfiguration.ApplySettings(Settings, true); } - [Test, TestCaseSource(typeof(ValidateEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] + [Test, TestCaseSource(typeof(SearchEntityDataSource), "GetTestData", new object[] { EntityDataSourceType.AllAccounts })] [Repeat(UnitTestsHelper.TestRepeatCount)] - public void RequireExactMatchDuringSearch(ValidateEntityData registrationData) + public override void SearchEntities(SearchEntityData registrationData) { - int expectedCount = registrationData.ShouldValidate ? 1 : 0; - UnitTestsHelper.TestSearchOperation(registrationData.ClaimValue, expectedCount, registrationData.ClaimValue); + base.SearchEntities(registrationData); } } } diff --git a/AzureCP.Tests/UnitTestsHelper.cs b/AzureCP.Tests/UnitTestsHelper.cs index 47835179..6c5ccb76 100644 --- a/AzureCP.Tests/UnitTestsHelper.cs +++ b/AzureCP.Tests/UnitTestsHelper.cs @@ -1,107 +1,108 @@ -using azurecp; -using DataAccess; +using DataAccess; using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; using Microsoft.SharePoint.Administration.Claims; -using Microsoft.SharePoint.WebControls; -using Newtonsoft.Json; using NUnit.Framework; using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; using System.Security.Claims; -using System.Text; +using Yvand.ClaimsProviders.Config; -[SetUpFixture] -public class UnitTestsHelper +namespace Yvand.ClaimsProviders.Tests { - public static readonly azurecp.AzureCP ClaimsProvider = new azurecp.AzureCP(UnitTestsHelper.ClaimsProviderName); - public static string ClaimsProviderName => "AzureCP"; - public static readonly string ClaimsProviderConfigName = TestContext.Parameters["ClaimsProviderConfigName"]; - private static Uri TestSiteCollUri; - public static readonly string TestSiteRelativePath = $"/sites/{TestContext.Parameters["TestSiteCollectionName"]}"; - public const int MaxTime = 50000; - public static readonly string FarmAdmin = TestContext.Parameters["FarmAdmin"]; + [SetUpFixture] + public class UnitTestsHelper + { + public static readonly AzureCP ClaimsProvider = new AzureCP(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"]; #if DEBUG - public const int TestRepeatCount = 1; + 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 AzureADObjectProperty RandomObjectProperty => AzureADObjectProperty.AccountEnabled; - - public static readonly string TrustedGroupToAdd_ClaimType = TestContext.Parameters["TrustedGroupToAdd_ClaimType"]; - 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 string GUEST_USERTYPE => ClaimsProviderConstants.GUEST_USERTYPE; - public static string MEMBER_USERTYPE => ClaimsProviderConstants.MEMBER_USERTYPE; - - public static readonly string AzureTenantsJsonFile = TestContext.Parameters["AzureTenantsJsonFile"]; - public static readonly string DataFile_GuestAccountsUPN_Search = TestContext.Parameters["DataFile_GuestAccountsUPN_Search"]; - public static readonly string DataFile_GuestAccountsUPN_Validate = TestContext.Parameters["DataFile_GuestAccountsUPN_Validate"]; - 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 string RandomClaimType => "http://schemas.yvand.net/ws/claims/random"; + public static string RandomClaimValue => "IDoNotExist"; + public static DirectoryObjectProperty RandomObjectProperty => DirectoryObjectProperty.AccountEnabled; - public static SPTrustedLoginProvider SPTrust => SPSecurityTokenServiceManager.Local.TrustedLoginProviders.FirstOrDefault(x => String.Equals(x.ClaimProviderName, UnitTestsHelper.ClaimsProviderName, StringComparison.InvariantCultureIgnoreCase)); + public static string TrustedGroupToAdd_ClaimType => TestContext.Parameters["TrustedGroupToAdd_ClaimType"]; + public static string TrustedGroupToAdd_ClaimValue => TestContext.Parameters["TrustedGroupToAdd_ClaimValue"]; + public static SPClaim TrustedGroup => new SPClaim(TrustedGroupToAdd_ClaimType, TrustedGroupToAdd_ClaimValue, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, SPTrust.Name)); - static TextWriterTraceListener logFileListener; + public static string AzureTenantsJsonFile => TestContext.Parameters["AzureTenantsJsonFile"]; + public static string DataFile_GuestAccountsUPN_Search => TestContext.Parameters["DataFile_GuestAccountsUPN_Search"]; + public static string DataFile_GuestAccountsUPN_Validate => TestContext.Parameters["DataFile_GuestAccountsUPN_Validate"]; + 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; } - [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(azurecp.AzureCP)).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")} DataFile_GuestAccountsUPN_Search: {DataFile_GuestAccountsUPN_Search}"); - Trace.WriteLine($"{DateTime.Now.ToString("s")} DataFile_GuestAccountsUPN_Validate: {DataFile_GuestAccountsUPN_Validate}"); - Trace.WriteLine($"{DateTime.Now.ToString("s")} TestSiteCollectionName: {TestContext.Parameters["TestSiteCollectionName"]}"); + [OneTimeSetUp] + public static void InitializeSiteCollection() + { + Logger = new TextWriterTraceListener(TestContext.Parameters["TestLogFileName"]); + Trace.Listeners.Add(Logger); + Trace.AutoFlush = true; + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Start integration tests of {AzureCP.ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(AzureCP)).Location).FileVersion}."); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} DataFile_AllAccounts_Search: {DataFile_AllAccounts_Search}"); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} DataFile_AllAccounts_Validate: {DataFile_AllAccounts_Validate}"); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} DataFile_GuestAccountsUPN_Search: {DataFile_GuestAccountsUPN_Search}"); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} DataFile_GuestAccountsUPN_Validate: {DataFile_GuestAccountsUPN_Validate}"); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} TestSiteCollectionName: {TestContext.Parameters["TestSiteCollectionName"]}"); + + if (SPTrust == null) + { + Trace.TraceError($"{DateTime.Now.ToString("s")} SPTrust: is null"); + } + else + { + Trace.TraceInformation($"{DateTime.Now.ToString("s")} SPTrust: {SPTrust.Name}"); + } #if DEBUG - TestSiteCollUri = new Uri("http://spsites/sites/" + TestContext.Parameters["TestSiteCollectionName"]); - //return; // Uncommented when debugging AzureCP code from unit tests + TestSiteCollUri = new Uri($"http://spsites{TestSiteRelativePath}"); + return; // Uncommented when debugging AzureCP code 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, AzureCP.ClaimsProviderName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + return false; + }); + if (wa == null) + { + Trace.TraceError($"{DateTime.Now.ToString("s")} Web application was NOT found."); + return; + } - if (SPTrust == null) - { - Trace.TraceError($"{DateTime.Now.ToString("s")} SPTrust: is null"); - } - else - { - Trace.WriteLine($"{DateTime.Now.ToString("s")} SPTrust: {SPTrust.Name}"); - } - - AzureCPConfig config = AzureCPConfig.GetConfiguration(UnitTestsHelper.ClaimsProviderConfigName, UnitTestsHelper.SPTrust.Name); - if (config == null) - { - AzureCPConfig.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."); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Web application {wa.Name} found."); + Uri waRootAuthority = wa.AlternateUrls[0].Uri; + TestSiteCollUri = new Uri($"{waRootAuthority.GetLeftPart(UriPartial.Authority)}{TestSiteRelativePath}"); 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)) + if (!SPSite.Exists(waRootAuthority)) { - 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); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Creating root site collection {waRootAuthority.AbsoluteUri}..."); + SPSite spSite = wa.Sites.Add(waRootAuthority.AbsoluteUri, "root", "root", 1033, "STS#3", FarmAdmin, String.Empty, String.Empty); spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; @@ -109,15 +110,10 @@ public static void InitializeSiteCollection() 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); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Creating site collection {TestSiteCollUri.AbsoluteUri}..."); + SPSite spSite = wa.Sites.Add(TestSiteCollUri.AbsoluteUri, AzureCP.ClaimsProviderName, AzureCP.ClaimsProviderName, 1033, "STS#3", FarmAdmin, String.Empty, String.Empty); spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; @@ -133,250 +129,131 @@ public static void InitializeSiteCollection() } } } - 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(azurecp.AzureCP)).Location).FileVersion} finished."); - Trace.Flush(); - if (logFileListener != null) + [OneTimeTearDown] + public static void Cleanup() { - logFileListener.Dispose(); + Trace.TraceInformation($"{DateTime.Now.ToString("s")} Integration tests of {AzureCP.ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(AzureCP)).Location).FileVersion} finished."); + Trace.Flush(); + if (Logger != null) + { + Logger.Dispose(); + } } } - public static void InitializeConfiguration(AzureCPConfig config) - { - config.ResetCurrentConfiguration(); - -#if DEBUG - config.Timeout = 99999; -#endif + //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" }; + // } + //} - string json = File.ReadAllText(AzureTenantsJsonFile); - List azureTenants = JsonConvert.DeserializeObject>(json); - config.AzureTenants = azureTenants; - config.Update(); - Trace.WriteLine($"{DateTime.Now.ToString("s")} Set {config.AzureTenants.Count} Azure AD tenants to AzureCP configuration"); + public enum ResultUserType + { + None, + Mixed, + Member, + Guest, } - /// - /// 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) + public enum ResultEntityType { - try - { - Stopwatch timer = new Stopwatch(); - timer.Start(); - var entityTypes = new[] { "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}'."); - } + None, + Mixed, + User, + Group, } - public static void VerifySearchTest(List entities, string input, int expectedCount, string expectedClaimValue) + public enum EntityDataSourceType { - 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()}"); + AllAccounts, + UPNB2BGuestAccounts } - public static void TestValidationOperation(SPClaim inputClaim, bool shouldValidate, string expectedClaimValue) + public class SearchEntityData { - 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 string Input; + public int SearchResultCount; + public string SearchResultSingleEntityClaimValue; + public ResultEntityType SearchResultEntityTypes; + public ResultUserType SearchResultUserTypes; + public bool ExactMatch; } - public static void TestAugmentationOperation(string claimType, string claimValue, bool isMemberOfTrustedGroup) + public class SearchEntityDataSource { - try + public static IEnumerable GetTestData(EntityDataSourceType entityDataSourceType) { - 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)) + string csvPath = UnitTestsHelper.DataFile_AllAccounts_Search; + if (entityDataSourceType == EntityDataSourceType.UPNB2BGuestAccounts) { - groupFound = true; + csvPath = UnitTestsHelper.DataFile_GuestAccountsUPN_Search; } - if (isMemberOfTrustedGroup) + DataTable dt = DataTable.New.ReadCsv(csvPath); + foreach (Row row in dt.Rows) { - 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."); + 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.SearchResultUserTypes = (ResultUserType)Enum.Parse(typeof(ResultUserType), row["SearchResultUserTypes"]); + registrationData.ExactMatch = Convert.ToBoolean(row["ExactMatch"]); + yield return new TestCaseData(new object[] { registrationData }); } - 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[] { "AADGroup1", "1", "5b0f6c56-c87f-44c3-9354-56cba03da433" }; -// yield return new[] { "AADGroupTes", "1", "99abdc91-e6e0-475c-a0ba-5014f91de853" }; -// } -//} -public enum EntityDataSourceType -{ - AllAccounts, - UPNB2BGuestAccounts -} + //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 SearchEntityDataSource -{ - public static IEnumerable GetTestData(EntityDataSourceType entityDataSourceType) + public class ValidateEntityDataSource { - string csvPath = UnitTestsHelper.DataFile_AllAccounts_Search; - if (entityDataSourceType == EntityDataSourceType.UPNB2BGuestAccounts) + public static IEnumerable GetTestData(EntityDataSourceType entityDataSourceType) { - csvPath = UnitTestsHelper.DataFile_GuestAccountsUPN_Search; - } + string csvPath = UnitTestsHelper.DataFile_AllAccounts_Validate; + if (entityDataSourceType == EntityDataSourceType.UPNB2BGuestAccounts) + { + csvPath = UnitTestsHelper.DataFile_GuestAccountsUPN_Validate; + } - DataTable dt = DataTable.New.ReadCsv(csvPath); + DataTable dt = DataTable.New.ReadCsv(csvPath); - foreach (Row row in dt.Rows) - { - var registrationData = new SearchEntityData(); - registrationData.Input = row["Input"]; - registrationData.ExpectedResultCount = Convert.ToInt32(row["ExpectedResultCount"]); - registrationData.ExpectedEntityClaimValue = row["ExpectedEntityClaimValue"]; - registrationData.ResultType = row["ResultType"]; - registrationData.UserType = row["UserType"]; - yield return new TestCaseData(new object[] { registrationData }); + 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"]); + registrationData.UserType = (ResultUserType)Enum.Parse(typeof(ResultUserType), row["UserType"]); + 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 string ResultType; - public string UserType; -} - -public class ValidateEntityDataSource -{ - public static IEnumerable GetTestData(EntityDataSourceType entityDataSourceType) + public class ValidateEntityData { - string csvPath = UnitTestsHelper.DataFile_AllAccounts_Validate; - if (entityDataSourceType == EntityDataSourceType.UPNB2BGuestAccounts) - { - csvPath = UnitTestsHelper.DataFile_GuestAccountsUPN_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.UserType = row["UserType"]; - yield return new TestCaseData(new object[] { registrationData }); - } + public string ClaimValue; + public bool ShouldValidate; + public bool IsMemberOfTrustedGroup; + public ResultEntityType EntityType; + public ResultUserType UserType; } -} - -public class ValidateEntityData -{ - public string ClaimValue; - public bool ShouldValidate; - public bool IsMemberOfTrustedGroup; - public string UserType; -} +} \ No newline at end of file diff --git a/AzureCP.Tests/WrongConfigTests.cs b/AzureCP.Tests/WrongConfigTests.cs new file mode 100644 index 00000000..677c357e --- /dev/null +++ b/AzureCP.Tests/WrongConfigTests.cs @@ -0,0 +1,35 @@ +using Microsoft.SharePoint; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yvand.ClaimsProviders.Config; + +namespace Yvand.ClaimsProviders.Tests +{ + [TestFixture] + [Parallelizable(ParallelScope.Children)] + public class WrongConfigTests : EntityTestsBase + { + public override bool ConfigurationIsValid => false; + public override void InitializeConfiguration() + { + base.InitializeConfiguration(); + ClaimTypeConfig randomClaimTypeConfig = new ClaimTypeConfig + { + ClaimType = UnitTestsHelper.RandomClaimType, + EntityProperty = UnitTestsHelper.RandomObjectProperty, + }; + Settings.ClaimTypes = new ClaimTypeConfigCollection(UnitTestsHelper.SPTrust) { randomClaimTypeConfig }; + GlobalConfiguration.ApplySettings(Settings, true); + } + + [TestCase(@"random", 0, "")] + public override void SearchEntities(string inputValue, int expectedResultCount, string expectedEntityClaimValue) + { + base.SearchEntities(inputValue, expectedResultCount, expectedEntityClaimValue); + } + } +} diff --git a/AzureCP.Tests/local.runsettings b/AzureCP.Tests/local.runsettings index 18b5d31c..911b617c 100644 --- a/AzureCP.Tests/local.runsettings +++ b/AzureCP.Tests/local.runsettings @@ -4,16 +4,19 @@ x64 + - + - + + + diff --git a/AzureCP/AzureCP.cs b/AzureCP/AzureCP.cs deleted file mode 100644 index 01e633e1..00000000 --- a/AzureCP/AzureCP.cs +++ /dev/null @@ -1,1916 +0,0 @@ -using Microsoft.Graph; -using Microsoft.Graph.Users; -using Microsoft.Graph.Groups; -using Microsoft.Graph.Users.Item.GetMemberGroups; -using Microsoft.Graph.Users.Item.MemberOf; -using Microsoft.Graph.Models; -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.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using static azurecp.ClaimsProviderLogging; -using WIF4_5 = System.Security.Claims; -using Microsoft.Kiota.Abstractions; -using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options; -using System.Collections; -using System.Diagnostics.Metrics; -using static Microsoft.Graph.CoreConstants; - -/* - * DO NOT directly edit AzureCP class. It is designed to be inherited to customize it as desired. - * Please download "AzureCP for Developers.zip" on https://github.com/Yvand/AzureCP to find examples and guidance. - * */ - -namespace azurecp -{ - /// - /// Provides search and resolution against Azure Active Directory - /// Visit https://github.com/Yvand/AzureCP for documentation and updates. - /// Please report any bug to https://github.com/Yvand/AzureCP. - /// Author: Yvan Duhamel - /// - public class AzureCP : SPClaimProvider - { - public static string _ProviderInternalName => "AzureCP"; - public virtual string ProviderInternalName => "AzureCP"; - public virtual string PersistedObjectName => ClaimsProviderConstants.CONFIG_NAME; - - private object Lock_Init = new object(); - private ReaderWriterLockSlim Lock_Config = new ReaderWriterLockSlim(); - private long CurrentConfigurationVersion = 0; - - /// - /// Contains configuration currently used by claims provider - /// - public IAzureCPConfiguration CurrentConfiguration - { - get => _CurrentConfiguration; - set => _CurrentConfiguration = value; - } - private IAzureCPConfiguration _CurrentConfiguration; - - /// - /// SPTrust associated with the claims provider - /// - protected SPTrustedLoginProvider SPTrust; - - /// - /// ClaimTypeConfig mapped to the identity claim in the SPTrustedIdentityTokenIssuer - /// - IdentityClaimTypeConfig IdentityClaimTypeConfig; - - /// - /// Group ClaimTypeConfig used to set the claim type for other group ClaimTypeConfig that have UseMainClaimTypeOfDirectoryObject set to true - /// - ClaimTypeConfig MainGroupClaimTypeConfig; - - /// - /// Processed list to use. It is guarranted to never contain an empty ClaimType - /// - public List ProcessedClaimTypesList - { - get => _ProcessedClaimTypesList; - set => _ProcessedClaimTypesList = value; - } - private List _ProcessedClaimTypesList; - - protected IEnumerable MetadataConfig; - protected virtual string PickerEntityDisplayText => "({0}) {1}"; - protected virtual string PickerEntityOnMouseOver => "{0}={1}"; - - /// - /// Returned issuer formatted like the property SPClaim.OriginalIssuer: "TrustedProvider:TrustedProviderName" - /// - protected string IssuerName => SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, SPTrust.Name); - - public AzureCP(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 - /// - public bool Initialize(Uri context, string[] entityTypes) - { - // Ensures thread safety to initialize class variables - lock (Lock_Init) - { - // 1ST PART: GET CONFIGURATION OBJECT - IAzureCPConfiguration 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) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' was not found in configuration database, use default configuration instead. Visit AzureCP admin pages in central administration to create it.", - TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - // Return default configuration and set refreshConfig to true to give a chance to deprecated method SetCustomConfiguration() to set AzureTenants list - globalConfiguration = AzureCPConfig.ReturnDefaultConfiguration(SPTrust.Name); - refreshConfig = true; - } - else - { - ((AzureCPConfig)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 AzureCP admin pages in central administration to create it.", - TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - // Cannot continue - success = false; - } - - if (success) - { - if (this.CurrentConfigurationVersion == ((SPPersistedObject)globalConfiguration).Version) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Configuration '{PersistedObjectName}' was found, version {((SPPersistedObject)globalConfiguration).Version.ToString()}", - TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Core); - } - 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; - } - - // If config is already initialized, double check that property GraphService is not null as it is required to query AAD tenants - if (!refreshConfig) - { - foreach (var tenant in this.CurrentConfiguration.AzureTenants) - { - if (tenant.GraphService == null) - { - // Mark config to be refreshed in the write lock - refreshConfig = true; - } - } - } - } - catch (Exception ex) - { - success = false; - ClaimsProviderLogging.LogException(ProviderInternalName, "in Initialize", TraceCategory.Core, ex); - } - - 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 persisted object that will never be saved in config DB, it's just a local copy - // This copy is unique to current object instance to avoid thread safety issues - this.CurrentConfiguration = ((AzureCPConfig)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; - } - - if (this.CurrentConfiguration.AzureTenants == null || this.CurrentConfiguration.AzureTenants.Count == 0) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] There is no Azure tenant registered in the configuration '{PersistedObjectName}'. Visit AzureCP in central administration to add it, or override method GetConfiguration.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); - return false; - } - - // Set properties AuthenticationProvider and GraphService - foreach (var tenant in this.CurrentConfiguration.AzureTenants) - { - tenant.InitializeGraphForAppOnlyAuth(ProviderInternalName, this.CurrentConfiguration.Timeout); - } - success = this.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.Equals(x.ClaimType, claimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase) && - !x.UseMainClaimTypeOfDirectoryObject && - x.DirectoryObjectProperty != AzureADObjectProperty.NotSet); - - if (claimTypeConfig == null) - { - continue; - } - claimTypeConfig.ClaimTypeDisplayName = claimTypeInformation.DisplayName; - claimTypesSetInTrust.Add(claimTypeConfig); - if (String.Equals(SPTrust.IdentityClaimTypeInformation.MappedClaimType, claimTypeConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase)) - { - // Identity claim type found, set IdentityClaimTypeConfig property - identityClaimTypeFound = true; - IdentityClaimTypeConfig = IdentityClaimTypeConfig.ConvertClaimTypeConfig(claimTypeConfig); - } - else if (!groupClaimTypeFound && claimTypeConfig.EntityType == DirectoryObjectType.Group) - { - groupClaimTypeFound = true; - MainGroupClaimTypeConfig = claimTypeConfig; - } - } - - 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.DirectoryObjectPropertyToShowAsDisplayText = IdentityClaimTypeConfig.DirectoryObjectPropertyToShowAsDisplayText; - } - else - { - // If not a user, it must be a group - if (MainGroupClaimTypeConfig == null) - { - continue; - } - claimTypeConfig.ClaimType = MainGroupClaimTypeConfig.ClaimType; - claimTypeConfig.DirectoryObjectPropertyToShowAsDisplayText = MainGroupClaimTypeConfig.DirectoryObjectPropertyToShowAsDisplayText; - claimTypeConfig.ClaimTypeDisplayName = MainGroupClaimTypeConfig.ClaimTypeDisplayName; - } - additionalClaimTypeConfigList.Add(claimTypeConfig); - } - - this.ProcessedClaimTypesList = new List(claimTypesSetInTrust.Count + additionalClaimTypeConfigList.Count); - this.ProcessedClaimTypesList.AddRange(claimTypesSetInTrust); - this.ProcessedClaimTypesList.AddRange(additionalClaimTypeConfigList); - - // Get all PickerEntity metadata with a DirectoryObjectProperty set - this.MetadataConfig = nonProcessedClaimTypes.Where(x => - !String.IsNullOrEmpty(x.EntityDataKey) && - x.DirectoryObjectProperty != AzureADObjectProperty.NotSet); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in InitializeClaimTypeConfigList", TraceCategory.Core, ex); - success = false; - } - return success; - } - - /// - /// Override this method to return a custom configuration of AzureCP. - /// DO NOT Override this method if you use a custom persisted object to store configuration in config DB. - /// To use a custom persisted object, override property PersistedObjectName and set its name - /// - /// - protected virtual IAzureCPConfiguration GetConfiguration(Uri context, string[] entityTypes, string persistedObjectName, string spTrustName) - { - return AzureCPConfig.GetConfiguration(persistedObjectName, spTrustName); - } - - /// - /// [Deprecated] Override this method to customize the configuration of AzureCP. 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 AzureCP should process input (and show results) based on current URL (context) - /// - /// The context, as a URI - /// - 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 AzureCP only if current web app uses it - // It is not possible to exclude zones where AzureCP is not used because: - // Consider following scenario: default zone is WinClaims, intranet zone is Federated: - // In intranet zone, when creating permission, AzureCP will be called 2 times. The 2nd time (in FillResolve (SPClaim)), the context will always be the URL of the 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; - } - - /// - /// Uses reflection to return the value of a public property for the given object - /// - /// - /// - /// Null if property doesn't exist, String.Empty if property exists but has no value, actual value otherwise - public static string GetPropertyValue(object directoryObject, string propertyName) - { - if (directoryObject == null) - { - return null; - } - - if (propertyName.StartsWith("extensionAttribute")) - { - try - { - var returnString = string.Empty; - if (directoryObject is User) - { - var userobject = (User)directoryObject; - if (userobject.AdditionalData != null) - { - var obj = userobject.AdditionalData.FirstOrDefault(s => s.Key.EndsWith(propertyName)); - if (obj.Value != null) - { - returnString = obj.Value.ToString(); - } - else - { - return null; - } - } - } - else if (directoryObject is Group) - { - var groupobject = (Group)directoryObject; - if (groupobject.AdditionalData != null) - { - var obj = groupobject.AdditionalData.FirstOrDefault(s => s.Key.EndsWith(propertyName)); - if (obj.Value != null) - { - returnString = obj.Value.ToString(); - } - else - { - return null; - } - } - } - return returnString == null ? propertyName : returnString; - } - catch - { - return null; - } - } - - PropertyInfo pi = directoryObject.GetType().GetProperty(propertyName); - if (pi == null) - { - return null; - } // Property doesn't exist - object propertyValue = pi.GetValue(directoryObject, null); - return propertyValue == null ? String.Empty : propertyValue.ToString(); - } - - /// - /// Create a SPClaim with property OriginalIssuer correctly set - /// - /// Claim type - /// Claim value - /// Claim value type - /// SPClaim object - protected virtual new SPClaim CreateClaim(string type, string value, string valueType) - { - // SPClaimProvider.CreateClaim sets property OriginalIssuer to SPOriginalIssuerType.ClaimProvider, which is not correct - //return CreateClaim(type, value, valueType); - return new SPClaim(type, value, valueType, IssuerName); - } - - protected virtual PickerEntity CreatePickerEntityHelper(AzureCPResult result) - { - PickerEntity entity = CreatePickerEntity(); - SPClaim claim; - string permissionValue = result.PermissionValue; - string permissionClaimType = result.ClaimTypeConfig.ClaimType; - bool isMappedClaimTypeConfig = false; - - if (String.Equals(result.ClaimTypeConfig.ClaimType, IdentityClaimTypeConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase) - || result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject) - { - isMappedClaimTypeConfig = true; - } - - entity.EntityType = result.ClaimTypeConfig.SharePointEntityType; - if (result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject) - { - string claimValueType; - if (result.ClaimTypeConfig.EntityType == DirectoryObjectType.User) - { - permissionClaimType = IdentityClaimTypeConfig.ClaimType; - claimValueType = IdentityClaimTypeConfig.ClaimValueType; - if (String.IsNullOrEmpty(entity.EntityType)) - { - entity.EntityType = SPClaimEntityTypes.User; - } - } - else - { - permissionClaimType = MainGroupClaimTypeConfig.ClaimType; - claimValueType = MainGroupClaimTypeConfig.ClaimValueType; - if (String.IsNullOrEmpty(entity.EntityType)) - { - entity.EntityType = ClaimsProviderConstants.GroupClaimEntityType; - } - } - permissionValue = FormatPermissionValue(permissionClaimType, permissionValue, isMappedClaimTypeConfig, result); - claim = CreateClaim( - permissionClaimType, - permissionValue, - claimValueType); - } - else - { - permissionValue = FormatPermissionValue(permissionClaimType, permissionValue, isMappedClaimTypeConfig, result); - claim = CreateClaim( - permissionClaimType, - permissionValue, - result.ClaimTypeConfig.ClaimValueType); - if (String.IsNullOrEmpty(entity.EntityType)) - { - entity.EntityType = result.ClaimTypeConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; - } - } - - entity.Claim = claim; - entity.IsResolved = true; - //entity.EntityGroupName = ""; - entity.Description = String.Format( - PickerEntityOnMouseOver, - result.ClaimTypeConfig.DirectoryObjectProperty.ToString(), - result.QueryMatchValue); - - int nbMetadata = 0; - // If current result is a SharePoint group but was found on an AAD User object, then 1 to many User objects could match so no metadata from the current match should be set - if (!String.Equals(result.ClaimTypeConfig.SharePointEntityType, ClaimsProviderConstants.GroupClaimEntityType, StringComparison.InvariantCultureIgnoreCase) || - result.ClaimTypeConfig.EntityType != DirectoryObjectType.User) - { - // Populate metadata of new PickerEntity - foreach (ClaimTypeConfig ctConfig in MetadataConfig.Where(x => x.EntityType == result.ClaimTypeConfig.EntityType)) - { - // if there is actally a value in the GraphObject, then it can be set - string entityAttribValue = GetPropertyValue(result.UserOrGroupResult, ctConfig.DirectoryObjectProperty.ToString()); - if (!String.IsNullOrEmpty(entityAttribValue)) - { - entity.EntityData[ctConfig.EntityDataKey] = entityAttribValue; - nbMetadata++; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Set metadata '{ctConfig.EntityDataKey}' of new entity to '{entityAttribValue}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - } - } - entity.DisplayText = FormatPermissionDisplayText(entity, isMappedClaimTypeConfig, result); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created entity: display text: '{entity.DisplayText}', value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}', and filled with {nbMetadata.ToString()} metadata.", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - return entity; - } - - /// - /// Override this method to customize value of permission created - /// - /// - /// - /// - /// - /// - protected virtual string FormatPermissionValue(string claimType, string claimValue, bool isIdentityClaimType, AzureCPResult result) - { - return claimValue; - } - - /// - /// Override this method to customize display text of permission created - /// - /// - /// - /// - /// - protected virtual string FormatPermissionDisplayText(PickerEntity entity, bool isMappedClaimTypeConfig, AzureCPResult result) - { - string entityDisplayText = this.CurrentConfiguration.EntityDisplayTextPrefix; - if (result.ClaimTypeConfig.DirectoryObjectPropertyToShowAsDisplayText != AzureADObjectProperty.NotSet) - { - if (!isMappedClaimTypeConfig || result.ClaimTypeConfig.EntityType == DirectoryObjectType.Group) - { - entityDisplayText += "(" + result.ClaimTypeConfig.ClaimTypeDisplayName + ") "; - } - - string graphPropertyToDisplayValue = GetPropertyValue(result.UserOrGroupResult, result.ClaimTypeConfig.DirectoryObjectPropertyToShowAsDisplayText.ToString()); - if (!String.IsNullOrEmpty(graphPropertyToDisplayValue)) - { - entityDisplayText += graphPropertyToDisplayValue; - } - else - { - entityDisplayText += result.PermissionValue; - } - } - else - { - if (isMappedClaimTypeConfig) - { - entityDisplayText += result.QueryMatchValue; - } - else - { - entityDisplayText += String.Format( - PickerEntityDisplayText, - result.ClaimTypeConfig.ClaimTypeDisplayName, - result.PermissionValue); - } - } - return entityDisplayText; - } - - protected virtual PickerEntity CreatePickerEntityForSpecificClaimType(string input, ClaimTypeConfig ctConfig, bool inputHasKeyword) - { - List entities = CreatePickerEntityForSpecificClaimTypes( - input, - new List() - { - ctConfig, - }, - inputHasKeyword); - return entities == null ? null : entities.First(); - } - - protected virtual List CreatePickerEntityForSpecificClaimTypes(string input, List ctConfigs, bool inputHasKeyword) - { - List entities = new List(); - foreach (var ctConfig in ctConfigs) - { - SPClaim claim = CreateClaim(ctConfig.ClaimType, input, ctConfig.ClaimValueType); - PickerEntity entity = CreatePickerEntity(); - entity.Claim = claim; - entity.IsResolved = true; - entity.EntityType = ctConfig.SharePointEntityType; - if (String.IsNullOrEmpty(entity.EntityType)) - { - entity.EntityType = ctConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; - } - //entity.EntityGroupName = ""; - entity.Description = String.Format(PickerEntityOnMouseOver, ctConfig.DirectoryObjectProperty.ToString(), input); - - if (!String.IsNullOrEmpty(ctConfig.EntityDataKey)) - { - entity.EntityData[ctConfig.EntityDataKey] = entity.Claim.Value; - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Added metadata '{ctConfig.EntityDataKey}' with value '{entity.EntityData[ctConfig.EntityDataKey]}' to new entity", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - - AzureCPResult result = new AzureCPResult(null); - result.ClaimTypeConfig = ctConfig; - result.PermissionValue = input; - result.QueryMatchValue = input; - bool isIdentityClaimType = String.Equals(claim.ClaimType, IdentityClaimTypeConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase); - entity.DisplayText = FormatPermissionDisplayText(entity, isIdentityClaimType, result); - - entities.Add(entity); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created entity: display text: '{entity.DisplayText}', value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'.", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - return entities.Count > 0 ? entities : null; - } - - /// - /// Called when claims provider is added to the farm. At this point the persisted object is not created yet so we can't pass actual claim type list - /// If assemblyBinding for Newtonsoft.Json was not correctly added on the server, this method will generate an assembly load exception during feature activation - /// Also called every 1st query in people picker - /// - /// - protected override void FillClaimTypes(List claimTypes) - { - if (claimTypes == null) { return; } - try - { - this.Lock_Config.EnterReadLock(); - if (ProcessedClaimTypesList == null) { return; } - foreach (var claimTypeSettings in ProcessedClaimTypesList) - { - claimTypes.Add(claimTypeSettings.ClaimType); - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in FillClaimTypes", TraceCategory.Core, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - } - - protected override void FillClaimValueTypes(List claimValueTypes) - { - claimValueTypes.Add(WIF4_5.ClaimValueTypes.String); - } - - protected override void FillClaimsForEntity(Uri context, SPClaim entity, SPClaimProviderContext claimProviderContext, List claims) - { - AugmentEntity(context, entity, claimProviderContext, claims); - } - - protected override void FillClaimsForEntity(Uri context, SPClaim entity, List claims) - { - AugmentEntity(context, entity, null, claims); - } - - /// - /// Perform augmentation of entity supplied - /// - /// - /// entity to augment - /// Can be null - /// - protected void AugmentEntity(Uri context, SPClaim entity, SPClaimProviderContext claimProviderContext, List claims) - { - Stopwatch timer = new Stopwatch(); - timer.Start(); - 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; - } - - if (!Initialize(context, null)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - // There can be multiple TrustedProvider on the farm, but AzureCP 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; } - - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Starting augmentation for user '{decodedEntity.Value}'.", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - ClaimTypeConfig groupClaimTypeSettings = this.ProcessedClaimTypesList.FirstOrDefault(x => x.EntityType == DirectoryObjectType.Group); - if (groupClaimTypeSettings == null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] No claim type with EntityType 'Group' was found, please check claims mapping table.", - TraceSeverity.High, EventSeverity.Error, TraceCategory.Augmentation); - return; - } - - OperationContext currentContext = new OperationContext(CurrentConfiguration, OperationType.Augmentation, ProcessedClaimTypesList, null, decodedEntity, context, null, null, Int32.MaxValue); - Task> resultsTask = GetGroupMembershipAsync(currentContext, groupClaimTypeSettings); - resultsTask.Wait(); - List groups = resultsTask.Result; - timer.Stop(); - if (groups?.Count > 0) - { - foreach (SPClaim group in groups) - { - claims.Add(group); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Added group '{group.Value}' to user '{currentContext.IncomingEntity.Value}'", - TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); - } - ClaimsProviderLogging.Log($"[{ProviderInternalName}] User '{currentContext.IncomingEntity.Value}' was augmented with {groups.Count.ToString()} groups in {timer.ElapsedMilliseconds.ToString()} ms", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); - } - else - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] No group found for user '{currentContext.IncomingEntity.Value}', search took {timer.ElapsedMilliseconds.ToString()} ms", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); - } - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, "in AugmentEntity", TraceCategory.Augmentation, ex); - } - finally - { - this.Lock_Config.ExitReadLock(); - } - } - - protected async Task> GetGroupMembershipAsync(OperationContext currentContext, ClaimTypeConfig groupClaimTypeSettings) - { - List groups = new List(); - - // Create a task for each tenant to query - // Using list CurrentConfiguration.AzureTenants directly doesn't cause thread safety issues because no property from this list is written during augmentation - var tenantQueryTasks = this.CurrentConfiguration.AzureTenants.Select(async tenant => - { - return await GetGroupMembershipFromAzureADAsync(currentContext, groupClaimTypeSettings, tenant).ConfigureAwait(false); - }); - - // Wait for all tasks to complete - List[] tenantResults = await Task.WhenAll(tenantQueryTasks).ConfigureAwait(false); - - // Process result returned by each tenant - foreach (List tenantResult in tenantResults) - { - if (tenantResult?.Count > 0) - { - // The logic is that there will always be only 1 tenant returning groups, so as soon as 1 returned groups, foreach can stop - groups = tenantResult; - break; - } - } - return groups; - } - - protected async Task> GetGroupMembershipFromAzureADAsync(OperationContext currentContext, ClaimTypeConfig groupClaimTypeConfig, AzureTenant tenant) - { - List claims = new List(); - // URL encode the filter to prevent that it gets truncated like this: "UserPrincipalName eq 'guest_contoso.com" instead of "UserPrincipalName eq 'guest_contoso.com#EXT#@TENANT.onmicrosoft.com'" - string filter = HttpUtility.UrlEncode($"{currentContext.IncomingEntityClaimTypeConfig.DirectoryObjectProperty} eq '{currentContext.IncomingEntity.Value}'"); - - // Do this operation in a try/catch, so if current tenant throws an exception (e.g. secret is expired), execution can still continue for other tenants - UserCollectionResponse userCollectionResult = null; - try - { - // https://github.com/Yvand/AzureCP/issues/78 - // In this method, awaiting on the async task hangs in some scenario (reproduced only in multi-server 2019 farm in the w3wp of a site while using "check permissions" feature) - // Workaround: Instead of awaiting on the async task directly, run it in a parent task, and await on the parent task. - // userResult = await tenant.GraphService.Users.Request().Filter(filter).GetAsync().ConfigureAwait(false); - userCollectionResult = await Task.Run(() => tenant.GraphService.Users.GetAsync((config) => - { - config.QueryParameters.Filter = filter; - })).ConfigureAwait(false); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"on tenant '{tenant.Name}' while running query '{filter}'", TraceCategory.Lookup, ex); - return claims; - } - - // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and- - User user = userCollectionResult?.Value?.FirstOrDefault(); - if (user == null) - { - // If user was not found, he might be a Guest user. Query to check this: /users?$filter=userType eq 'Guest' and mail eq 'guest@live.com'&$select=userPrincipalName, Id - string guestFilter = HttpUtility.UrlEncode($"userType eq 'Guest' and {IdentityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers} eq '{currentContext.IncomingEntity.Value}'"); - //userResult = await tenant.GraphService.Users.Request().Filter(guestFilter).Select(HttpUtility.UrlEncode("userPrincipalName, Id")).GetAsync().ConfigureAwait(false); - //userResult = await Task.Run(() => tenant.GraphService.Users.Request().Filter(guestFilter).Select(HttpUtility.UrlEncode("userPrincipalName, Id")).GetAsync()).ConfigureAwait(false); - userCollectionResult = await Task.Run(() => tenant.GraphService.Users.GetAsync((config) => - { - config.QueryParameters.Filter = guestFilter; - config.QueryParameters.Select = new[] { "userPrincipalName", "Id" }; - })).ConfigureAwait(false); - user = userCollectionResult?.Value?.FirstOrDefault(); - if (user == null) { return claims; } - } - - if (groupClaimTypeConfig.DirectoryObjectProperty == AzureADObjectProperty.Id) - { - // POST to /v1.0/users/user@TENANT.onmicrosoft.com/microsoft.graph.getMemberGroups is the preferred way to return security groups as it includes nested groups - // But it returns only the group IDs so it can be used only if groupClaimTypeConfig.DirectoryObjectProperty == AzureADObjectProperty.Id - // For Guest users, it must be the id: POST to /v1.0/users/18ff6ae9-dd01-4008-a786-aabf71f1492a/microsoft.graph.getMemberGroups - //IDirectoryObjectGetMemberGroupsCollectionPage groupIDs = await tenant.GraphService.Users[user.Id].GetMemberGroups(CurrentConfiguration.FilterSecurityEnabledGroupsOnly).Request().PostAsync().ConfigureAwait(false); - //IDirectoryObjectGetMemberGroupsCollectionPage groupIDs = await Task.Run(() => tenant.GraphService.Users[user.Id].GetMemberGroups(CurrentConfiguration.FilterSecurityEnabledGroupsOnly).Request().PostAsync()).ConfigureAwait(false); - GetMemberGroupsPostRequestBody getGroupsOptions = new GetMemberGroupsPostRequestBody(); - getGroupsOptions.SecurityEnabledOnly = CurrentConfiguration.FilterSecurityEnabledGroupsOnly; - GetMemberGroupsResponse memberGroupsResponse = await Task.Run(() => tenant.GraphService.Users[user.Id].GetMemberGroups.PostAsync(getGroupsOptions)).ConfigureAwait(false); - if (memberGroupsResponse?.Value != null) - { - bool morePages; - do - { - foreach (string groupID in memberGroupsResponse.Value) - { - claims.Add(CreateClaim(groupClaimTypeConfig.ClaimType, groupID, groupClaimTypeConfig.ClaimValueType)); - } - - morePages = !string.IsNullOrWhiteSpace(memberGroupsResponse.OdataNextLink); - if (morePages) - { - var nextPageRequest = new GetMemberGroupsRequestBuilder(memberGroupsResponse.OdataNextLink, tenant.GraphService.RequestAdapter); - memberGroupsResponse = await Task.Run(() => nextPageRequest.PostAsync(getGroupsOptions)).ConfigureAwait(false); - } - } - while (morePages); - } - } - else - { - // Fallback to GET to /v1.0/users/user@TENANT.onmicrosoft.com/memberOf, which returns all group properties but does not return nested groups - //IUserMemberOfCollectionWithReferencesPage memberGroupsResponse = await tenant.GraphService.Users[user.Id].MemberOf.Request().GetAsync().ConfigureAwait(false); - DirectoryObjectCollectionResponse memberOfResponse = await Task.Run(() => tenant.GraphService.Users[user.Id].MemberOf.GetAsync()).ConfigureAwait(false); - if (memberOfResponse?.Value != null) - { - do - { - foreach (Group group in memberOfResponse.Value.OfType()) - { - string groupClaimValue = GetPropertyValue(group, groupClaimTypeConfig.DirectoryObjectProperty.ToString()); - claims.Add(CreateClaim(groupClaimTypeConfig.ClaimType, groupClaimValue, groupClaimTypeConfig.ClaimValueType)); - } - - if (!string.IsNullOrWhiteSpace(memberOfResponse.OdataNextLink)) - { - var nextPageRequest = new MemberOfRequestBuilder(memberOfResponse.OdataNextLink, tenant.GraphService.RequestAdapter); - //memberGroupsResponse = await memberGroupsResponse.NextPageRequest.GetAsync().ConfigureAwait(false); - //memberGroupsResponse = await Task.Run(() => memberGroupsResponse.NextPageRequest.GetAsync()).ConfigureAwait(false); - memberOfResponse = await Task.Run(() => nextPageRequest.GetAsync()).ConfigureAwait(false); - } - } - while (!string.IsNullOrWhiteSpace(memberOfResponse.OdataNextLink)); - } - } - return claims; - } - - protected override void FillEntityTypes(List entityTypes) - { - entityTypes.Add(SPClaimEntityTypes.User); - entityTypes.Add(ClaimsProviderConstants.GroupClaimEntityType); - } - - 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); } - - if (!Initialize(context, entityTypes)) { return; } - - this.Lock_Config.EnterReadLock(); - try - { - if (hierarchyNodeID == null) - { - // Root level - foreach (var azureObject in this.ProcessedClaimTypesList.FindAll(x => !x.UseMainClaimTypeOfDirectoryObject && aadEntityTypes.Contains(x.EntityType))) - { - hierarchy.AddChild( - new Microsoft.SharePoint.WebControls.SPProviderHierarchyNode( - _ProviderInternalName, - azureObject.ClaimTypeDisplayName, - azureObject.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 AzureCP, or add new ones - /// - /// - /// - /// - /// List of entities created by LDAPCP - protected virtual void FillEntities(OperationContext currentContext, ref List resolved) - { - } - - protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List resolved) - { - //ClaimsProviderLogging.LogDebug($"context passed to FillResolve (SPClaim): {context.ToString()}"); - if (!Initialize(context, entityTypes)) { return; } - - // Ensure incoming claim should be validated by AzureCP - // 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(); - } - } - - protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List resolved) - { - 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(currentContext, 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 FillSchema(Microsoft.SharePoint.WebControls.SPProviderSchema schema) - { - schema.AddSchemaElement(new SPSchemaElement(PeopleEditorEntityDataKeys.DisplayName, "Display Name", SPSchemaElementType.Both)); - } - - protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, Microsoft.SharePoint.WebControls.SPProviderHierarchyTree searchTree) - { - 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(currentContext, ref entities); - if (entities == null || entities.Count == 0) { return; } - SPProviderHierarchyNode matchNode = null; - foreach (PickerEntity entity in entities) - { - // Add current PickerEntity to the corresponding ClaimType 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 && - String.Equals(x.ClaimType, entity.Claim.ClaimType, StringComparison.InvariantCultureIgnoreCase)); - - 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 List SearchOrValidate(OperationContext currentContext) - { - List entities = new List(); - try - { - if (this.CurrentConfiguration.AlwaysResolveUserInput) - { - // Completely bypass query to Azure AD - entities = CreatePickerEntityForSpecificClaimTypes( - currentContext.Input, - currentContext.CurrentClaimTypeConfigList.FindAll(x => !x.UseMainClaimTypeOfDirectoryObject), - false); - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Created {entities.Count} entity(ies) without contacting Azure AD tenant(s) because AzureCP property AlwaysResolveUserInput is set to true.", - TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); - return entities; - } - - if (currentContext.OperationType == OperationType.Search) - { - entities = SearchOrValidateInAzureAD(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 without contacting Azure AD tenant(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 = SearchOrValidateInAzureAD(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 Azure AD tenant(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); - } - entities = this.ValidateEntities(currentContext, entities); - return entities; - } - - protected List SearchOrValidateInAzureAD(OperationContext currentContext) - { - string userFilter = String.Empty; - string groupFilter = String.Empty; - string userSelect = String.Empty; - string groupSelect = String.Empty; - - // 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 azureTenants = new List(this.CurrentConfiguration.AzureTenants.Count); - foreach (AzureTenant tenant in this.CurrentConfiguration.AzureTenants) - { - azureTenants.Add(tenant.CopyConfiguration()); - } - - BuildFilter(currentContext, azureTenants); - - List aadResults = null; - using (new SPMonitoredScope($"[{ProviderInternalName}] Total time spent to query Azure AD tenant(s)", 1000)) - { - // Call async method in a task to avoid error "Asynchronous operations are not allowed in this context" error when permission is validated (POST from people picker) - // More info on the error: https://stackoverflow.com/questions/672237/running-an-asynchronous-operation-triggered-by-an-asp-net-web-page-request - Task azureADQueryTask = Task.Run(async () => - { - aadResults = await QueryAzureADTenantsAsync(currentContext, azureTenants).ConfigureAwait(false); - }); - azureADQueryTask.Wait(); - } - - if (aadResults == null || aadResults.Count <= 0) { return null; } - List results = ProcessAzureADResults(currentContext, aadResults); - if (results == null || results.Count <= 0) { return null; } - List entities = new List(); - foreach (var result in results) - { - entities.Add(result.PickerEntity); - //ClaimsProviderLogging.Log($"[{ProviderInternalName}] Added entity returned by Azure AD: claim value: '{result.PickerEntity.Claim.Value}', claim type: '{result.PickerEntity.Claim.ClaimType}'", - // TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); - } - return entities; - } - - /// - /// Override this method to inspect the entities generated by AzureCP, and remove some before they are returned to SharePoint. - /// - /// Entities generated by AzureCP - /// List of entities that AzureCP will return to SharePoint - protected virtual List ValidateEntities(OperationContext currentContext, List entities) - { - return entities; - } - - /// - /// Build filter and select statements used in queries sent to Azure AD - /// $filter and $select must be URL encoded as documented in https://developer.microsoft.com/en-us/graph/docs/concepts/query_parameters#encoding-query-parameters - /// - /// - protected virtual void BuildFilter(OperationContext currentContext, List azureTenants) - { - string searchPatternEquals = "{0} eq '{1}'"; - string searchPatternStartsWith = "startswith({0}, '{1}')"; - string identityConfigSearchPatternEquals = "({0} eq '{1}' and UserType eq '{2}')"; - string identityConfigSearchPatternStartsWith = "(startswith({0}, '{1}') and UserType eq '{2}')"; - - StringBuilder userFilterBuilder = new StringBuilder(); - StringBuilder groupFilterBuilder = new StringBuilder(); - List userSelectBuilder = new List { "UserType", "Mail" }; // UserType and Mail are always needed to deal with Guest users - List groupSelectBuilder = new List { "Id", "securityEnabled" }; // Id is always required for groups - - string preferredFilterPattern; - string input = currentContext.Input; - - // https://github.com/Yvand/AzureCP/issues/88: Escape single quotes as documented in https://docs.microsoft.com/en-us/graph/query-parameters#escaping-single-quotes - input = input.Replace("'", "''"); - - if (currentContext.ExactSearch) - { - preferredFilterPattern = String.Format(searchPatternEquals, "{0}", input); - } - else - { - preferredFilterPattern = String.Format(searchPatternStartsWith, "{0}", input); - } - - bool firstUserObjectProcessed = false; - bool firstGroupObjectProcessed = false; - foreach (ClaimTypeConfig ctConfig in currentContext.CurrentClaimTypeConfigList) - { - string currentPropertyString = ctConfig.DirectoryObjectProperty.ToString(); - if (currentPropertyString.StartsWith("extensionAttribute")) - { - currentPropertyString = String.Format("{0}_{1}_{2}", "extension", "EXTENSIONATTRIBUTESAPPLICATIONID", currentPropertyString); - } - - string currentFilter; - if (!ctConfig.SupportsWildcard) - { - currentFilter = String.Format(searchPatternEquals, currentPropertyString, input); - } - else - { - // Use String.Replace instead of String.Format because String.Format trows an exception if input contains a '{' - currentFilter = preferredFilterPattern.Replace("{0}", currentPropertyString); - } - - // Id needs a specific check: input must be a valid GUID AND equals filter must be used, otherwise Azure AD will throw an error - if (ctConfig.DirectoryObjectProperty == AzureADObjectProperty.Id) - { - Guid idGuid = new Guid(); - if (!Guid.TryParse(input, out idGuid)) - { - continue; - } - else - { - currentFilter = String.Format(searchPatternEquals, currentPropertyString, idGuid.ToString()); - } - } - - if (ctConfig.EntityType == DirectoryObjectType.User) - { - if (ctConfig is IdentityClaimTypeConfig) - { - IdentityClaimTypeConfig identityClaimTypeConfig = ctConfig as IdentityClaimTypeConfig; - if (!ctConfig.SupportsWildcard) - { - currentFilter = "( " + String.Format(identityConfigSearchPatternEquals, currentPropertyString, input, ClaimsProviderConstants.MEMBER_USERTYPE) + " or " + String.Format(identityConfigSearchPatternEquals, identityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers, input, ClaimsProviderConstants.GUEST_USERTYPE) + " )"; - } - else - { - if (currentContext.ExactSearch) - { - currentFilter = "( " + String.Format(identityConfigSearchPatternEquals, currentPropertyString, input, ClaimsProviderConstants.MEMBER_USERTYPE) + " or " + String.Format(identityConfigSearchPatternEquals, identityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers, input, ClaimsProviderConstants.GUEST_USERTYPE) + " )"; - } - else - { - currentFilter = "( " + String.Format(identityConfigSearchPatternStartsWith, currentPropertyString, input, ClaimsProviderConstants.MEMBER_USERTYPE) + " or " + String.Format(identityConfigSearchPatternStartsWith, identityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers, input, ClaimsProviderConstants.GUEST_USERTYPE) + " )"; - } - } - } - - if (!firstUserObjectProcessed) - { - firstUserObjectProcessed = true; - } - else - { - currentFilter = " or " + currentFilter; - } - userFilterBuilder.Append(currentFilter); - userSelectBuilder.Add(currentPropertyString); - } - else - { - // else assume it's a Group - if (!firstGroupObjectProcessed) - { - firstGroupObjectProcessed = true; - } - else - { - currentFilter = " or " + currentFilter; - } - groupFilterBuilder.Append(currentFilter); - groupSelectBuilder.Add(currentPropertyString); - } - } - - // Also add metadata properties to $select of corresponding object type - if (firstUserObjectProcessed) - { - foreach (ClaimTypeConfig ctConfig in MetadataConfig.Where(x => x.EntityType == DirectoryObjectType.User)) - { - userSelectBuilder.Add(ctConfig.DirectoryObjectProperty.ToString()); - } - } - if (firstGroupObjectProcessed) - { - foreach (ClaimTypeConfig ctConfig in MetadataConfig.Where(x => x.EntityType == DirectoryObjectType.Group)) - { - groupSelectBuilder.Add(ctConfig.DirectoryObjectProperty.ToString()); - } - } - - foreach (AzureTenant tenant in azureTenants) - { - string userFilterForTenant = userFilterBuilder.ToString().Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N")); - List userSelectBuilderForTenant = userSelectBuilder.Select(elem => elem.Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N"))).ToList(); - string groupFilterForTenant = groupFilterBuilder.ToString().Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N")); - List groupSelectBuilderForTenant = groupSelectBuilder.Select(elem => elem.Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N"))).ToList(); - - if (firstUserObjectProcessed) - { - tenant.UserFilter = userFilterForTenant; - } - else - { - // Reset filter if no corresponding object was found in requestInfo.ClaimTypeConfigList, to detect that tenant should not be queried - tenant.UserFilter = String.Empty; - } - - if (firstGroupObjectProcessed) - { - tenant.GroupFilter = groupFilterForTenant; - } - else - { - tenant.GroupFilter = String.Empty; - } - - tenant.UserSelect = userSelectBuilderForTenant.ToArray(); - tenant.GroupSelect = groupSelectBuilderForTenant.ToArray(); - } - } - - protected async Task> QueryAzureADTenantsAsync(OperationContext currentContext, List azureTenants) - { - // Create a task for each tenant to query - var tenantQueryTasks = azureTenants.Select(async tenant => - { - Stopwatch timer = new Stopwatch(); - AzureADResult tenantResult = null; - try - { - timer.Start(); - tenantResult = await QueryAzureADTenantAsync(currentContext, tenant, true).ConfigureAwait(false); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"in QueryAzureADTenantsAsync while querying tenant '{tenant.Name}'", TraceCategory.Lookup, ex); - } - finally - { - timer.Stop(); - } - if (tenantResult != null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Got {tenantResult.UsersAndGroups.Count().ToString()} users/groups in {timer.ElapsedMilliseconds.ToString()} ms from '{tenant.Name}' with input '{currentContext.Input}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Lookup); - } - else - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Got no result from '{tenant.Name}' with input '{currentContext.Input}', search took {timer.ElapsedMilliseconds.ToString()} ms", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Lookup); - } - return tenantResult; - }); - - // Wait for all tasks to complete and return result as a List - AzureADResult[] tenantResults = await Task.WhenAll(tenantQueryTasks).ConfigureAwait(false); - return tenantResults.ToList(); - } - - protected virtual async Task QueryAzureADTenantAsync(OperationContext currentContext, AzureTenant tenant, bool firstAttempt) - { - AzureADResult tenantResults = new AzureADResult(); - if (String.IsNullOrWhiteSpace(tenant.UserFilter) && String.IsNullOrWhiteSpace(tenant.GroupFilter)) - { - return tenantResults; - } - - if (tenant.GraphService == null) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Cannot query Azure AD tenant '{tenant.Name}' because it was not initialized", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Lookup); - return tenantResults; - } - - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Querying Azure AD tenant '{tenant.Name}' for users and groups, with input '{currentContext.Input}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Lookup); - object lockAddResultToCollection = new object(); - int timeout = this.CurrentConfiguration.Timeout; - int maxRetry = currentContext.OperationType == OperationType.Validation ? 3 : 2; - - try - { - using (new SPMonitoredScope($"[{ProviderInternalName}] Querying Azure AD tenant '{tenant.Name}' for users and groups, with input '{currentContext.Input}'", 1000)) - { - RetryHandlerOption retryHandlerOption = new RetryHandlerOption() - { - Delay = 1, - RetriesTimeLimit = TimeSpan.FromMilliseconds(timeout), - MaxRetry = maxRetry, - ShouldRetry = (delay, attempt, httpResponse) => - { - // Pointless to retry if this is Unauthorized - if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - return false; - } - return true; - } - }; - - // Build the batch - BatchRequestContent batchRequestContent = new BatchRequestContent(tenant.GraphService); - - - // Allow Advanced query as documented in https://learn.microsoft.com/en-us/graph/sdks/create-requests?tabs=csharp#retrieve-a-list-of-entities - // Add ConsistencyLevel header to eventual and $count=true to fix $filter on CompanyName - https://github.com/Yvand/AzureCP/issues/166 - //// (Only work for non-batched requests) - /// - string usersRequestId = String.Empty; - if (!String.IsNullOrWhiteSpace(tenant.UserFilter)) - { - // https://stackoverflow.com/questions/56417435/when-i-set-an-object-using-an-action-the-object-assigned-is-always-null - RequestInformation userRequest = tenant.GraphService.Users.ToGetRequestInformation(conf => - { - conf.QueryParameters = new UsersRequestBuilder.UsersRequestBuilderGetQueryParameters - { - Count = true, - //Filter = tenant.UserFilter, - Select = tenant.UserSelect, - Top = 2, // YVANDEBUG - }; - conf.Headers = new RequestHeaders - { - { "ConsistencyLevel", "eventual" } - }; - conf.Options = new List - { - retryHandlerOption, - }; - }); - // Using AddBatchRequestStepAsync adds each request as a step with no specified order of execution - usersRequestId = await batchRequestContent.AddBatchRequestStepAsync(userRequest).ConfigureAwait(false); - } - - // Groups - string groupsRequestId = String.Empty; - if (!String.IsNullOrWhiteSpace(tenant.GroupFilter)) - { - GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration groupsRequestConfig = new GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration - { - QueryParameters = new GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters - { - Count = true, - Filter = tenant.GroupFilter, - Select = tenant.GroupSelect, - }, - Headers = new RequestHeaders - { - { "ConsistencyLevel", "eventual" } - }, - Options = new List - { - retryHandlerOption, - } - }; - RequestInformation groupRequest = tenant.GraphService.Groups.ToGetRequestInformation(conf => - { - conf.QueryParameters = new GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters - { - Count = true, - Filter = tenant.GroupFilter, - Select = tenant.GroupSelect, - }; - conf.Headers = new RequestHeaders - { - { "ConsistencyLevel", "eventual" } - }; - conf.Options = new List - { - retryHandlerOption, - }; - }); - // Using AddBatchRequestStepAsync adds each request as a step with no specified order of execution - groupsRequestId = await batchRequestContent.AddBatchRequestStepAsync(groupRequest).ConfigureAwait(false); - } - - BatchResponseContent returnedResponse = await tenant.GraphService.Batch.PostAsync(batchRequestContent).ConfigureAwait(false); - UserCollectionResponse userCollectionResult = await returnedResponse.GetResponseByIdAsync(usersRequestId).ConfigureAwait(false); - GroupCollectionResponse groupCollectionResult = await returnedResponse.GetResponseByIdAsync(groupsRequestId).ConfigureAwait(false); - - // Process users result - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Query to tenant '{tenant.Name}' returned {(userCollectionResult?.Value == null ? 0 : userCollectionResult.Value.Count)} user(s) with filter \"{tenant.UserFilter}\"", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Lookup); - if (userCollectionResult?.Value != null) - { - PageIterator usersPageIterator = PageIterator.CreatePageIterator( - tenant.GraphService, - userCollectionResult, - (user) => - { - lock (lockAddResultToCollection) - { - if (tenant.ExcludeMembers == true && !String.Equals(user.UserType, ClaimsProviderConstants.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) - { - tenantResults.UsersAndGroups.Add(user); - } - else if (tenant.ExcludeGuests == true && !String.Equals(user.UserType, ClaimsProviderConstants.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) - { - tenantResults.UsersAndGroups.Add(user); - } - else - { - tenantResults.UsersAndGroups.Add(user); - } - } - return true; // return true to continue the iteration - }); - await usersPageIterator.IterateAsync().ConfigureAwait(false); - } - - // Process groups result - if (groupCollectionResult?.Value != null) - { - PageIterator groupsPageIterator = PageIterator.CreatePageIterator( - tenant.GraphService, - groupCollectionResult, - (group) => - { - lock (lockAddResultToCollection) - { - tenantResults.UsersAndGroups.Add(group); - } - return true; // return true to continue the iteration - }); - await groupsPageIterator.IterateAsync().ConfigureAwait(false); - } - - //// Cannot use Task.WaitAll() because it's actually blocking the threads, preventing parallel queries on others AAD tenants. - //// Use await Task.WhenAll() as it does not block other threads, so all AAD tenants are actually queried in parallel. - //// More info: https://stackoverflow.com/questions/12337671/using-async-await-for-multiple-tasks - //await Task.WhenAll(new Task[1] { batchQueryTask }).ConfigureAwait(false); - //ClaimsProviderLogging.LogDebug($"Waiting on Task.WaitAll for {tenant.Name} finished"); - } - } - catch (OperationCanceledException) - { - ClaimsProviderLogging.Log($"[{ProviderInternalName}] Queries on Azure AD tenant '{tenant.Name}' exceeded timeout of {timeout} ms and were cancelled.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Lookup); - } - catch (ServiceException ex) - { - ClaimsProviderLogging.LogException(ProviderInternalName, $"Microsoft.Graph could not query tenant '{tenant.Name}'", TraceCategory.Lookup, ex); - } - catch (AggregateException ex) - { - // Task.WaitAll throws an AggregateException, which contains all exceptions thrown by tasks it waited on - ClaimsProviderLogging.LogException(ProviderInternalName, $"while querying Azure AD tenant '{tenant.Name}'", TraceCategory.Lookup, ex); - } - finally - { - } - return tenantResults; - } - - protected virtual List ProcessAzureADResults(OperationContext currentContext, List azureADResults) - { - // Split results between users/groups and list of registered domains in the tenant - List usersAndGroups = new List(); - // For each Azure AD tenant where list of result (UsersAndGroups) is not null - // singleTenantResults in azureADResults can be null if AzureCP failed to get a valid access token for it - foreach (AzureADResult singleTenantResults in azureADResults.Where(singleTenantResults => singleTenantResults != null && singleTenantResults.UsersAndGroups != null)) - { - usersAndGroups.AddRange(singleTenantResults.UsersAndGroups); - //domains.AddRange(tenantResults.DomainsRegisteredInAzureADTenant); - } - - // Return if no user / groups is found, or if no registered domain is found - if (usersAndGroups == null || !usersAndGroups.Any() /*|| domains == null || !domains.Any()*/) - { - return null; - }; - - List ctConfigs = currentContext.CurrentClaimTypeConfigList; - if (currentContext.ExactSearch) - { - ctConfigs = currentContext.CurrentClaimTypeConfigList.FindAll(x => !x.UseMainClaimTypeOfDirectoryObject); - } - - List processedResults = new List(); - foreach (DirectoryObject userOrGroup in usersAndGroups) - { - DirectoryObject currentObject = null; - DirectoryObjectType objectType; - if (userOrGroup is User) - { - // This section has become irrelevant since the specific handling of guest users is done lower in the filtering, introduced in v13 - //// Always exclude shadow users: UserType is Guest and his mail matches a verified domain in any Azure AD tenant - //string userType = ((User)userOrGroup).UserType; - //if (String.Equals(userType, ClaimsProviderConstants.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) - //{ - // string userMail = ((User)userOrGroup).Mail; - // if (String.IsNullOrEmpty(userMail)) - // { - // ClaimsProviderLogging.Log($"[{ProviderInternalName}] Guest user '{((User)userOrGroup).UserPrincipalName}' filtered out because his mail is empty.", - // TraceSeverity.Unexpected, EventSeverity.Warning, TraceCategory.Lookup); - // continue; - // } - // if (!userMail.Contains('@')) continue; - // string maildomain = userMail.Split('@')[1]; - // if (domains.Any(x => String.Equals(x, maildomain, StringComparison.InvariantCultureIgnoreCase))) - // { - // ClaimsProviderLogging.Log($"[{ProviderInternalName}] Guest user '{((User)userOrGroup).UserPrincipalName}' filtered out because his email '{userMail}' matches a domain registered in a Azure AD tenant.", - // TraceSeverity.Verbose, EventSeverity.Verbose, TraceCategory.Lookup); - // continue; - // } - //} - currentObject = userOrGroup; - objectType = DirectoryObjectType.User; - } - else - { - currentObject = userOrGroup; - objectType = DirectoryObjectType.Group; - - if (CurrentConfiguration.FilterSecurityEnabledGroupsOnly) - { - Group group = (Group)userOrGroup; - // If Group.SecurityEnabled is not set, assume the group is not SecurityEnabled - verified per tests, it is not documentated in https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0 - bool isSecurityEnabled = group.SecurityEnabled ?? false; - if (!isSecurityEnabled) - { - continue; - } - } - } - - foreach (ClaimTypeConfig ctConfig in ctConfigs.Where(x => x.EntityType == objectType)) - { - // Get value with of current GraphProperty - string directoryObjectPropertyValue = GetPropertyValue(currentObject, ctConfig.DirectoryObjectProperty.ToString()); - - if (ctConfig is IdentityClaimTypeConfig) - { - if (String.Equals(((User)currentObject).UserType, ClaimsProviderConstants.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) - { - // For Guest users, use the value set in property DirectoryObjectPropertyForGuestUsers - directoryObjectPropertyValue = GetPropertyValue(currentObject, ((IdentityClaimTypeConfig)ctConfig).DirectoryObjectPropertyForGuestUsers.ToString()); - } - } - - // Check if property exists (not null) and has a value (not String.Empty) - if (String.IsNullOrEmpty(directoryObjectPropertyValue)) { continue; } - - // Check if current value mathes input, otherwise go to next GraphProperty to check - if (currentContext.ExactSearch) - { - if (!String.Equals(directoryObjectPropertyValue, currentContext.Input, StringComparison.InvariantCultureIgnoreCase)) { continue; } - } - else - { - if (!directoryObjectPropertyValue.StartsWith(currentContext.Input, StringComparison.InvariantCultureIgnoreCase)) { continue; } - } - - // Current DirectoryObjectProperty value matches user input. Add current result to search results if it is not already present - string entityClaimValue = directoryObjectPropertyValue; - ClaimTypeConfig claimTypeConfigToCompare; - if (ctConfig.UseMainClaimTypeOfDirectoryObject) - { - if (objectType == DirectoryObjectType.User) - { - claimTypeConfigToCompare = IdentityClaimTypeConfig; - if (String.Equals(((User)currentObject).UserType, ClaimsProviderConstants.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) - { - // For Guest users, use the value set in property DirectoryObjectPropertyForGuestUsers - entityClaimValue = GetPropertyValue(currentObject, IdentityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers.ToString()); - } - else - { - // Get the value of the DirectoryObjectProperty linked to current directory object - entityClaimValue = GetPropertyValue(currentObject, IdentityClaimTypeConfig.DirectoryObjectProperty.ToString()); - } - } - else - { - claimTypeConfigToCompare = MainGroupClaimTypeConfig; - // Get the value of the DirectoryObjectProperty linked to current directory object - entityClaimValue = GetPropertyValue(currentObject, claimTypeConfigToCompare.DirectoryObjectProperty.ToString()); - } - - if (String.IsNullOrEmpty(entityClaimValue)) { continue; } - } - else - { - claimTypeConfigToCompare = ctConfig; - } - - // if claim type and claim value already exists, skip - bool resultAlreadyExists = processedResults.Exists(x => - String.Equals(x.ClaimTypeConfig.ClaimType, claimTypeConfigToCompare.ClaimType, StringComparison.InvariantCultureIgnoreCase) && - String.Equals(x.PermissionValue, entityClaimValue, StringComparison.InvariantCultureIgnoreCase)); - if (resultAlreadyExists) { continue; } - - // Passed the checks, add it to the processedResults list - processedResults.Add( - new AzureCPResult(currentObject) - { - ClaimTypeConfig = ctConfig, - PermissionValue = entityClaimValue, - QueryMatchValue = directoryObjectPropertyValue, - }); - } - } - - ClaimsProviderLogging.Log($"[{ProviderInternalName}] {processedResults.Count} entity(ies) to create after filtering", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Lookup); - foreach (AzureCPResult result in processedResults) - { - PickerEntity pe = CreatePickerEntityHelper(result); - result.PickerEntity = pe; - } - return processedResults; - } - - 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 - /// - /// - public override string GetClaimTypeForUserKey() - { - // Initialization may fail because there is no yet configuration (fresh install) - // In this case, AzureCP should not return null because it causes null exceptions in SharePoint when users sign-in - 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 - /// - /// - /// - protected override SPClaim GetUserKeyForEntity(SPClaim entity) - { - // Initialization may fail because there is no yet configuration (fresh install) - // In this case, AzureCP should not return null because it causes null exceptions in SharePoint when users sign-in - bool 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 AzureCP: 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($"[{ProviderInternalName}] Returning user key for '{entity.Value}'", - TraceSeverity.VerboseEx, 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; - } - } - - public class AzureADResult - { - public List UsersAndGroups - { - get => _UsersAndGroups; - set => _UsersAndGroups = value; - } - private List _UsersAndGroups; - - public List DomainsRegisteredInAzureADTenant - { - get => _DomainsRegisteredInAzureADTenant; - set => _DomainsRegisteredInAzureADTenant = value; - } - private List _DomainsRegisteredInAzureADTenant; - //public string TenantName; - - public AzureADResult() - { - UsersAndGroups = new List(); - DomainsRegisteredInAzureADTenant = new List(); - //this.TenantName = tenantName; - } - } - - /// - /// User / group found in Azure AD, with additional information - /// - public class AzureCPResult - { - public readonly DirectoryObject UserOrGroupResult; - public ClaimTypeConfig ClaimTypeConfig; - public PickerEntity PickerEntity; - public string PermissionValue; - public string QueryMatchValue; - //public string TenantName; - - public AzureCPResult(DirectoryObject directoryObject) - { - UserOrGroupResult = directoryObject; - //TenantName = tenantName; - } - } -} diff --git a/AzureCP/AzureCP.csproj b/AzureCP/AzureCP.csproj index 1fb233cd..b7ea420c 100644 --- a/AzureCP/AzureCP.csproj +++ b/AzureCP/AzureCP.csproj @@ -6,9 +6,10 @@ {EEC47949-34B5-4805-A04D-A372BE75D3CB} Library Properties - azurecp - AzureCP + Yvand.ClaimsProviders + AzureCPSE v4.8 + 8.0 19.0 512 {C1CDDADD-2546-481F-9697-4EA41081F2FC};{14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} @@ -74,41 +75,47 @@ - - - - + + + ASPXCodeBehind - - + + + + + + + + ClaimTypesConfig.ascx ASPXCodeBehind - + ClaimTypesConfig.ascx.cs - - AzureCPGlobalSettings.ascx + + GlobalSettings.ascx ASPXCodeBehind - - AzureCPGlobalSettings.ascx.cs + + GlobalSettings.ascx.cs - - AzureCP.feature + + AzureCPSE.feature + - + {c15a0bf3-126a-42e6-af6b-5880f2e9af42} - + {70b104e2-19df-4cb1-9802-c98eaf14d84e} - + {b82964c9-f57c-4826-b1dd-f03f63c7f197} @@ -127,7 +134,7 @@ 1.9.0 - 5.19.0 + 5.22.0 5.1.2 @@ -137,19 +144,19 @@ - - - - - AzureCP.feature + + + + + AzureCPSE.feature - - AzureCP_Administration.feature + + AzureCPSE.Administration.feature - - - - + + + + @@ -178,6 +185,7 @@ copy /Y "$(TargetDir)Microsoft.Kiota.Authentication.Azure.dll" $(ProjectDir)\bin copy /Y "$(TargetDir)Microsoft.Kiota.Http.HttpClientLibrary.dll" $(ProjectDir)\bin copy /Y "$(TargetDir)Microsoft.Kiota.Serialization.Form.dll" $(ProjectDir)\bin copy /Y "$(TargetDir)Microsoft.Kiota.Serialization.Json.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Kiota.Serialization.Multipart.dll" $(ProjectDir)\bin copy /Y "$(TargetDir)Microsoft.Kiota.Serialization.Text.dll" $(ProjectDir)\bin copy /Y "$(TargetDir)Nito.AsyncEx.Context.dll" $(ProjectDir)\bin copy /Y "$(TargetDir)Nito.AsyncEx.Coordination.dll" $(ProjectDir)\bin diff --git a/AzureCP/AzureCPConfig.cs b/AzureCP/AzureCPConfig.cs deleted file mode 100644 index 9fada7aa..00000000 --- a/AzureCP/AzureCPConfig.cs +++ /dev/null @@ -1,1284 +0,0 @@ -using Azure.Core; -using Azure.Identity; -using Microsoft.Graph; -using Microsoft.Graph.Authentication; -using Microsoft.Identity.Client; -using Microsoft.Kiota.Authentication.Azure; -using Microsoft.Kiota.Http.HttpClientLibrary.Middleware; -using Microsoft.SharePoint; -using Microsoft.SharePoint.Administration; -using Microsoft.SharePoint.Administration.Claims; -using Microsoft.SharePoint.WebControls; -using Microsoft.Web.Hosting.Administration; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Runtime; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Web; -using static azurecp.ClaimsProviderLogging; -using WIF4_5 = System.Security.Claims; - -namespace azurecp -{ - public interface IAzureCPConfiguration - { - List AzureTenants { get; set; } - ClaimTypeConfigCollection ClaimTypes { get; set; } - bool AlwaysResolveUserInput { get; set; } - bool FilterExactMatchOnly { get; set; } - bool EnableAugmentation { get; set; } - string EntityDisplayTextPrefix { get; set; } - //bool EnableRetry { get; set; } - int Timeout { get; set; } - string CustomData { get; set; } - int MaxSearchResultsCount { get; set; } - bool FilterSecurityEnabledGroupsOnly { get; set; } - } - - public static class ClaimsProviderConstants - { - public static string CONFIG_ID => "0E9F8FB6-B314-4CCC-866D-DEC0BE76C237"; - public static string CONFIG_NAME => "AzureCPConfig"; - public static string GraphServiceEndpointVersion => "v1.0"; - //public static string DefaultGraphServiceEndpoint => "https://graph.microsoft.com/"; - //public static string DefaultLoginServiceEndpoint => "https://login.microsoftonline.com/"; - /// - /// List of Microsoft Graph service root endpoints based on National Cloud as described on https://docs.microsoft.com/en-us/graph/deployments - /// - public static List> AzureCloudEndpoints = new List>() - { - new KeyValuePair(AzureCloudInstance.AzurePublic, AzureAuthorityHosts.AzurePublicCloud), - new KeyValuePair(AzureCloudInstance.AzureChina, AzureAuthorityHosts.AzureChina), - new KeyValuePair(AzureCloudInstance.AzureGermany, AzureAuthorityHosts.AzureGermany), - new KeyValuePair(AzureCloudInstance.AzureUsGovernment, AzureAuthorityHosts.AzureGovernment), - new KeyValuePair(AzureCloudInstance.None, AzureAuthorityHosts.AzurePublicCloud), - }; - public static string GroupClaimEntityType { get; set; } = SPClaimEntityTypes.FormsRole; - public static bool EnforceOnly1ClaimTypeForGroup => true; // In AzureCP, only 1 claim type can be used to create group permissions - public static string DefaultMainGroupClaimType => WIF4_5.ClaimTypes.Role; - public static string PUBLICSITEURL => "https://azurecp.yvand.net/"; - public static string GUEST_USERTYPE => "Guest"; - public static string MEMBER_USERTYPE => "Member"; - private static object Sync_SetClaimsProviderVersion = new object(); - private static string _ClaimsProviderVersion; - public static readonly string ClientCertificatePrivateKeyPassword = "YVANDwRrEHVHQ57ge?uda"; - 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(AzureCP)).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; - } - } - } - -#if DEBUG - public static int DEFAULT_TIMEOUT => 10000; -#else - public static int DEFAULT_TIMEOUT => 4000; // 4 secs -#endif - } - - public class AzureCPConfig : SPPersistedObject, IAzureCPConfiguration - { - public List AzureTenants - { - get => AzureTenantsPersisted; - set => AzureTenantsPersisted = value; - } - [Persisted] - private List AzureTenantsPersisted; - - /// - /// Configuration of claim types and their mapping with LDAP attribute/class - /// - public ClaimTypeConfigCollection ClaimTypes - { - get - { - if (_ClaimTypes == null) - { - _ClaimTypes = new ClaimTypeConfigCollection(ref this._ClaimTypesCollection); - } - return _ClaimTypes; - } - set - { - _ClaimTypes = value; - _ClaimTypesCollection = value == null ? null : value.innerCol; - } - } - [Persisted] - private Collection _ClaimTypesCollection; - - private ClaimTypeConfigCollection _ClaimTypes; - - public bool AlwaysResolveUserInput - { - get => AlwaysResolveUserInputPersisted; - set => AlwaysResolveUserInputPersisted = value; - } - [Persisted] - private bool AlwaysResolveUserInputPersisted; - - public bool FilterExactMatchOnly - { - get => FilterExactMatchOnlyPersisted; - set => FilterExactMatchOnlyPersisted = value; - } - [Persisted] - private bool FilterExactMatchOnlyPersisted; - - public bool EnableAugmentation - { - get => AugmentAADRolesPersisted; - set => AugmentAADRolesPersisted = value; - } - [Persisted] - private bool AugmentAADRolesPersisted = true; - - public string EntityDisplayTextPrefix - { - get => _EntityDisplayTextPrefix; - set => _EntityDisplayTextPrefix = value; - } - [Persisted] - private string _EntityDisplayTextPrefix; - - //public bool EnableRetry - //{ - // get => _EnableRetry; - // set => _EnableRetry = value; - //} - //[Persisted] - //private bool _EnableRetry = false; - - public int Timeout - { - get { -#if DEBUG - return _Timeout * 100; -#endif - return _Timeout; - } - set => _Timeout = value; - } - [Persisted] - private int _Timeout = ClaimsProviderConstants.DEFAULT_TIMEOUT; - - /// - /// Name of the SPTrustedLoginProvider where AzureCP 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 - - /// - /// Set if only AAD groups with securityEnabled = true should be returned - https://docs.microsoft.com/en-us/graph/api/resources/groups-overview?view=graph-rest-1.0 - /// - public bool FilterSecurityEnabledGroupsOnly - { - get => _FilterSecurityEnabledGroupsOnly; - set => _FilterSecurityEnabledGroupsOnly = value; - } - [Persisted] - private bool _FilterSecurityEnabledGroupsOnly = false; - - public AzureCPConfig(string persistedObjectName, SPPersistedObject parent, string spTrustName) : base(persistedObjectName, parent) - { - this.SPTrustName = spTrustName; - } - - public AzureCPConfig() { } - - /// - /// 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 AzureCP - /// - /// - public static AzureCPConfig GetConfiguration() - { - return GetConfiguration(ClaimsProviderConstants.CONFIG_NAME, String.Empty); - } - - /// - /// Returns the configuration of AzureCP - /// - /// - /// - public static AzureCPConfig GetConfiguration(string persistedObjectName) - { - return GetConfiguration(persistedObjectName, String.Empty); - } - - /// - /// Returns the configuration of AzureCP - /// - /// Name of the configuration - /// Name of the SPTrustedLoginProvider using the claims provider - /// - public static AzureCPConfig GetConfiguration(string persistedObjectName, string spTrustName) - { - SPPersistedObject parent = SPFarm.Local; - try - { - AzureCPConfig 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 AzureCPConfig ResetConfiguration(string persistedObjectName) - { - AzureCPConfig previousConfig = GetConfiguration(persistedObjectName, String.Empty); - if (previousConfig == null) { return null; } - Guid configId = previousConfig.Id; - string spTrustName = previousConfig.SPTrustName; - DeleteConfiguration(persistedObjectName); - AzureCPConfig 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() - { - AzureCPConfig defaultConfig = ReturnDefaultConfiguration(this.SPTrustName) as AzureCPConfig; - ApplyConfiguration(defaultConfig); - CheckAndCleanConfiguration(String.Empty); - } - - /// - /// Apply configuration in parameter to current object. It does not copy SharePoint base class members - /// - /// - public void ApplyConfiguration(AzureCPConfig 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 AzureCPConfig CopyConfiguration() - { - // Cannot use reflection here to copy object because of the calls to methods CopyConfiguration() on some properties - AzureCPConfig copy = new AzureCPConfig(); - copy.SPTrustName = this.SPTrustName; - copy.AzureTenants = new List(this.AzureTenants); - copy.ClaimTypes = new ClaimTypeConfigCollection(); - copy.ClaimTypes.SPTrust = this.ClaimTypes.SPTrust; - foreach (ClaimTypeConfig currentObject in this.ClaimTypes) - { - copy.ClaimTypes.Add(currentObject.CopyConfiguration(), false); - } - copy.AlwaysResolveUserInput = this.AlwaysResolveUserInput; - copy.FilterExactMatchOnly = this.FilterExactMatchOnly; - copy.EnableAugmentation = this.EnableAugmentation; - copy.EntityDisplayTextPrefix = this.EntityDisplayTextPrefix; - //copy.EnableRetry = this.EnableRetry; - copy.Timeout = this.Timeout; - copy.CustomData = this.CustomData; - copy.MaxSearchResultsCount = this.MaxSearchResultsCount; - copy.FilterSecurityEnabledGroupsOnly = this.FilterSecurityEnabledGroupsOnly; - return copy; - } - - public void ResetClaimTypesList() - { - ClaimTypes.Clear(); - ClaimTypes = ReturnDefaultClaimTypesConfig(this.SPTrustName); - ClaimsProviderLogging.Log($"Claim types list of configuration '{Name}' was successfully reset to default configuration", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - } - - /// - /// If AzureCP 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 AzureCPConfig CreateDefaultConfiguration() - { - SPTrustedLoginProvider spTrust = AzureCP.GetSPTrustAssociatedWithCP(AzureCP._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 AzureCP. - /// - /// 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 AzureCPConfig 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 - AzureCPConfig existingConfig = AzureCPConfig.GetConfiguration(persistedObjectName, String.Empty); - if (existingConfig != null) - { - DeleteConfiguration(persistedObjectName); - } - - ClaimsProviderLogging.Log($"Creating configuration '{persistedObjectName}' with Id {persistedObjectID}...", TraceSeverity.VerboseEx, EventSeverity.Error, TraceCategory.Core); - AzureCPConfig PersistedObject = new AzureCPConfig(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; - } - - /// - /// Generate and return default configuration - /// - /// - public static IAzureCPConfiguration ReturnDefaultConfiguration(string spTrustName) - { - AzureCPConfig defaultConfig = new AzureCPConfig(); - defaultConfig.SPTrustName = spTrustName; - defaultConfig.AzureTenants = new List(); - defaultConfig.ClaimTypes = ReturnDefaultClaimTypesConfig(spTrustName); - 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() - { - // Identity claim type. "Name" (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name) is a reserved claim type in SharePoint that cannot be used in the SPTrust. - //new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.UserPrincipalName, ClaimType = WIF4_5.ClaimTypes.Upn}, - new IdentityClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.UserPrincipalName, ClaimType = spTrust.IdentityClaimTypeInformation.MappedClaimType}, - - // Additional properties to find user and create entity with the identity claim type (UseMainClaimTypeOfDirectoryObject=true) - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.DisplayName, UseMainClaimTypeOfDirectoryObject = true, EntityDataKey = PeopleEditorEntityDataKeys.DisplayName}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.GivenName, UseMainClaimTypeOfDirectoryObject = true}, //Yvan - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.Surname, UseMainClaimTypeOfDirectoryObject = true}, //Duhamel - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.Mail, EntityDataKey = PeopleEditorEntityDataKeys.Email, UseMainClaimTypeOfDirectoryObject = true}, - - // 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, DirectoryObjectProperty = AzureADObjectProperty.MobilePhone, EntityDataKey = PeopleEditorEntityDataKeys.MobilePhone}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.JobTitle, EntityDataKey = PeopleEditorEntityDataKeys.JobTitle}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.Department, EntityDataKey = PeopleEditorEntityDataKeys.Department}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.OfficeLocation, EntityDataKey = PeopleEditorEntityDataKeys.Location}, - - // Group - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, DirectoryObjectProperty = AzureADObjectProperty.Id, ClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType, DirectoryObjectPropertyToShowAsDisplayText = AzureADObjectProperty.DisplayName}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, DirectoryObjectProperty = AzureADObjectProperty.DisplayName, UseMainClaimTypeOfDirectoryObject = true, EntityDataKey = PeopleEditorEntityDataKeys.DisplayName}, - new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, DirectoryObjectProperty = AzureADObjectProperty.Mail, EntityDataKey = PeopleEditorEntityDataKeys.Email}, - }; - newCTConfigCollection.SPTrust = spTrust; - return newCTConfigCollection; - } - - /// - /// Delete persisted object from configuration database - /// - /// Name of persisted object to delete - public static void DeleteConfiguration(string persistedObjectName) - { - AzureCPConfig config = AzureCPConfig.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 AzureCP was updated from a version < v12, this.ClaimTypes.Count will throw a NullReferenceException - int testClaimTypeCollection = this.ClaimTypes.Count; - } - catch (NullReferenceException) - { - this.ClaimTypes = ReturnDefaultClaimTypesConfig(this.SPTrustName); - configUpdated = true; - } - - if (!String.IsNullOrEmpty(this.SPTrustName)) - { - // ClaimTypeConfigCollection.SPTrust is not persisted so it should always be set explicitely - this.ClaimTypes.SPTrust = this.SPTrust; - } - - - // Starting with v13, identity claim type is automatically detected and added when list is reset to default (so it should always be present) - // And it has its own class IdentityClaimTypeConfig that must be present as is to work correctly with Guest accounts - // Since this is fixed by resetting claim type config list, this also addresses the duplicate DirectoryObjectProperty per EntityType constraint - if (this.SPTrust != null) - { - ClaimTypeConfig identityCTConfig = this.ClaimTypes.FirstOrDefault(x => String.Equals(x.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)); - if (identityCTConfig == null || !(identityCTConfig is IdentityClaimTypeConfig)) - { - this.ClaimTypes = ReturnDefaultClaimTypesConfig(this.SPTrustName); - ClaimsProviderLogging.Log($"Claim types configuration list was reset because the identity claim type was either not found or not configured correctly", - TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); - configUpdated = true; - } - } - - // Starting with v13, adding 2 times a ClaimTypeConfig with the same EntityType and same DirectoryObjectProperty 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 => x.DirectoryObjectProperty) // Group by DirectoryObjectProperty - .Select(x => new - { - DirectoryObjectProperty = x.Key, - ObjectCount = x.Count() // For each DirectoryObjectProperty, how many items found - }) - .Where(x => x.ObjectCount > 1); // Keep only DirectoryObjectProperty 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.DirectoryObjectProperty == duplicatedProperty.DirectoryObjectProperty && x.EntityType == entityType && !String.Equals(x.ClaimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)); - } - else - { - ctConfigToDelete = this.ClaimTypes.FirstOrDefault(x => x.DirectoryObjectProperty == duplicatedProperty.DirectoryObjectProperty && x.EntityType == entityType); - } - - this.ClaimTypes.Remove(ctConfigToDelete); - configUpdated = true; - ClaimsProviderLogging.Log($"Removed claim type '{ctConfigToDelete.ClaimType}' from claim types configuration list because it duplicates property {ctConfigToDelete.DirectoryObjectProperty}", - 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; - } - - /// - /// Return the Azure AD tenant in the current configuration based on its name. - /// - /// Name of the tenant, for example TENANTNAME.onMicrosoft.com. - /// AzureTenant found in the current configuration. - public AzureTenant GetAzureTenantByName(string azureTenantName) - { - AzureTenant match = null; - foreach (AzureTenant tenant in this.AzureTenants) - { - if (String.Equals(tenant.Name, azureTenantName, StringComparison.InvariantCultureIgnoreCase)) - { - match = tenant; - break; - } - } - return match; - } - } - - public class AzureTenant : SPAutoSerializingObject - { - public Guid Identifier - { - get => Id; - set => Id = value; - } - [Persisted] - private Guid Id = Guid.NewGuid(); - - /// - /// Name of the tenant, e.g. TENANTNAME.onMicrosoft.com - /// - public string Name - { - get => TenantName; - set => TenantName = value; - } - [Persisted] - private string TenantName; - - /// - /// Application ID of the application created in Azure AD tenant to authorize AzureCP - /// - public string ApplicationId - { - get => ClientId; - set => ClientId = value; - } - [Persisted] - private string ClientId; - - /// - /// Password of the application - /// - public string ApplicationSecret - { - get => ClientSecret; - set => ClientSecret = value; - } - [Persisted] - private string ClientSecret; - - /// - /// Set to true to return only Member users from this tenant - /// - public bool ExcludeMembers - { - get => ExcludeMemberUsers; - set => ExcludeMemberUsers = value; - } - [Persisted] - private bool ExcludeMemberUsers = false; - - /// - /// Set to true to return only Guest users from this tenant - /// - public bool ExcludeGuests - { - get => ExcludeGuestUsers; - set => ExcludeGuestUsers = value; - } - [Persisted] - private bool ExcludeGuestUsers = false; - - /// - /// Client ID of AD Connect used in extension attribues - /// - [Persisted] - private Guid ExtensionAttributesApplicationIdPersisted; - - public Guid ExtensionAttributesApplicationId - { - get => ExtensionAttributesApplicationIdPersisted; - set => ExtensionAttributesApplicationIdPersisted = value; - } - - public X509Certificate2 ClientCertificatePrivateKey - { - get - { - return m_ClientCertificatePrivateKey; - } - set - { - if (value == null) { return; } - m_ClientCertificatePrivateKey = value; - try - { - // https://stackoverflow.com/questions/32354790/how-to-check-is-x509certificate2-exportable-or-not - m_ClientCertificatePrivateKeyRawData = value.Export(X509ContentType.Pfx, ClaimsProviderConstants.ClientCertificatePrivateKeyPassword); - } - catch (CryptographicException ex) - { - // X509Certificate2.Export() is expected to fail if the private key is not exportable, which depends on the X509KeyStorageFlags used when creating the X509Certificate2 object - //ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"while setting the certificate for tenant '{this.Name}'. Is the private key of the certificate exportable?", TraceCategory.Core, ex); - //throw; // The caller should be informed that the certificate could not be set - } - } - } - private X509Certificate2 m_ClientCertificatePrivateKey; - [Persisted] - private byte[] m_ClientCertificatePrivateKeyRawData; - - public string AuthenticationMode - { - get - { - return String.IsNullOrWhiteSpace(this.ClientSecret) ? "ClientCertificate" : "ClientSecret"; - } - } - - public AzureCloudInstance CloudInstance - { - get => (AzureCloudInstance)Enum.Parse(typeof(AzureCloudInstance), m_CloudInstance); - set => m_CloudInstance = value.ToString(); - } - [Persisted] - private string m_CloudInstance = AzureAuthorityHosts.AzurePublicCloud.ToString(); - - /// - /// Instance of the IAuthenticationProvider class for this specific Azure AD tenant - /// - //private AADAppOnlyAuthenticationProvider AuthenticationProvider { get; set; } - - public GraphServiceClient GraphService { get; set; } - - public string UserFilter { get; set; } - public string GroupFilter { get; set; } - public string[] UserSelect { get; set; } - public string[] GroupSelect { get; set; } - - public AzureTenant() - { - } - - protected override void OnDeserialization() - { - if (m_ClientCertificatePrivateKeyRawData != null) - { - try - { - // EphemeralKeySet: Keep the private key in-memory, it won't be written to disk - https://www.pkisolutions.com/handling-x509keystorageflags-in-applications/ - m_ClientCertificatePrivateKey = ImportPfxCertificateBlob(m_ClientCertificatePrivateKeyRawData, ClaimsProviderConstants.ClientCertificatePrivateKeyPassword, X509KeyStorageFlags.EphemeralKeySet); - } - catch (CryptographicException ex) - { - ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"while deserializating the certificate for tenant '{this.Name}'.", TraceCategory.Core, ex); - } - } - } - - /// - /// Set properties AuthenticationProvider and GraphService - /// - public void InitializeGraphForAppOnlyAuth(string claimsProviderName, int timeout) - { - try - { - var handlers = GraphClientFactory.CreateDefaultHandlers(); - handlers.Add(new ChaosHandler()); // DEBUG - // PROXY ISSUE: https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/1456 - //HttpClient httpClient = GraphClientFactory.Create(handlers: handlers, proxy: new WebProxy(new Uri("http://localhost:8888"), false)); - HttpClient httpClient = GraphClientFactory.Create(proxy: new WebProxy(new Uri("http://localhost:8888"), false)); - //httpClient.Timeout = TimeSpan.FromMilliseconds(1000 * 10); - httpClient.Timeout = TimeSpan.FromMilliseconds(timeout); - - TokenCredential tokenCredential; - TokenCredentialOptions tokenCredentialOptions = new TokenCredentialOptions(); - tokenCredentialOptions.AuthorityHost = ClaimsProviderConstants.AzureCloudEndpoints.SingleOrDefault(kvp => kvp.Key == this.CloudInstance).Value; - //tokenCredentialOptions.AuthorityHost = AzureAuthorityHosts.AzurePublicCloud; - - if (!String.IsNullOrWhiteSpace(ClientSecret)) - { - tokenCredential = new ClientSecretCredential(this.Name, this.ApplicationId, this.ApplicationSecret, tokenCredentialOptions); - } - else - { - tokenCredential = new ClientCertificateCredential(this.Name, this.ApplicationId, this.ClientCertificatePrivateKey, tokenCredentialOptions); - } - - // https://learn.microsoft.com/en-us/graph/sdks/customize-client?tabs=csharp - var authProvider = new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider( - credential: tokenCredential, - //allowedHosts: new[] { AzureAuthorityHosts.AzurePublicCloud.ToString() }, - scopes: new[] { "https://graph.microsoft.com/.default", - }); - this.GraphService = new GraphServiceClient(httpClient, authProvider); - //this.GraphService = new GraphServiceClient(tokenCredential, new[] { "User.Read.All", "GroupMember.Read.All" }); - } - catch (Exception ex) - { - ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"while setting client context for tenant '{this.Name}'.", TraceCategory.Core, ex); - } - } - - /// - /// Returns a copy of the current object. This copy does not have any member of the base SharePoint base class set - /// - /// - internal AzureTenant CopyConfiguration() - { - AzureTenant copy = new AzureTenant(); - // 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; - } - - /// - /// Update the credentials used to connect to the Azure AD tenant - /// - /// New application (client) secret - public void UpdateCredentials(string newApplicationSecret) - { - SetCredentials(this.ApplicationId, newApplicationSecret); - } - - /// - /// Set the credentials used to connect to the Azure AD tenant - /// - /// Application (client) ID - /// Application (client) secret - public void SetCredentials(string applicationId, string applicationSecret) - { - this.ApplicationId = applicationId; - this.ApplicationSecret = applicationSecret; - this.ClientCertificatePrivateKey = null; - } - - /// - /// Update the credentials used to connect to the Azure AD tenant - /// - /// New certificate with its private key - public void UpdateCredentials(X509Certificate2 newCertificate) - { - SetCredentials(this.ApplicationId, newCertificate); - } - - /// - /// Set the credentials used to connect to the Azure AD tenant - /// - /// Application (client) secret - /// Certificate with its private key - public void SetCredentials(string applicationId, X509Certificate2 certificate) - { - this.ApplicationId = applicationId; - this.ApplicationSecret = String.Empty; - this.ClientCertificatePrivateKey = certificate; - } - - /// - /// Import the input blob certificate into a pfx X509Certificate2 object - /// - /// - /// - /// - /// - public static X509Certificate2 ImportPfxCertificateBlob(byte[] blob, string certificatePassword, X509KeyStorageFlags keyStorageFlags) - { - if (X509Certificate2.GetCertContentType(blob) != X509ContentType.Pfx) - { - return null; - } - - if (String.IsNullOrWhiteSpace(certificatePassword)) - { - // If passwordless, import private key as documented in https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b - return new X509Certificate2(blob, (string)null, keyStorageFlags); - } - else - { - return new X509Certificate2(blob, certificatePassword, keyStorageFlags); - } - } - } - - /// - /// Contains information about current operation - /// - public class OperationContext - { - /// - /// 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 request is a validation: contains the value of the SPClaim. If request is a search: contains the input - /// - 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(IAzureCPConfiguration 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.FilterExactMatchOnly); - } - 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($"[{AzureCP._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($"[{AzureCP._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 = this.IncomingEntity.Value; - } - - /// - /// 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($"[{AzureCP._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($"[{AzureCP._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."); - } - } - } - - public enum AzureADObjectProperty - { - NotSet, - AccountEnabled, - Department, - DisplayName, - GivenName, - Id, - JobTitle, - Mail, - MobilePhone, - OfficeLocation, - Surname, - UserPrincipalName, - UserType, - // https://github.com/Yvand/AzureCP/issues/77: Include all other String properties of class User - https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties - AgeGroup, - City, - CompanyName, - ConsentProvidedForMinor, - Country, - EmployeeId, - FaxNumber, - LegalAgeGroupClassification, - MailNickname, - OnPremisesDistinguishedName, - OnPremisesImmutableId, - OnPremisesSecurityIdentifier, - OnPremisesDomainName, - OnPremisesSamAccountName, - OnPremisesUserPrincipalName, - PasswordPolicies, - PostalCode, - PreferredLanguage, - State, - StreetAddress, - UsageLocation, - AboutMe, - MySite, - PreferredName, - ODataType, - extensionAttribute1, - extensionAttribute2, - extensionAttribute3, - extensionAttribute4, - extensionAttribute5, - extensionAttribute6, - extensionAttribute7, - extensionAttribute8, - extensionAttribute9, - extensionAttribute10, - extensionAttribute11, - extensionAttribute12, - extensionAttribute13, - extensionAttribute14, - extensionAttribute15 - } - - public enum DirectoryObjectType - { - User, - Group - } - - public enum OperationType - { - Search, - Validation, - Augmentation, - } -} diff --git a/AzureCP/AzureCP.AdminLinks/Elements.xml b/AzureCP/AzureCPSE.Administration.Links/Elements.xml similarity index 61% rename from AzureCP/AzureCP.AdminLinks/Elements.xml rename to AzureCP/AzureCPSE.Administration.Links/Elements.xml index 9f80b6d0..345836ab 100644 --- a/AzureCP/AzureCP.AdminLinks/Elements.xml +++ b/AzureCP/AzureCPSE.Administration.Links/Elements.xml @@ -1,37 +1,37 @@  + Title = "AzureCP Subscription Edition" + ImageUrl="/_layouts/15/AzureCPSE/AzureCP_logo_small.png"> - + - + - + diff --git a/AzureCP/AzureCP.AdminLinks/SharePointProjectItem.spdata b/AzureCP/AzureCPSE.Administration.Links/SharePointProjectItem.spdata similarity index 76% rename from AzureCP/AzureCP.AdminLinks/SharePointProjectItem.spdata rename to AzureCP/AzureCPSE.Administration.Links/SharePointProjectItem.spdata index 955f9baf..95127177 100644 --- a/AzureCP/AzureCP.AdminLinks/SharePointProjectItem.spdata +++ b/AzureCP/AzureCPSE.Administration.Links/SharePointProjectItem.spdata @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/AzureCP/Features/AzureCP/AzureCP.feature b/AzureCP/Features/AzureCP/AzureCP.feature deleted file mode 100644 index 6cc878f4..00000000 --- a/AzureCP/Features/AzureCP/AzureCP.feature +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/AzureCP/Features/AzureCP/AzureCP.Template.xml b/AzureCP/Features/AzureCPSE.Administration/AzureCPSE.Administration.Template.xml similarity index 100% rename from AzureCP/Features/AzureCP/AzureCP.Template.xml rename to AzureCP/Features/AzureCPSE.Administration/AzureCPSE.Administration.Template.xml diff --git a/AzureCP/Features/AzureCPSE.Administration/AzureCPSE.Administration.feature b/AzureCP/Features/AzureCPSE.Administration/AzureCPSE.Administration.feature new file mode 100644 index 00000000..2eeba87d --- /dev/null +++ b/AzureCP/Features/AzureCPSE.Administration/AzureCPSE.Administration.feature @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AzureCP/Features/AzureCP/AzureCP.EventReceiver.cs b/AzureCP/Features/AzureCPSE/AzureCPSE.EventReceiver.cs similarity index 50% rename from AzureCP/Features/AzureCP/AzureCP.EventReceiver.cs rename to AzureCP/Features/AzureCPSE/AzureCPSE.EventReceiver.cs index 5cbe9210..c13eda77 100644 --- a/AzureCP/Features/AzureCP/AzureCP.EventReceiver.cs +++ b/AzureCP/Features/AzureCPSE/AzureCPSE.EventReceiver.cs @@ -3,8 +3,9 @@ using Microsoft.SharePoint.Administration.Claims; using System; using System.Runtime.InteropServices; +using Yvand.ClaimsProviders.Config; -namespace azurecp +namespace Yvand.ClaimsProviders { /// /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade. @@ -17,9 +18,9 @@ public class AzureCPEventReceiver : SPClaimProviderFeatureReceiver { public override string ClaimProviderAssembly => typeof(AzureCP).Assembly.FullName; - public override string ClaimProviderDescription => AzureCP._ProviderInternalName; + public override string ClaimProviderDescription => AzureCP.ClaimsProviderName; - public override string ClaimProviderDisplayName => AzureCP._ProviderInternalName; + public override string ClaimProviderDisplayName => AzureCP.ClaimsProviderName; public override string ClaimProviderType => typeof(AzureCP).FullName; @@ -33,58 +34,58 @@ private void ExecBaseFeatureActivated(Microsoft.SharePoint.SPFeatureReceiverProp // Wrapper function for base FeatureActivated. // Used because base keywork can lead to unverifiable code inside lambda expression base.FeatureActivated(properties); - SPSecurity.RunWithElevatedPrivileges(delegate () + SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate () { try { - ClaimsProviderLogging svc = ClaimsProviderLogging.Local; - ClaimsProviderLogging.Log($"[{AzureCP._ProviderInternalName}] Activating farm-scoped feature for claims provider \"{AzureCP._ProviderInternalName}\"", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - AzureCPConfig existingConfig = AzureCPConfig.GetConfiguration(ClaimsProviderConstants.CONFIG_NAME); - if (existingConfig == null) - { - AzureCPConfig.CreateDefaultConfiguration(); - } - else - { - ClaimsProviderLogging.Log($"[{AzureCP._ProviderInternalName}] Use configuration \"{ClaimsProviderConstants.CONFIG_NAME}\" found in the configuration database", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - } + Logger svc = Logger.Local; + Logger.Log($"[{AzureCP.ClaimsProviderName}] Activating farm-scoped feature for claims provider \"{AzureCP.ClaimsProviderName}\"", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + //AzureCPConfig existingConfig = AzureCPConfig.GetConfiguration(ClaimsProviderConstants.CONFIG_NAME); + //if (existingConfig == null) + //{ + // AzureCPConfig.CreateDefaultConfiguration(); + //} + //else + //{ + // ClaimsProviderLogging.Log($"[{AzureCP._ProviderInternalName}] Use configuration \"{ClaimsProviderConstants.CONFIG_NAME}\" found in the configuration database", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); + //} } catch (Exception ex) { - ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"activating farm-scoped feature for claims provider \"{AzureCP._ProviderInternalName}\"", ClaimsProviderLogging.TraceCategory.Configuration, ex); + Logger.LogException((string)AzureCP.ClaimsProviderName, $"activating farm-scoped feature for claims provider \"{AzureCP.ClaimsProviderName}\"", TraceCategory.Configuration, ex); } }); } public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { - SPSecurity.RunWithElevatedPrivileges(delegate () + SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate () { try { - ClaimsProviderLogging.Log($"[{AzureCP._ProviderInternalName}] Uninstalling farm-scoped feature for claims provider \"{AzureCP._ProviderInternalName}\": Deleting configuration from the farm", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - AzureCPConfig.DeleteConfiguration(ClaimsProviderConstants.CONFIG_NAME); - ClaimsProviderLogging.Unregister(); + Logger.Log($"[{AzureCP.ClaimsProviderName}] Uninstalling farm-scoped feature for claims provider \"{AzureCP.ClaimsProviderName}\": Deleting configuration from the farm", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + //AzureCPConfig.DeleteConfiguration(ClaimsProviderConstants.CONFIG_NAME); + Logger.Unregister(); } catch (Exception ex) { - ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"deactivating farm-scoped feature for claims provider \"{AzureCP._ProviderInternalName}\"", ClaimsProviderLogging.TraceCategory.Configuration, ex); + Logger.LogException((string)AzureCP.ClaimsProviderName, $"deactivating farm-scoped feature for claims provider \"{AzureCP.ClaimsProviderName}\"", TraceCategory.Configuration, ex); } }); } public override void FeatureDeactivating(SPFeatureReceiverProperties properties) { - SPSecurity.RunWithElevatedPrivileges(delegate () + SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate () { try { - ClaimsProviderLogging.Log($"[{AzureCP._ProviderInternalName}] Deactivating farm-scoped feature for claims provider \"{AzureCP._ProviderInternalName}\": Removing claims provider from the farm (but not its configuration)", TraceSeverity.High, EventSeverity.Information, ClaimsProviderLogging.TraceCategory.Configuration); - base.RemoveClaimProvider(AzureCP._ProviderInternalName); + Logger.Log($"[{AzureCP.ClaimsProviderName}] Deactivating farm-scoped feature for claims provider \"{AzureCP.ClaimsProviderName}\": Removing claims provider from the farm (but not its configuration)", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + base.RemoveClaimProvider((string)AzureCP.ClaimsProviderName); } catch (Exception ex) { - ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"deactivating farm-scoped feature for claims provider \"{AzureCP._ProviderInternalName}\"", ClaimsProviderLogging.TraceCategory.Configuration, ex); + Logger.LogException((string)AzureCP.ClaimsProviderName, $"deactivating farm-scoped feature for claims provider \"{AzureCP.ClaimsProviderName}\"", TraceCategory.Configuration, ex); } }); } diff --git a/AzureCP/Features/AzureCP_Administration/AzureCP_Administration.Template.xml b/AzureCP/Features/AzureCPSE/AzureCPSE.Template.xml similarity index 100% rename from AzureCP/Features/AzureCP_Administration/AzureCP_Administration.Template.xml rename to AzureCP/Features/AzureCPSE/AzureCPSE.Template.xml diff --git a/AzureCP/Features/AzureCPSE/AzureCPSE.feature b/AzureCP/Features/AzureCPSE/AzureCPSE.feature new file mode 100644 index 00000000..7cad2f1c --- /dev/null +++ b/AzureCP/Features/AzureCPSE/AzureCPSE.feature @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/AzureCP/Features/AzureCP_Administration/AzureCP_Administration.feature b/AzureCP/Features/AzureCP_Administration/AzureCP_Administration.feature deleted file mode 100644 index e59a844b..00000000 --- a/AzureCP/Features/AzureCP_Administration/AzureCP_Administration.feature +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/AzureCP/Package/Package.package b/AzureCP/Package/Package.package index 1e2206a8..0c72b1d7 100644 --- a/AzureCP/Package/Package.package +++ b/AzureCP/Package/Package.package @@ -1,48 +1,49 @@  - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -50,5 +51,6 @@ + \ No newline at end of file diff --git a/AzureCP/Properties/AssemblyInfo.cs b/AzureCP/Properties/AssemblyInfo.cs index ed37581c..f2f15dbe 100644 --- a/AzureCP/Properties/AssemblyInfo.cs +++ b/AzureCP/Properties/AssemblyInfo.cs @@ -6,12 +6,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these azureObject values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("AzureCP")] +[assembly: AssemblyTitle("AzureCP Subscription Edition")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Yvan Duhamel - yvandev@outlook.fr")] [assembly: AssemblyProduct("AzureCP")] -[assembly: AssemblyCopyright("Copyright © 2018, Yvan Duhamel - yvandev@outlook.fr")] +[assembly: AssemblyCopyright("Copyright © 2023, Yvan Duhamel - yvandev@outlook.fr")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx similarity index 98% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx index ad45b5fb..948861b2 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx @@ -1,6 +1,6 @@ <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ClaimTypesConfig.ascx.cs" Inherits="azurecp.ControlTemplates.ClaimTypesConfigUserControl" %> -<%@ Import Namespace="azurecp.ControlTemplates" %> +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ClaimTypesConfig.ascx.cs" Inherits="Yvand.ClaimsProviders.Administration.ClaimTypesConfigUserControl" %> +<%@ Import Namespace="Yvand.ClaimsProviders.Administration" %> <%@ Register TagPrefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %> diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx.cs b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx.cs similarity index 88% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx.cs rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx.cs index 11687045..3206220f 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx.cs +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx.cs @@ -8,8 +8,10 @@ using System.Text; using System.Web.UI; using System.Web.UI.WebControls; +using Yvand.ClaimsProviders; +using Yvand.ClaimsProviders.Config; -namespace azurecp.ControlTemplates +namespace Yvand.ClaimsProviders.Administration { public partial class ClaimTypesConfigUserControl : AzureCPUserControl { @@ -50,7 +52,7 @@ protected void Initialize() return; } - TrustName = CurrentTrustedLoginProvider.Name; + TrustName = Configuration.SPTrust.Name; if (!this.IsPostBack) { // NEW ITEM FORM @@ -71,7 +73,7 @@ protected void Initialize() DdlNewGraphProperty.Items.Add(String.Empty); DdlNewGraphPropertyToDisplay.Items.Add(String.Empty); - foreach (object field in typeof(AzureADObjectProperty).GetFields()) + foreach (object field in typeof(DirectoryObjectProperty).GetFields()) { string prop = ((System.Reflection.FieldInfo)field).Name; if (AzureCP.GetPropertyValue(new User(), prop) == null) { continue; } @@ -94,7 +96,7 @@ private void BuildAttributesListTable(bool pendingUpdate) // Copy claims list in a key value pair so that each item has a unique ID that can be used later for update/delete operations ClaimsMapping = new List>(); int i = 0; - foreach (ClaimTypeConfig attr in this.PersistedObject.ClaimTypes) + foreach (ClaimTypeConfig attr in this.Configuration.ClaimTypes) { ClaimsMapping.Add(new KeyValuePair(i++, attr)); } @@ -148,7 +150,7 @@ private void BuildAttributesListTable(bool pendingUpdate) tc.Controls.Add(new LiteralControl(String.Format(HtmlEditLink, attr.Key) + "  ")); } // But we don't allow to delete identity claim - if (!String.Equals(attr.Value.ClaimType, CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) + if (!String.Equals(attr.Value.ClaimType, Configuration.SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) { LinkButton LnkDeleteItem = new LinkButton(); LnkDeleteItem.ID = "DeleteItemLink_" + attr.Key; @@ -187,12 +189,12 @@ private void BuildAttributesListTable(bool pendingUpdate) { html = String.Format(HtmlCellClaimType, attr.Value.ClaimType, attr.Key); c = GetTableCell(html); - if (String.Equals(CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType, attr.Value.ClaimType, StringComparison.InvariantCultureIgnoreCase) && !attr.Value.UseMainClaimTypeOfDirectoryObject) + if (String.Equals(Configuration.SPTrust.IdentityClaimTypeInformation.MappedClaimType, attr.Value.ClaimType, StringComparison.InvariantCultureIgnoreCase) && !attr.Value.UseMainClaimTypeOfDirectoryObject) { tr.CssClass = "azurecp-rowidentityclaim"; identityClaimPresent = true; } - else if (CurrentTrustedLoginProvider.ClaimTypeInformation.FirstOrDefault(x => String.Equals(x.MappedClaimType, attr.Value.ClaimType, StringComparison.InvariantCultureIgnoreCase)) == null) + else if (Configuration.SPTrust.ClaimTypeInformation.FirstOrDefault(x => String.Equals(x.MappedClaimType, attr.Value.ClaimType, StringComparison.InvariantCultureIgnoreCase)) == null) { tr.CssClass = "azurecp-rowClaimTypeNotUsedInTrust"; } @@ -250,7 +252,7 @@ private void BuildAttributesListTable(bool pendingUpdate) if (!identityClaimPresent && !pendingUpdate) { - LabelErrorMessage.Text = String.Format(TextErrorNoIdentityClaimType, CurrentTrustedLoginProvider.DisplayName, CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType); + LabelErrorMessage.Text = String.Format(TextErrorNoIdentityClaimType, Configuration.SPTrust.DisplayName, Configuration.SPTrust.IdentityClaimTypeInformation.MappedClaimType); } } @@ -304,7 +306,7 @@ private void BuildGraphPropertyDDLs(KeyValuePair azureObje directoryObjectTypeOptions.Append(String.Format(option, DirectoryObjectType.Group.ToString(), selectedText, DirectoryObjectType.Group.ToString())); // Build DirectoryObjectProperty and DirectoryObjectPropertyToShowAsDisplayText lists - foreach (AzureADObjectProperty prop in Enum.GetValues(typeof(AzureADObjectProperty))) + foreach (DirectoryObjectProperty prop in Enum.GetValues(typeof(DirectoryObjectProperty))) { // Ensure property exists for the current object type if (azureObject.Value.EntityType == DirectoryObjectType.User) @@ -322,9 +324,9 @@ private void BuildGraphPropertyDDLs(KeyValuePair azureObje } } - graphPropertySelected = azureObject.Value.DirectoryObjectProperty == prop ? "selected" : String.Empty; + graphPropertySelected = azureObject.Value.EntityProperty == prop ? "selected" : String.Empty; - if (azureObject.Value.DirectoryObjectPropertyToShowAsDisplayText == prop) + if (azureObject.Value.EntityPropertyToUseAsDisplayText == prop) { graphPropertyToDisplaySelected = "selected"; graphPropertyToDisplayFound = true; @@ -340,10 +342,10 @@ private void BuildGraphPropertyDDLs(KeyValuePair azureObje // Insert at 1st position AzureADObjectProperty.NotSet in GraphPropertyToDisplay DDL and select it if needed string selectNotSet = graphPropertyToDisplayFound ? String.Empty : "selected"; - graphPropertyToDisplayOptions = graphPropertyToDisplayOptions.Insert(0, String.Format(option, AzureADObjectProperty.NotSet, selectNotSet, AzureADObjectProperty.NotSet)); + graphPropertyToDisplayOptions = graphPropertyToDisplayOptions.Insert(0, String.Format(option, DirectoryObjectProperty.NotSet, selectNotSet, DirectoryObjectProperty.NotSet)); - htmlCellGraphProperty = String.Format(HtmlCellGraphProperty, azureObject.Value.DirectoryObjectProperty, azureObject.Key, graphPropertyOptions.ToString()); - string graphPropertyToDisplaySpanDisplay = azureObject.Value.DirectoryObjectPropertyToShowAsDisplayText == AzureADObjectProperty.NotSet ? String.Empty : azureObject.Value.DirectoryObjectPropertyToShowAsDisplayText.ToString(); + htmlCellGraphProperty = String.Format(HtmlCellGraphProperty, azureObject.Value.EntityProperty, azureObject.Key, graphPropertyOptions.ToString()); + string graphPropertyToDisplaySpanDisplay = azureObject.Value.EntityPropertyToUseAsDisplayText == DirectoryObjectProperty.NotSet ? String.Empty : azureObject.Value.EntityPropertyToUseAsDisplayText.ToString(); htmlCellGraphPropertyToDisplay = String.Format(HtmlCellGraphPropertyToDisplay, graphPropertyToDisplaySpanDisplay, azureObject.Key, graphPropertyToDisplayOptions.ToString()); htmlCellDirectoryObjectType = String.Format(HtmlCellDirectoryObjectType, azureObject.Value.EntityType, azureObject.Key, directoryObjectTypeOptions.ToString()); } @@ -367,7 +369,7 @@ void LnkDeleteItem_Command(object sender, CommandEventArgs e) string itemId = e.CommandArgument.ToString(); ClaimTypeConfig ctConfig = ClaimsMapping.Find(x => x.Key == Convert.ToInt32(itemId)).Value; - PersistedObject.ClaimTypes.Remove(ctConfig); + Configuration.ClaimTypes.Remove(ctConfig); CommitChanges(); this.BuildAttributesListTable(false); } @@ -398,22 +400,22 @@ void LnkUpdateItem_Command(object sender, CommandEventArgs e) newCTConfig.PrefixToBypassLookup = formData["input_PrefixToBypassLookup_" + itemId]; newCTConfig.EntityDataKey = formData["list_Metadata_" + itemId]; - AzureADObjectProperty prop; - bool convertSuccess = Enum.TryParse(formData["list_graphproperty_" + itemId], out prop); + DirectoryObjectProperty prop; + bool convertSuccess = Enum.TryParse(formData["list_graphproperty_" + itemId], out prop); if (convertSuccess) { - newCTConfig.DirectoryObjectProperty = prop; + newCTConfig.EntityProperty = prop; } - convertSuccess = Enum.TryParse(formData["list_GraphPropertyToDisplay_" + itemId], out prop); + convertSuccess = Enum.TryParse(formData["list_GraphPropertyToDisplay_" + itemId], out prop); if (convertSuccess) { - newCTConfig.DirectoryObjectPropertyToShowAsDisplayText = prop; + newCTConfig.EntityPropertyToUseAsDisplayText = prop; } try { // ClaimTypeConfigCollection.Update() may thrown an exception if new ClaimTypeConfig is not valid for any reason - PersistedObject.ClaimTypes.Update(existingCTConfig.ClaimType, newCTConfig); + Configuration.ClaimTypes.Update(existingCTConfig.ClaimType, newCTConfig); } catch (Exception ex) { @@ -427,8 +429,8 @@ void LnkUpdateItem_Command(object sender, CommandEventArgs e) protected void BtnReset_Click(object sender, EventArgs e) { - PersistedObject.ResetClaimTypesList(); - PersistedObject.Update(); + Configuration.ResetClaimTypesList(); + Configuration.Update(); Response.Redirect(Request.Url.ToString()); } @@ -440,8 +442,8 @@ protected void BtnReset_Click(object sender, EventArgs e) protected void BtnCreateNewItem_Click(object sender, EventArgs e) { string newClaimType = TxtNewClaimType.Text.Trim(); - AzureADObjectProperty newDirectoryObjectProp; - Enum.TryParse(DdlNewGraphProperty.SelectedValue, out newDirectoryObjectProp); + DirectoryObjectProperty newDirectoryObjectProp; + Enum.TryParse(DdlNewGraphProperty.SelectedValue, out newDirectoryObjectProp); DirectoryObjectType newDirectoryObjectType; Enum.TryParse(DdlNewDirectoryObjectType.SelectedValue, out newDirectoryObjectType); bool useMainClaimTypeOfDirectoryObject = false; @@ -475,17 +477,17 @@ protected void BtnCreateNewItem_Click(object sender, EventArgs e) ClaimTypeConfig newCTConfig = new ClaimTypeConfig(); newCTConfig.ClaimType = newClaimType; - newCTConfig.DirectoryObjectProperty = newDirectoryObjectProp; + newCTConfig.EntityProperty = newDirectoryObjectProp; newCTConfig.EntityType = newDirectoryObjectType; newCTConfig.UseMainClaimTypeOfDirectoryObject = useMainClaimTypeOfDirectoryObject; newCTConfig.EntityDataKey = DdlNewEntityMetadata.SelectedValue; - bool convertSuccess = Enum.TryParse(DdlNewGraphPropertyToDisplay.SelectedValue, out newDirectoryObjectProp); - newCTConfig.DirectoryObjectPropertyToShowAsDisplayText = convertSuccess ? newDirectoryObjectProp : AzureADObjectProperty.NotSet; + bool convertSuccess = Enum.TryParse(DdlNewGraphPropertyToDisplay.SelectedValue, out newDirectoryObjectProp); + newCTConfig.EntityPropertyToUseAsDisplayText = convertSuccess ? newDirectoryObjectProp : DirectoryObjectProperty.NotSet; try { // ClaimTypeConfigCollection.Add() may thrown an exception if new ClaimTypeConfig is not valid for any reason - PersistedObject.ClaimTypes.Add(newCTConfig); + Configuration.ClaimTypes.Add(newCTConfig); } catch (Exception ex) { diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx.designer.cs b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx.designer.cs similarity index 95% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx.designer.cs rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx.designer.cs index f1d2aa91..18102817 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.ascx.designer.cs +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.ascx.designer.cs @@ -7,11 +7,13 @@ // //------------------------------------------------------------------------------ -namespace azurecp.ControlTemplates { - - - public partial class ClaimTypesConfigUserControl { - +namespace Yvand.ClaimsProviders.Administration +{ + + + public partial class ClaimTypesConfigUserControl + { + /// /// LabelMessage control. /// @@ -20,7 +22,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Label LabelMessage; - + /// /// LabelErrorMessage control. /// @@ -29,7 +31,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Label LabelErrorMessage; - + /// /// ValSummary control. /// @@ -38,7 +40,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.ValidationSummary ValSummary; - + /// /// DeleteItemLink_ control. /// @@ -47,7 +49,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.LinkButton DeleteItemLink_; - + /// /// UpdateItemLink_ control. /// @@ -56,7 +58,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.LinkButton UpdateItemLink_; - + /// /// TblClaimsMapping control. /// @@ -65,7 +67,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Table TblClaimsMapping; - + /// /// BtnReset control. /// @@ -74,7 +76,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.Button BtnReset; - + /// /// RdbNewItemClassicClaimType control. /// @@ -83,7 +85,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.RadioButton RdbNewItemClassicClaimType; - + /// /// RdbNewItemLinkdedToIdClaim control. /// @@ -92,7 +94,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.RadioButton RdbNewItemLinkdedToIdClaim; - + /// /// RdbNewItemPermissionMetadata control. /// @@ -101,7 +103,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.RadioButton RdbNewItemPermissionMetadata; - + /// /// TxtNewClaimType control. /// @@ -110,7 +112,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.TextBox TxtNewClaimType; - + /// /// DdlNewDirectoryObjectType control. /// @@ -119,7 +121,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.DropDownList DdlNewDirectoryObjectType; - + /// /// DdlNewGraphProperty control. /// @@ -128,7 +130,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.DropDownList DdlNewGraphProperty; - + /// /// DdlNewGraphPropertyToDisplay control. /// @@ -137,7 +139,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.DropDownList DdlNewGraphPropertyToDisplay; - + /// /// DdlNewEntityMetadata control. /// @@ -146,7 +148,7 @@ public partial class ClaimTypesConfigUserControl { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.DropDownList DdlNewEntityMetadata; - + /// /// BtnCreateNewItem control. /// diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.aspx b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.aspx similarity index 57% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.aspx rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.aspx index ef5f9580..90cea44d 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/ClaimTypesConfig.aspx +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/ClaimTypesConfig.aspx @@ -1,19 +1,20 @@ <%@ Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase" MasterPageFile="~/_admin/admin.master" %> <%@ Register TagPrefix="AzureCP" TagName="ClaimTypesConfigUC" src="ClaimTypesConfig.ascx" %> -<%@ Import Namespace="azurecp" %> +<%@ Import Namespace="Yvand.ClaimsProviders.Config" %> +<%@ Import Namespace="Yvand.ClaimsProviders" %> <%@ Import Namespace="System.Diagnostics" %> <%@ Import Namespace="System.Reflection" %> - Claim types configuration for AzureCP + AzureCP Subscription Edition - Claim types configuration - <%= String.Format("AzureCP {0} - Visit AzureCP site", FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(AzureCP)).Location).FileVersion, ClaimsProviderConstants.PUBLICSITEURL) %> + <%= String.Format("AzureCP Subscription Edition {0}", ClaimsProviderConstants.ClaimsProviderVersion, ClaimsProviderConstants.PUBLICSITEURL) %> - +
diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx similarity index 93% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx index e987d9b4..cb32f4f4 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx @@ -1,5 +1,5 @@ <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> -<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="AzureCPGlobalSettings.ascx.cs" Inherits="azurecp.ControlTemplates.AzureCPGlobalSettings" %> +<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="GlobalSettings.ascx.cs" Inherits="Yvand.ClaimsProviders.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" %> @@ -134,10 +134,10 @@ - + - - + + @@ -262,6 +262,15 @@ + + + + + +
+ +
+
diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx.cs b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx.cs similarity index 74% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx.cs rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx.cs index 6fe6b111..4c3fcbd0 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx.cs +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx.cs @@ -1,5 +1,4 @@ -using Microsoft.Graph; -using Microsoft.Graph.Models; +using Microsoft.Graph.Models; using Microsoft.Identity.Client; using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; @@ -13,20 +12,20 @@ using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; -using static azurecp.ClaimsProviderLogging; +using Yvand.ClaimsProviders.Config; -namespace azurecp.ControlTemplates +namespace Yvand.ClaimsProviders.Administration { - public partial class AzureCPGlobalSettings : AzureCPUserControl + public partial class GlobalSettingsUserControl : AzureCPUserControl { readonly string TextErrorNewTenantFieldsMissing = "Some mandatory fields are missing."; readonly string TextErrorTestAzureADConnection = "Unable to get access token for tenant '{0}': {1}"; readonly string TextConnectionSuccessful = "Connection successful."; readonly string TextErrorNewTenantCreds = "Specify either a client secret or a client certificate, but not both."; readonly string TextErrorExtensionAttributesApplicationId = "Please specify a valid Client ID for AD Connect."; + readonly string TextSummaryPersistedObjectInformation = "Found configuration '{0}' v{1} (Persisted Object ID: '{2}')"; protected void Page_Load(object sender, EventArgs e) { @@ -46,6 +45,7 @@ protected void Initialize() return; } + LabelMessage.Text = String.Format(TextSummaryPersistedObjectInformation, Configuration.Name, Configuration.Version, Configuration.Id); PopulateConnectionsGrid(); if (!this.IsPostBack) { @@ -56,12 +56,12 @@ protected void Initialize() void PopulateConnectionsGrid() { - if (PersistedObject.AzureTenants != null) + if (Configuration.AzureTenants != null) { PropertyCollectionBinder pcb = new PropertyCollectionBinder(); - foreach (AzureTenant tenant in PersistedObject.AzureTenants) + foreach (AzureTenant tenant in Configuration.AzureTenants) { - pcb.AddRow(tenant.Identifier, tenant.Name, tenant.ApplicationId, tenant.CloudInstance.ToString(), tenant.ExtensionAttributesApplicationId); + pcb.AddRow(tenant.Identifier, tenant.Name, tenant.ClientId, tenant.AuthenticationMode, tenant.ExtensionAttributesApplicationId); } pcb.BindGrid(grdAzureTenants); } @@ -69,21 +69,22 @@ void PopulateConnectionsGrid() private void PopulateFields() { - if (IdentityCTConfig.DirectoryObjectPropertyToShowAsDisplayText == AzureADObjectProperty.NotSet) + if (Configuration.LocalSettings.IdentityClaimTypeConfig.EntityPropertyToUseAsDisplayText == DirectoryObjectProperty.NotSet) { this.RbIdentityDefault.Checked = true; } else { this.RbIdentityCustomGraphProperty.Checked = true; - this.DDLGraphPropertyToDisplay.Items.FindByValue(((int)IdentityCTConfig.DirectoryObjectPropertyToShowAsDisplayText).ToString()).Selected = true; + this.DDLGraphPropertyToDisplay.Items.FindByValue(((int)Configuration.LocalSettings.IdentityClaimTypeConfig.EntityPropertyToUseAsDisplayText).ToString()).Selected = true; } - this.DDLDirectoryPropertyMemberUsers.Items.FindByValue(((int)IdentityCTConfig.DirectoryObjectProperty).ToString()).Selected = true; - this.DDLDirectoryPropertyGuestUsers.Items.FindByValue(((int)IdentityCTConfig.DirectoryObjectPropertyForGuestUsers).ToString()).Selected = true; - this.ChkAlwaysResolveUserInput.Checked = PersistedObject.AlwaysResolveUserInput; - this.ChkFilterExactMatchOnly.Checked = PersistedObject.FilterExactMatchOnly; - this.ChkAugmentAADRoles.Checked = PersistedObject.EnableAugmentation; - this.ChkFilterSecurityEnabledGroupsOnly.Checked = PersistedObject.FilterSecurityEnabledGroupsOnly; + this.DDLDirectoryPropertyMemberUsers.Items.FindByValue(((int)Configuration.LocalSettings.IdentityClaimTypeConfig.EntityProperty).ToString()).Selected = true; + this.DDLDirectoryPropertyGuestUsers.Items.FindByValue(((int)Configuration.LocalSettings.IdentityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers).ToString()).Selected = true; + this.ChkAlwaysResolveUserInput.Checked = Configuration.AlwaysResolveUserInput; + this.ChkFilterExactMatchOnly.Checked = Configuration.FilterExactMatchOnly; + this.ChkAugmentAADRoles.Checked = Configuration.EnableAugmentation; + this.ChkFilterSecurityEnabledGroupsOnly.Checked = Configuration.FilterSecurityEnabledGroupsOnly; + this.InputProxyAddress.Text = Configuration.ProxyAddress; AzureCloudInstance[] azureCloudInstanceValues = (AzureCloudInstance[])Enum.GetValues(typeof(AzureCloudInstance)); foreach (var azureCloudInstanceValue in azureCloudInstanceValues) @@ -96,9 +97,9 @@ private void PopulateFields() private void BuildGraphPropertyDDL() { - AzureADObjectProperty[] aadPropValues = (AzureADObjectProperty[])Enum.GetValues(typeof(AzureADObjectProperty)); - IEnumerable aadPropValuesSorted = aadPropValues.OrderBy(v => v.ToString()); - foreach (AzureADObjectProperty prop in aadPropValuesSorted) + DirectoryObjectProperty[] aadPropValues = (DirectoryObjectProperty[])Enum.GetValues(typeof(DirectoryObjectProperty)); + IEnumerable aadPropValuesSorted = aadPropValues.OrderBy(v => v.ToString()); + foreach (DirectoryObjectProperty prop in aadPropValuesSorted) { // Ensure property exists for the User object type if (AzureCP.GetPropertyValue(new User(), prop.ToString()) == null) { continue; } @@ -117,16 +118,17 @@ private void BuildGraphPropertyDDL() protected void grdAzureTenants_RowDeleting(object sender, GridViewDeleteEventArgs e) { if (ValidatePrerequisite() != ConfigStatus.AllGood) { return; } - if (PersistedObject.AzureTenants == null) { return; } + if (Configuration.AzureTenants == null) { return; } GridViewRow rowToDelete = grdAzureTenants.Rows[e.RowIndex]; Guid Id = new Guid(rowToDelete.Cells[0].Text); - AzureTenant tenantToRemove = PersistedObject.AzureTenants.FirstOrDefault(x => x.Identifier == Id); + AzureTenant tenantToRemove = Configuration.AzureTenants.FirstOrDefault(x => x.Identifier == Id); if (tenantToRemove != null) { - PersistedObject.AzureTenants.Remove(tenantToRemove); + Configuration.AzureTenants.Remove(tenantToRemove); CommitChanges(); - ClaimsProviderLogging.Log($"Azure AD tenant '{tenantToRemove.Name}' was successfully removed from configuration '{PersistedObjectName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); + Logger.Log($"Azure AD tenant '{tenantToRemove.Name}' was successfully removed from configuration '{ConfigurationName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); + LabelMessage.Text = String.Format(TextSummaryPersistedObjectInformation, Configuration.Name, Configuration.Version, Configuration.Id); PopulateConnectionsGrid(); } } @@ -137,29 +139,30 @@ protected bool UpdateConfiguration(bool commitChanges) if (this.RbIdentityCustomGraphProperty.Checked) { - IdentityCTConfig.DirectoryObjectPropertyToShowAsDisplayText = (AzureADObjectProperty)Convert.ToInt32(this.DDLGraphPropertyToDisplay.SelectedValue); + Configuration.ClaimTypes.IdentityClaim.EntityPropertyToUseAsDisplayText = (DirectoryObjectProperty)Convert.ToInt32(this.DDLGraphPropertyToDisplay.SelectedValue); } else { - IdentityCTConfig.DirectoryObjectPropertyToShowAsDisplayText = AzureADObjectProperty.NotSet; + Configuration.ClaimTypes.IdentityClaim.EntityPropertyToUseAsDisplayText = DirectoryObjectProperty.NotSet; } - AzureADObjectProperty newUserIdentifier = (AzureADObjectProperty)Convert.ToInt32(this.DDLDirectoryPropertyMemberUsers.SelectedValue); - if (newUserIdentifier != AzureADObjectProperty.NotSet) + DirectoryObjectProperty newUserIdentifier = (DirectoryObjectProperty)Convert.ToInt32(this.DDLDirectoryPropertyMemberUsers.SelectedValue); + if (newUserIdentifier != DirectoryObjectProperty.NotSet) { - PersistedObject.ClaimTypes.UpdateUserIdentifier(newUserIdentifier); + Configuration.ClaimTypes.UpdateUserIdentifier(newUserIdentifier); } - AzureADObjectProperty newIdentifierForGuestUsers = (AzureADObjectProperty)Convert.ToInt32(this.DDLDirectoryPropertyGuestUsers.SelectedValue); - if (newIdentifierForGuestUsers != AzureADObjectProperty.NotSet) + DirectoryObjectProperty newIdentifierForGuestUsers = (DirectoryObjectProperty)Convert.ToInt32(this.DDLDirectoryPropertyGuestUsers.SelectedValue); + if (newIdentifierForGuestUsers != DirectoryObjectProperty.NotSet) { - PersistedObject.ClaimTypes.UpdateIdentifierForGuestUsers(newIdentifierForGuestUsers); + Configuration.ClaimTypes.UpdateIdentifierForGuestUsers(newIdentifierForGuestUsers); } - PersistedObject.AlwaysResolveUserInput = this.ChkAlwaysResolveUserInput.Checked; - PersistedObject.FilterExactMatchOnly = this.ChkFilterExactMatchOnly.Checked; - PersistedObject.EnableAugmentation = this.ChkAugmentAADRoles.Checked; - PersistedObject.FilterSecurityEnabledGroupsOnly = this.ChkFilterSecurityEnabledGroupsOnly.Checked; + Configuration.AlwaysResolveUserInput = this.ChkAlwaysResolveUserInput.Checked; + Configuration.FilterExactMatchOnly = this.ChkFilterExactMatchOnly.Checked; + Configuration.EnableAugmentation = this.ChkAugmentAADRoles.Checked; + Configuration.FilterSecurityEnabledGroupsOnly = this.ChkFilterSecurityEnabledGroupsOnly.Checked; + Configuration.ProxyAddress = this.InputProxyAddress.Text; if (commitChanges) { CommitChanges(); } return true; @@ -241,7 +244,7 @@ protected void BtnOK_Click(Object sender, EventArgs e) protected void BtnResetAzureCPConfig_Click(Object sender, EventArgs e) { - AzureCPConfig.DeleteConfiguration(PersistedObjectName); + AADEntityProviderConfig.DeleteGlobalConfiguration(ConfigurationID); Response.Redirect(Request.RawUrl, false); } @@ -293,24 +296,28 @@ void AddTenantConnection() } } - if (PersistedObject.AzureTenants == null) + Uri cloudInstance = ClaimsProviderConstants.AzureCloudEndpoints.FirstOrDefault(item => item.Key == (AzureCloudInstance)Enum.Parse(typeof(AzureCloudInstance), this.DDLAzureCloudInstance.SelectedValue)).Value; + + + if (Configuration.AzureTenants == null) { - PersistedObject.AzureTenants = new List(); + Configuration.AzureTenants = new List(); } - this.PersistedObject.AzureTenants.Add( + this.Configuration.AzureTenants.Add( new AzureTenant { Name = this.TxtTenantName.Text, - ApplicationId = this.TxtClientId.Text, - ApplicationSecret = this.TxtClientSecret.Text, - ExcludeGuests = this.ChkMemberUserTypeOnly.Checked, - ClientCertificatePrivateKey = cert, - CloudInstance = (AzureCloudInstance)Enum.Parse(typeof(AzureCloudInstance), this.DDLAzureCloudInstance.SelectedValue), + ClientId = this.TxtClientId.Text, + ClientSecret = this.TxtClientSecret.Text, + ExcludeGuestUsers = this.ChkMemberUserTypeOnly.Checked, + ClientCertificateWithPrivateKey = cert, + AzureAuthority = cloudInstance, ExtensionAttributesApplicationId = string.IsNullOrWhiteSpace(this.TxtExtensionAttributesApplicationId.Text) ? Guid.Empty : Guid.Parse(this.TxtExtensionAttributesApplicationId.Text) }); CommitChanges(); - ClaimsProviderLogging.Log($"Azure AD tenant '{this.TxtTenantName.Text}' was successfully added to configuration '{PersistedObjectName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); + Logger.Log($"Azure AD tenant '{this.TxtTenantName.Text}' was successfully added to configuration '{ConfigurationName}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Configuration); + LabelMessage.Text = String.Format(TextSummaryPersistedObjectInformation, Configuration.Name, Configuration.Version, Configuration.Id); PopulateConnectionsGrid(); this.TxtTenantName.Text = "TENANTNAME.onMicrosoft.com"; @@ -384,18 +391,18 @@ public PropertyCollectionBinder() PropertyCollection.Columns.Add("TenantName", typeof(string)); PropertyCollection.Columns.Add("ClientID", typeof(string)); //PropertyCollection.Columns.Add("MemberUserTypeOnly", typeof(bool)); - PropertyCollection.Columns.Add("CloudInstance", typeof(string)); + PropertyCollection.Columns.Add("AuthenticationMode", typeof(string)); PropertyCollection.Columns.Add("ExtensionAttributesApplicationId", typeof(Guid)); } - public void AddRow(Guid Id, string TenantName, string ClientID, string CloudInstance, Guid ExtensionAttributesApplicationId) + public void AddRow(Guid Id, string TenantName, string ClientID, string AuthenticationMode, Guid ExtensionAttributesApplicationId) { DataRow newRow = PropertyCollection.Rows.Add(); newRow["Id"] = Id; newRow["TenantName"] = TenantName; newRow["ClientID"] = ClientID; //newRow["MemberUserTypeOnly"] = MemberUserTypeOnly; - newRow["CloudInstance"] = CloudInstance; + newRow["AuthenticationMode"] = AuthenticationMode; newRow["ExtensionAttributesApplicationId"] = ExtensionAttributesApplicationId; } diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx.designer.cs b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx.designer.cs similarity index 95% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx.designer.cs rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx.designer.cs index cb08e82b..8441b99b 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPGlobalSettings.ascx.designer.cs +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.ascx.designer.cs @@ -7,11 +7,11 @@ // //------------------------------------------------------------------------------ -namespace azurecp.ControlTemplates +namespace Yvand.ClaimsProviders.Administration { - public partial class AzureCPGlobalSettings + public partial class GlobalSettingsUserControl { /// @@ -239,6 +239,15 @@ public partial class AzureCPGlobalSettings /// protected global::System.Web.UI.WebControls.CheckBox ChkAugmentAADRoles; + /// + /// InputProxyAddress control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::Microsoft.SharePoint.WebControls.InputFormTextBox InputProxyAddress; + /// /// ChkFilterSecurityEnabledGroupsOnly control. /// diff --git a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPSettings.aspx b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.aspx similarity index 54% rename from AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPSettings.aspx rename to AzureCP/TEMPLATE/ADMIN/AzureCPSE/GlobalSettings.aspx index e3fa9c0e..340cda02 100644 --- a/AzureCP/TEMPLATE/ADMIN/AzureCP/AzureCPSettings.aspx +++ b/AzureCP/TEMPLATE/ADMIN/AzureCPSE/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="AzureCP" TagName="GlobalSettings" src="AzureCPGlobalSettings.ascx" %> -<%@ Import Namespace="azurecp" %> +<%@ Register TagPrefix="AzureCP" TagName="GlobalSettings" src="GlobalSettings.ascx" %> +<%@ Import Namespace="Yvand.ClaimsProviders.Config" %> +<%@ Import Namespace="Yvand.ClaimsProviders" %> <%@ Import Namespace="System.Diagnostics" %> <%@ Import Namespace="System.Reflection" %> -AzureCP Configuration +AzureCP Subscription Edition - Configuration - <%= String.Format("AzureCP {0} - Visit AzureCP site", ClaimsProviderConstants.ClaimsProviderVersion, ClaimsProviderConstants.PUBLICSITEURL) %> + <%= String.Format("AzureCP Subscription Edition {0}", ClaimsProviderConstants.ClaimsProviderVersion, ClaimsProviderConstants.PUBLICSITEURL) %> - +
diff --git a/AzureCP/TEMPLATE/LAYOUTS/AzureCP/AzureCP_logo_small.png b/AzureCP/TEMPLATE/LAYOUTS/AzureCPSE/AzureCP_logo_small.png similarity index 100% rename from AzureCP/TEMPLATE/LAYOUTS/AzureCP/AzureCP_logo_small.png rename to AzureCP/TEMPLATE/LAYOUTS/AzureCPSE/AzureCP_logo_small.png diff --git a/AzureCP/TEMPLATE/LAYOUTS/AzureCP/jquery-1.9.1.min.js b/AzureCP/TEMPLATE/LAYOUTS/AzureCPSE/jquery-1.9.1.min.js similarity index 100% rename from AzureCP/TEMPLATE/LAYOUTS/AzureCP/jquery-1.9.1.min.js rename to AzureCP/TEMPLATE/LAYOUTS/AzureCPSE/jquery-1.9.1.min.js diff --git a/AzureCP/AzureCPUserControl.cs b/AzureCP/Yvand.ClaimsProviders/Administration/AzureCPUserControl.cs similarity index 62% rename from AzureCP/AzureCPUserControl.cs rename to AzureCP/Yvand.ClaimsProviders/Administration/AzureCPUserControl.cs index d432f450..555c6ede 100644 --- a/AzureCP/AzureCPUserControl.cs +++ b/AzureCP/Yvand.ClaimsProviders/Administration/AzureCPUserControl.cs @@ -1,16 +1,14 @@ -using azurecp; -using Microsoft.SharePoint; +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 azurecp.ClaimsProviderLogging; +using Yvand.ClaimsProviders.Config; -namespace azurecp.ControlTemplates +namespace Yvand.ClaimsProviders.Administration { + // Sadly, using a generic class with a UserControl seems not possible: https://stackoverflow.com/questions/74733106/asp-net-webforms-usercontrol-with-generic-type-parameter + //public abstract class AzureCPUserControl : UserControl where TSettings : EntityProviderConfiguration public abstract class AzureCPUserControl : UserControl { /// @@ -21,54 +19,42 @@ public abstract class AzureCPUserControl : UserControl /// /// This member is used in the markup code and cannot be made as a property /// - public string PersistedObjectName; + public string ConfigurationName; - private Guid _PersistedObjectID; - public string PersistedObjectID - { - get - { - return (this._PersistedObjectID == null || this._PersistedObjectID == Guid.Empty) ? String.Empty : this._PersistedObjectID.ToString(); - } - set - { - this._PersistedObjectID = new Guid(value); - } - } + public Guid ConfigurationID { get; set; } = Guid.Empty; + - private IAzureCPConfiguration _PersistedObject; - protected AzureCPConfig PersistedObject + private AADEntityProviderConfig _Configuration; + protected AADEntityProviderConfig Configuration { get { SPSecurity.RunWithElevatedPrivileges(delegate () { - if (_PersistedObject == null) + if (_Configuration == null) { - _PersistedObject = AzureCPConfig.GetConfiguration(PersistedObjectName, this.CurrentTrustedLoginProvider.Name); + _Configuration = (AADEntityProviderConfig)AADEntityProviderConfig.GetGlobalConfiguration(this.ConfigurationID, true); } - if (_PersistedObject == null) + if (_Configuration == null) { SPContext.Current.Web.AllowUnsafeUpdates = true; - _PersistedObject = AzureCPConfig.CreateConfiguration(this.PersistedObjectID, this.PersistedObjectName, this.CurrentTrustedLoginProvider.Name); + _Configuration = (AADEntityProviderConfig)AADEntityProviderConfig.CreateGlobalConfiguration(this.ConfigurationID, this.ConfigurationName, this.ClaimsProviderName, typeof(AADEntityProviderConfig)); SPContext.Current.Web.AllowUnsafeUpdates = false; } + _Configuration.RefreshLocalSettingsIfNeeded(); }); - return _PersistedObject as AzureCPConfig; + return _Configuration; } - //set { _PersistedObject = value; } } - protected SPTrustedLoginProvider CurrentTrustedLoginProvider; - protected IdentityClaimTypeConfig IdentityCTConfig; protected ConfigStatus Status; - protected long PersistedObjectVersion + protected long ConfigurationVersion { get { if (ViewState[ViewStatePersistedObjectVersionKey] == null) - ViewState.Add(ViewStatePersistedObjectVersionKey, PersistedObject.Version); + ViewState.Add(ViewStatePersistedObjectVersionKey, Configuration.Version); return (long)ViewState[ViewStatePersistedObjectVersionKey]; } set { ViewState[ViewStatePersistedObjectVersionKey] = value; } @@ -93,9 +79,14 @@ protected string MostImportantError return TextErrorPersistedObjectNotFound; } + if ((Status & ConfigStatus.ConfigurationInvalid) == ConfigStatus.ConfigurationInvalid) + { + return TextErrorPersistedConfigInvalid; + } + if ((Status & ConfigStatus.NoIdentityClaimType) == ConfigStatus.NoIdentityClaimType) { - return String.Format(TextErrorNoIdentityClaimType, CurrentTrustedLoginProvider.DisplayName, CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType); + return String.Format(TextErrorNoIdentityClaimType, Configuration.SPTrust.DisplayName, Configuration.SPTrust.IdentityClaimTypeInformation.MappedClaimType); } if ((Status & ConfigStatus.PersistedObjectStale) == ConfigStatus.PersistedObjectStale) @@ -130,6 +121,7 @@ protected string MostImportantError 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."; + protected static readonly string TextErrorPersistedConfigInvalid = "PersistedObject was found but its configuration is not valid. Check the SharePoint logs to see the actual problem."; /// /// Ensures configuration is valid to proceed @@ -143,76 +135,72 @@ public virtual ConfigStatus ValidatePrerequisite() // 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); + ViewState.Add("PersistedObjectName", ConfigurationName); + ViewState.Add("PersistedObjectID", ConfigurationID); } else { ClaimsProviderName = ViewState["ClaimsProviderName"].ToString(); - PersistedObjectName = ViewState["PersistedObjectName"].ToString(); - PersistedObjectID = ViewState["PersistedObjectID"].ToString(); + ConfigurationName = ViewState["PersistedObjectName"].ToString(); + ConfigurationID = new Guid(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 (String.IsNullOrEmpty(ConfigurationName)) { Status |= ConfigStatus.PersistedObjectNamePropNotSet; } + if (ConfigurationID == Guid.Empty) { Status |= ConfigStatus.PersistedObjectIDPropNotSet; } if (Status != ConfigStatus.AllGood) { - ClaimsProviderLogging.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); + Logger.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); // Should not go further if those requirements are not met return Status; } - if (CurrentTrustedLoginProvider == null) + if (Configuration == null) { - CurrentTrustedLoginProvider = AzureCP.GetSPTrustAssociatedWithCP(this.ClaimsProviderName); - if (CurrentTrustedLoginProvider == null) - { - Status |= ConfigStatus.NoSPTrustAssociation; - return Status; - } + Status |= ConfigStatus.PersistedObjectNotFound; + return Status; } - if (PersistedObject == null) + if (Configuration.SPTrust == null) { - Status |= ConfigStatus.PersistedObjectNotFound; + Status |= ConfigStatus.NoSPTrustAssociation; + return Status; } if (Status != ConfigStatus.AllGood) { - ClaimsProviderLogging.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); + Logger.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); // Should not go further if those requirements are not met return Status; } - // AzureCPConfig.GetConfiguration will call method AzureCPConfig.CheckAndCleanConfiguration(); - //PersistedObject.CheckAndCleanConfiguration(CurrentTrustedLoginProvider.Name); - PersistedObject.ClaimTypes.SPTrust = CurrentTrustedLoginProvider; - if (IdentityCTConfig == null && Status == ConfigStatus.AllGood) + if (Configuration.LocalSettings == null) { - IdentityCTConfig = PersistedObject.ClaimTypes.FirstOrDefault(x => String.Equals(CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase) && !x.UseMainClaimTypeOfDirectoryObject) as IdentityClaimTypeConfig; - if (IdentityCTConfig == null) - { - Status |= ConfigStatus.NoIdentityClaimType; - } + Status |= ConfigStatus.ConfigurationInvalid; + return Status; + } + + if (Configuration.LocalSettings.IdentityClaimTypeConfig == null) + { + Status |= ConfigStatus.NoIdentityClaimType; } - if (PersistedObjectVersion != PersistedObject.Version) + if (ConfigurationVersion != Configuration.Version) { Status |= ConfigStatus.PersistedObjectStale; } if (Status != ConfigStatus.AllGood) { - ClaimsProviderLogging.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); + Logger.Log($"[{ClaimsProviderName}] {MostImportantError}", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); } return Status; } public virtual void CommitChanges() { - PersistedObject.Update(); - PersistedObjectVersion = PersistedObject.Version; + Configuration.Update(); + ConfigurationVersion = Configuration.Version; } } @@ -226,6 +214,7 @@ public enum ConfigStatus PersistedObjectStale = 0x8, ClaimsProviderNamePropNotSet = 0x10, PersistedObjectNamePropNotSet = 0x20, - PersistedObjectIDPropNotSet = 0x40 + PersistedObjectIDPropNotSet = 0x40, + ConfigurationInvalid = 0x80, }; } diff --git a/AzureCP/Yvand.ClaimsProviders/AzureAD/AzureADEntityProvider.cs b/AzureCP/Yvand.ClaimsProviders/AzureAD/AzureADEntityProvider.cs new file mode 100644 index 00000000..254b5ebf --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/AzureAD/AzureADEntityProvider.cs @@ -0,0 +1,621 @@ +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.Graph.Groups; +using Microsoft.Graph.Models; +using Microsoft.Graph.Users; +using Microsoft.Graph.Users.Item.GetMemberGroups; +using Microsoft.Identity.Client; +using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options; +using Microsoft.SharePoint.Administration; +using Microsoft.SharePoint.Utilities; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Yvand.ClaimsProviders.Config; + +namespace Yvand.ClaimsProviders.AzureAD +{ + public class AzureADEntityProvider : EntityProviderBase// + { + public AzureADEntityProvider(string claimsProviderName) : base(claimsProviderName) { } + + public async override Task> GetEntityGroupsAsync(OperationContext currentContext, DirectoryObjectProperty groupProperty) + { + List azureTenants = currentContext.AzureTenants; + // URL encode the filter to prevent that it gets truncated like this: "UserPrincipalName eq 'guest_contoso.com" instead of "UserPrincipalName eq 'guest_contoso.com#EXT#@TENANT.onmicrosoft.com'" + string getMemberUserFilter = $"{currentContext.IncomingEntityClaimTypeConfig.EntityProperty} eq '{currentContext.IncomingEntity.Value}'"; + string getGuestUserFilter = $"userType eq 'Guest' and {currentContext.Configuration.IdentityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers} eq '{currentContext.IncomingEntity.Value}'"; + + // Create a task for each tenant to query + IEnumerable>> tenantTasks = azureTenants.Select(async tenant => + { + List groupsInTenant = new List(); + Stopwatch timer = new Stopwatch(); + timer.Start(); + try + { + // Search the user as a member + var userCollectionResult = await tenant.GraphService.Users.GetAsync((config) => + { + config.QueryParameters.Filter = getMemberUserFilter; + config.QueryParameters.Select = new[] { "Id" }; + config.QueryParameters.Top = 1; + }).ConfigureAwait(false); + + User user = userCollectionResult?.Value?.FirstOrDefault(); + if (user == null) + { + // If user was not found, he might be a Guest user. Query to check this: /users?$filter=userType eq 'Guest' and mail eq 'guest@live.com'&$select=userPrincipalName, Id + //string guestFilter = HttpUtility.UrlEncode($"userType eq 'Guest' and {IdentityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers} eq '{currentContext.IncomingEntity.Value}'"); + //userResult = await tenant.GraphService.Users.Request().Filter(guestFilter).Select(HttpUtility.UrlEncode("userPrincipalName, Id")).GetAsync().ConfigureAwait(false); + //userResult = await Task.Run(() => tenant.GraphService.Users.Request().Filter(guestFilter).Select(HttpUtility.UrlEncode("userPrincipalName, Id")).GetAsync()).ConfigureAwait(false); + userCollectionResult = await Task.Run(() => tenant.GraphService.Users.GetAsync((config) => + { + config.QueryParameters.Filter = getGuestUserFilter; + config.QueryParameters.Select = new[] { "Id" }; + config.QueryParameters.Top = 1; + })).ConfigureAwait(false); + user = userCollectionResult?.Value?.FirstOrDefault(); + } + if (user == null) { return groupsInTenant; } + + if (groupProperty == DirectoryObjectProperty.Id) + { + // POST to /v1.0/users/user@TENANT.onmicrosoft.com/microsoft.graph.getMemberGroups is the preferred way to return security groups as it includes nested groups + // But it returns only the group IDs so it can be used only if groupClaimTypeConfig.DirectoryObjectProperty == AzureADObjectProperty.Id + // For Guest users, it must be the id: POST to /v1.0/users/18ff6ae9-dd01-4008-a786-aabf71f1492a/microsoft.graph.getMemberGroups + GetMemberGroupsPostRequestBody getGroupsOptions = new GetMemberGroupsPostRequestBody { SecurityEnabledOnly = currentContext.Configuration.FilterSecurityEnabledGroupsOnly }; + GetMemberGroupsResponse memberGroupsResponse = await Task.Run(() => tenant.GraphService.Users[user.Id].GetMemberGroups.PostAsync(getGroupsOptions)).ConfigureAwait(false); + if (memberGroupsResponse?.Value != null) + { + PageIterator memberGroupsPageIterator = PageIterator.CreatePageIterator( + tenant.GraphService, + memberGroupsResponse, + (groupId) => + { + groupsInTenant.Add(groupId); + return true; // return true to continue the iteration + }); + await memberGroupsPageIterator.IterateAsync().ConfigureAwait(false); + } + } + else + { + // Fallback to GET to /v1.0/users/user@TENANT.onmicrosoft.com/memberOf, which returns all group properties but does not return nested groups + DirectoryObjectCollectionResponse memberOfResponse = await Task.Run(() => tenant.GraphService.Users[user.Id].MemberOf.GetAsync()).ConfigureAwait(false); + if (memberOfResponse?.Value != null) + { + PageIterator memberGroupsPageIterator = PageIterator.CreatePageIterator( + tenant.GraphService, + memberOfResponse, + (group) => + { + string groupClaimValue = GetPropertyValue(group, groupProperty.ToString()); + groupsInTenant.Add(groupClaimValue); + return true; // return true to continue the iteration + }); + await memberGroupsPageIterator.IterateAsync().ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + Logger.LogException(ClaimsProviderName, $"while getting groups for user '{currentContext.IncomingEntity.Value}' in tenant '{tenant.Name}'", TraceCategory.Augmentation, ex); + } + finally + { + timer.Stop(); + } + if (groupsInTenant != null) + { + Logger.Log($"[{ClaimsProviderName}] Got {groupsInTenant.Count} users/groups in {timer.ElapsedMilliseconds.ToString()} ms from '{tenant.Name}' with input '{currentContext.Input}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); + } + else + { + Logger.Log($"[{ClaimsProviderName}] Got no group for user '{currentContext.IncomingEntity.Value}' in tenant, search took {timer.ElapsedMilliseconds} ms", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); + } + return groupsInTenant; + }); + + List groups = new List(); + // Wait for all tasks to complete + List[] groupsInAllTenants = await Task.WhenAll(tenantTasks).ConfigureAwait(false); + for (int i = 0; i < groupsInAllTenants.Length; i++) + { + groups.AddRange(groupsInAllTenants[i]); + } + return groups; + } + + public async override Task> SearchOrValidateEntitiesAsync(OperationContext currentContext) + { + //// this.CurrentConfiguration.AzureTenants must be cloned locally to ensure its properties ($select / $filter) won't be updated by multiple threads + //List azureTenants = new List(this.Configuration.AzureTenants.Count); + //foreach (AzureTenant tenant in this.Configuration.AzureTenants) + //{ + // azureTenants.Add(tenant.CopyPublicProperties()); + //} + this.BuildFilter(currentContext, currentContext.AzureTenants); + List results = await this.QueryAzureADTenantsAsync(currentContext, currentContext.AzureTenants); + return results; + } + + protected virtual void BuildFilter(OperationContext currentContext, List azureTenants) + { + string searchPatternEquals = "{0} eq '{1}'"; + string searchPatternStartsWith = "startswith({0}, '{1}')"; + string identityConfigSearchPatternEquals = "({0} eq '{1}' and UserType eq '{2}')"; + string identityConfigSearchPatternStartsWith = "(startswith({0}, '{1}') and UserType eq '{2}')"; + + List userFilterBuilder = new List(); + List groupFilterBuilder = new List(); + List userSelectBuilder = new List { "UserType", "Mail" }; // UserType and Mail are always needed to deal with Guest users + List groupSelectBuilder = new List { "Id", "securityEnabled" }; // Id is always required for groups + + string filterPattern; + string input = currentContext.Input; + + // https://github.com/Yvand/AzureCP/issues/88: Escape single quotes as documented in https://docs.microsoft.com/en-us/graph/query-parameters#escaping-single-quotes + input = input.Replace("'", "''"); + + if (currentContext.ExactSearch) + { + filterPattern = String.Format(searchPatternEquals, "{0}", input); + } + else + { + filterPattern = String.Format(searchPatternStartsWith, "{0}", input); + } + + foreach (ClaimTypeConfig ctConfig in currentContext.CurrentClaimTypeConfigList) + { + string currentPropertyString = ctConfig.EntityProperty.ToString(); + if (currentPropertyString.StartsWith("extensionAttribute")) + { + currentPropertyString = String.Format("{0}_{1}_{2}", "extension", "EXTENSIONATTRIBUTESAPPLICATIONID", currentPropertyString); + } + + string currentFilter; + if (!ctConfig.SupportsWildcard) + { + currentFilter = String.Format(searchPatternEquals, currentPropertyString, input); + } + else + { + // Use String.Replace instead of String.Format because String.Format trows an exception if input contains a '{' + currentFilter = filterPattern.Replace("{0}", currentPropertyString); + } + + // Id needs a specific check: input must be a valid GUID AND equals filter must be used, otherwise Azure AD will throw an error + if (ctConfig.EntityProperty == DirectoryObjectProperty.Id) + { + Guid idGuid = new Guid(); + if (!Guid.TryParse(input, out idGuid)) + { + continue; + } + else + { + currentFilter = String.Format(searchPatternEquals, currentPropertyString, idGuid.ToString()); + } + } + + if (ctConfig.EntityType == DirectoryObjectType.User) + { + if (ctConfig is IdentityClaimTypeConfig) + { + IdentityClaimTypeConfig identityClaimTypeConfig = ctConfig as IdentityClaimTypeConfig; + if (!ctConfig.SupportsWildcard) + { + currentFilter = "( " + String.Format(identityConfigSearchPatternEquals, currentPropertyString, input, ClaimsProviderConstants.MEMBER_USERTYPE) + " or " + String.Format(identityConfigSearchPatternEquals, identityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers, input, ClaimsProviderConstants.GUEST_USERTYPE) + " )"; + } + else + { + if (currentContext.ExactSearch) + { + currentFilter = "( " + String.Format(identityConfigSearchPatternEquals, currentPropertyString, input, ClaimsProviderConstants.MEMBER_USERTYPE) + " or " + String.Format(identityConfigSearchPatternEquals, identityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers, input, ClaimsProviderConstants.GUEST_USERTYPE) + " )"; + } + else + { + currentFilter = "( " + String.Format(identityConfigSearchPatternStartsWith, currentPropertyString, input, ClaimsProviderConstants.MEMBER_USERTYPE) + " or " + String.Format(identityConfigSearchPatternStartsWith, identityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers, input, ClaimsProviderConstants.GUEST_USERTYPE) + " )"; + } + } + } + + userFilterBuilder.Add(currentFilter); + userSelectBuilder.Add(currentPropertyString); + } + else + { + // else assume it's a Group + groupFilterBuilder.Add(currentFilter); + groupSelectBuilder.Add(currentPropertyString); + } + } + + // Also add metadata properties to $select of corresponding object type + if (userFilterBuilder.Count > 0) + { + foreach (ClaimTypeConfig ctConfig in currentContext.Configuration.RuntimeMetadataConfig.Where(x => x.EntityType == DirectoryObjectType.User)) + { + userSelectBuilder.Add(ctConfig.EntityProperty.ToString()); + } + } + if (groupFilterBuilder.Count > 0) + { + foreach (ClaimTypeConfig ctConfig in currentContext.Configuration.RuntimeMetadataConfig.Where(x => x.EntityType == DirectoryObjectType.Group)) + { + groupSelectBuilder.Add(ctConfig.EntityProperty.ToString()); + } + } + + foreach (AzureTenant tenant in azureTenants) + { + List userFilterBuilderForTenantList; + List groupFilterBuilderForTenantList; + List userSelectBuilderForTenantList; + List groupSelectBuilderForTenantList; + + // Add extension attribute on current tenant only if it is configured for it, otherwise request fails with this error: + // message=Property 'extension_00000000000000000000000000000000_extensionAttribute1' does not exist as a declared property or extension property. + if (tenant.ExtensionAttributesApplicationId == Guid.Empty) + { + userFilterBuilderForTenantList = userFilterBuilder.FindAll(elem => !elem.Contains("EXTENSIONATTRIBUTESAPPLICATIONID")); + groupFilterBuilderForTenantList = groupFilterBuilder.FindAll(elem => !elem.Contains("EXTENSIONATTRIBUTESAPPLICATIONID")); + userSelectBuilderForTenantList = userSelectBuilder.FindAll(elem => !elem.Contains("EXTENSIONATTRIBUTESAPPLICATIONID")); + groupSelectBuilderForTenantList = groupSelectBuilder.FindAll(elem => !elem.Contains("EXTENSIONATTRIBUTESAPPLICATIONID")); + } + else + { + userFilterBuilderForTenantList = userFilterBuilder.Select(elem => elem.Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N"))).ToList(); + groupFilterBuilderForTenantList = groupFilterBuilder.Select(elem => elem.Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N"))).ToList(); + userSelectBuilderForTenantList = userSelectBuilder.Select(elem => elem.Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N"))).ToList(); + groupSelectBuilderForTenantList = groupSelectBuilder.Select(elem => elem.Replace("EXTENSIONATTRIBUTESAPPLICATIONID", tenant.ExtensionAttributesApplicationId.ToString("N"))).ToList(); + } + + if (userFilterBuilder.Count > 0) + { + tenant.UserFilter = String.Join(" or ", userFilterBuilderForTenantList); + } + else + { + // Reset filter if no corresponding object was found in requestInfo.ClaimTypeConfigList, to detect that tenant should not be queried + tenant.UserFilter = String.Empty; + } + + if (groupFilterBuilder.Count > 0) + { + tenant.GroupFilter = String.Join(" or ", groupFilterBuilderForTenantList); + } + else + { + tenant.GroupFilter = String.Empty; + } + + tenant.UserSelect = userSelectBuilderForTenantList.ToArray(); + tenant.GroupSelect = groupSelectBuilderForTenantList.ToArray(); + } + } + + protected async Task> QueryAzureADTenantsAsync(OperationContext currentContext, List azureTenants) + { + // Create a task for each tenant to query + var tenantQueryTasks = azureTenants.Select(async tenant => + { + Stopwatch timer = new Stopwatch(); + List tenantResults = null; + try + { + timer.Start(); + tenantResults = await QueryAzureADTenantAsync(currentContext, tenant).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogException(ClaimsProviderName, $"in QueryAzureADTenantsAsync while querying tenant '{tenant.Name}'", TraceCategory.Lookup, ex); + } + finally + { + timer.Stop(); + } + if (tenantResults != null) + { + Logger.Log($"[{ClaimsProviderName}] Got {tenantResults.Count} users/groups in {timer.ElapsedMilliseconds.ToString()} ms from '{tenant.Name}' with input '{currentContext.Input}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Lookup); + } + else + { + Logger.Log($"[{ClaimsProviderName}] Got no result from '{tenant.Name}' with input '{currentContext.Input}', search took {timer.ElapsedMilliseconds.ToString()} ms", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Lookup); + } + return tenantResults; + }); + + // Wait for all tasks to complete + List allResults = new List(); + List[] tenantsResults = await Task.WhenAll(tenantQueryTasks).ConfigureAwait(false); + for (int i = 0; i < tenantsResults.Length; i++) + { + allResults.AddRange(tenantsResults[i]); + } + return allResults; + } + + protected virtual async Task> QueryAzureADTenantAsync(OperationContext currentContext, AzureTenant tenant) + { + List tenantResults = new List(); + if (String.IsNullOrWhiteSpace(tenant.UserFilter) && String.IsNullOrWhiteSpace(tenant.GroupFilter)) + { + return tenantResults; + } + + if (tenant.GraphService == null) + { + Logger.Log($"[{ClaimsProviderName}] Cannot query Azure AD tenant '{tenant.Name}' because it was not initialized", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Lookup); + return tenantResults; + } + + Logger.Log($"[{ClaimsProviderName}] Querying Azure AD tenant '{tenant.Name}' for users and groups, with input '{currentContext.Input}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Lookup); + object lockAddResultToCollection = new object(); + int timeout = currentContext.Configuration.Timeout; + int maxRetry = currentContext.OperationType == OperationType.Validation ? 3 : 2; + int tenantResultCount = 0; + + try + { + using (new SPMonitoredScope($"[{ClaimsProviderName}] Querying Azure AD tenant '{tenant.Name}' for users and groups, with input '{currentContext.Input}'", 1000)) + { + RetryHandlerOption retryHandlerOption = new RetryHandlerOption() + { + Delay = 1, + RetriesTimeLimit = TimeSpan.FromMilliseconds(timeout), + MaxRetry = maxRetry, + ShouldRetry = (delay, attempt, httpResponse) => + { + // Pointless to retry if this is Unauthorized + if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return false; + } + return true; + } + }; + + // Build the batch + BatchRequestContent batchRequestContent = new BatchRequestContent(tenant.GraphService); + string usersRequestId = String.Empty; + if (!String.IsNullOrWhiteSpace(tenant.UserFilter)) + { + // https://stackoverflow.com/questions/56417435/when-i-set-an-object-using-an-action-the-object-assigned-is-always-null + RequestInformation userRequest = tenant.GraphService.Users.ToGetRequestInformation(conf => + { + conf.QueryParameters = new UsersRequestBuilder.UsersRequestBuilderGetQueryParameters + { + Count = true, + Filter = tenant.UserFilter, + Select = tenant.UserSelect, + Top = currentContext.MaxCount, + }; + conf.Headers = new RequestHeaders + { + // Allow Advanced query as documented in https://learn.microsoft.com/en-us/graph/sdks/create-requests?tabs=csharp#retrieve-a-list-of-entities + //to fix $filter on CompanyName - https://github.com/Yvand/AzureCP/issues/166 + { "ConsistencyLevel", "eventual" } + }; + conf.Options = new List + { + retryHandlerOption, + }; + }); + // Using AddBatchRequestStepAsync adds each request as a step with no specified order of execution + usersRequestId = await batchRequestContent.AddBatchRequestStepAsync(userRequest).ConfigureAwait(false); + } + + // Groups + string groupsRequestId = String.Empty; + if (!String.IsNullOrWhiteSpace(tenant.GroupFilter)) + { + RequestInformation groupRequest = tenant.GraphService.Groups.ToGetRequestInformation(conf => + { + conf.QueryParameters = new GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters + { + Count = true, + Filter = tenant.GroupFilter, + Select = tenant.GroupSelect, + Top = currentContext.MaxCount, + }; + conf.Headers = new RequestHeaders + { + // Allow Advanced query as documented in https://learn.microsoft.com/en-us/graph/sdks/create-requests?tabs=csharp#retrieve-a-list-of-entities + //to fix $filter on CompanyName - https://github.com/Yvand/AzureCP/issues/166 + { "ConsistencyLevel", "eventual" } + }; + conf.Options = new List + { + retryHandlerOption, + }; + }); + // Using AddBatchRequestStepAsync adds each request as a step with no specified order of execution + groupsRequestId = await batchRequestContent.AddBatchRequestStepAsync(groupRequest).ConfigureAwait(false); + } + + BatchResponseContent returnedResponse = await tenant.GraphService.Batch.PostAsync(batchRequestContent).ConfigureAwait(false); + UserCollectionResponse userCollectionResult = await returnedResponse.GetResponseByIdAsync(usersRequestId).ConfigureAwait(false); + GroupCollectionResponse groupCollectionResult = await returnedResponse.GetResponseByIdAsync(groupsRequestId).ConfigureAwait(false); + + Logger.Log($"[{ClaimsProviderName}] Query to tenant '{tenant.Name}' returned {(userCollectionResult?.Value == null ? 0 : userCollectionResult.Value.Count)} user(s) with filter \"{tenant.UserFilter}\"", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Lookup); + // Process users result + if (userCollectionResult?.Value != null) + { + PageIterator usersPageIterator = PageIterator.CreatePageIterator( + tenant.GraphService, + userCollectionResult, + (user) => + { + bool addUser = false; + if (tenant.ExcludeMemberUsers == false || tenant.ExcludeGuestUsers == false) + { + bool userIsAMember = String.Equals(user.UserType, ClaimsProviderConstants.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase); + bool userIsAGuest = !userIsAMember; + + if (tenant.ExcludeMemberUsers == false && tenant.ExcludeGuestUsers == false) + { + addUser = true; + } + else if (tenant.ExcludeMemberUsers == true && userIsAMember == false + || tenant.ExcludeGuestUsers == true && userIsAGuest == false) + { + addUser = true; + } + } + + bool continueIteration = true; + if (addUser) + { + lock (lockAddResultToCollection) + { + if (tenantResultCount < currentContext.MaxCount) + { + tenantResults.Add(user); + tenantResultCount++; + } + else + { + continueIteration = false; + } + } + } + return continueIteration; // return true to continue the iteration + }); + await usersPageIterator.IterateAsync().ConfigureAwait(false); + } + + // Process groups result + if (groupCollectionResult?.Value != null) + { + PageIterator groupsPageIterator = PageIterator.CreatePageIterator( + tenant.GraphService, + groupCollectionResult, + (group) => + { + bool continueIteration = true; + lock (lockAddResultToCollection) + { + if (tenantResultCount < currentContext.MaxCount) + { + tenantResults.Add(group); + tenantResultCount++; + } + else + { + continueIteration = false; + } + } + return continueIteration; // return true to continue the iteration + }); + await groupsPageIterator.IterateAsync().ConfigureAwait(false); + } + + //// Cannot use Task.WaitAll() because it's actually blocking the threads, preventing parallel queries on others AAD tenants. + //// Use await Task.WhenAll() as it does not block other threads, so all AAD tenants are actually queried in parallel. + //// More info: https://stackoverflow.com/questions/12337671/using-async-await-for-multiple-tasks + //await Task.WhenAll(new Task[1] { batchQueryTask }).ConfigureAwait(false); + //ClaimsProviderLogging.LogDebug($"Waiting on Task.WaitAll for {tenant.Name} finished"); + } + } + catch (OperationCanceledException) + { + Logger.Log($"[{ClaimsProviderName}] Queries on Azure AD tenant '{tenant.Name}' exceeded timeout of {timeout} ms and were cancelled.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Lookup); + } + catch (AuthenticationFailedException ex) + { + Logger.LogException(ClaimsProviderName, $": Could not authenticate for tenant '{tenant.Name}'", TraceCategory.Lookup, ex); + } + catch (MsalServiceException ex) + { + Logger.LogException(ClaimsProviderName, $": Msal could not query tenant '{tenant.Name}'", TraceCategory.Lookup, ex); + } + catch (ServiceException ex) + { + Logger.LogException(ClaimsProviderName, $": Microsoft.Graph could not query tenant '{tenant.Name}'", TraceCategory.Lookup, ex); + } + catch (AggregateException ex) + { + // Task.WaitAll throws an AggregateException, which contains all exceptions thrown by tasks it waited on + Logger.LogException(ClaimsProviderName, $"while querying Azure AD tenant '{tenant.Name}'", TraceCategory.Lookup, ex); + } + finally + { + } + return tenantResults; + } + + /// + /// Uses reflection to return the value of a public property for the given object + /// + /// + /// + /// Null if property doesn't exist, String.Empty if property exists but has no value, actual value otherwise + public static string GetPropertyValue(DirectoryObject directoryObject, string propertyName) + { + if (directoryObject == null) + { + return null; + } + + if (propertyName.StartsWith("extensionAttribute")) + { + try + { + var returnString = string.Empty; + if (directoryObject is User) + { + var userobject = (User)directoryObject; + if (userobject.AdditionalData != null) + { + var obj = userobject.AdditionalData.FirstOrDefault(s => s.Key.EndsWith(propertyName)); + if (obj.Value != null) + { + returnString = obj.Value.ToString(); + } + else + { + return null; + } + } + } + else if (directoryObject is Group) + { + var groupobject = (Group)directoryObject; + if (groupobject.AdditionalData != null) + { + var obj = groupobject.AdditionalData.FirstOrDefault(s => s.Key.EndsWith(propertyName)); + if (obj.Value != null) + { + returnString = obj.Value.ToString(); + } + else + { + return null; + } + } + } + return returnString == null ? propertyName : returnString; + } + catch + { + return null; + } + } + + PropertyInfo pi = directoryObject.GetType().GetProperty(propertyName); + if (pi == null) + { + return null; + } // Property doesn't exist + object propertyValue = pi.GetValue(directoryObject, null); + return propertyValue == null ? String.Empty : propertyValue.ToString(); + } + } +} diff --git a/AzureCP/Yvand.ClaimsProviders/AzureCP.cs b/AzureCP/Yvand.ClaimsProviders/AzureCP.cs new file mode 100644 index 00000000..a1de3327 --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/AzureCP.cs @@ -0,0 +1,1004 @@ +using Microsoft.Graph.Models; +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.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Yvand.ClaimsProviders.AzureAD; +using Yvand.ClaimsProviders.Config; +using WIF4_5 = System.Security.Claims; + +namespace Yvand.ClaimsProviders +{ + public class AzureCP : SPClaimProvider + { + public static string ClaimsProviderName => "AzureCPSE"; + public override string Name => ClaimsProviderName; + 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; + public AzureADEntityProvider EntityProvider { get; private set; } + private ReaderWriterLockSlim Lock_LocalConfigurationRefresh = new ReaderWriterLockSlim(); + protected virtual string PickerEntityDisplayText => "({0}) {1}"; + protected virtual string PickerEntityOnMouseOver => "{0}={1}"; + protected AADEntityProviderConfig PersistedConfiguration { get; private set; } + public IAADSettings LocalSettings { get; private set; } + + /// + /// Gets the issuer formatted to be like the property SPClaim.OriginalIssuer: "TrustedProvider:TrustedProviderName" + /// + public string OriginalIssuerName => this.PersistedConfiguration.SPTrust != null ? SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, this.PersistedConfiguration.SPTrust.Name) : String.Empty; + + public AzureCP(string displayName) : base(displayName) + { + this.EntityProvider = new AzureADEntityProvider(Name); + } + + public static AADEntityProviderConfig GetConfiguration(bool initializeLocalConfiguration = false) + { + AADEntityProviderConfig configuration = (AADEntityProviderConfig)AADEntityProviderConfig.GetGlobalConfiguration(new Guid(ClaimsProviderConstants.CONFIGURATION_ID), initializeLocalConfiguration); + return configuration; + } + + public static AADEntityProviderConfig CreateConfiguration() + { + AADEntityProviderConfig configuration = (AADEntityProviderConfig)AADEntityProviderConfig.CreateGlobalConfiguration(new Guid(ClaimsProviderConstants.CONFIGURATION_ID), ClaimsProviderConstants.CONFIGURATION_NAME, AzureCP.ClaimsProviderName, typeof(AADEntityProviderConfig)); + return configuration; + } + + public static void DeleteConfiguration() + { + AADEntityProviderConfig configuration = (AADEntityProviderConfig)AADEntityProviderConfig.GetGlobalConfiguration(new Guid(ClaimsProviderConstants.CONFIGURATION_ID)); + if (configuration != null) + { + configuration.Delete(); + } + } + + public bool ValidateLocalConfiguration(Uri context) + { + if (!Utils.ShouldRun(context, Name)) + { + return false; + } + + bool success = true; + this.Lock_LocalConfigurationRefresh.EnterWriteLock(); + try + { + if (this.PersistedConfiguration == null) + { + this.PersistedConfiguration = (AADEntityProviderConfig)AADEntityProviderConfig.GetGlobalConfiguration(new Guid(ClaimsProviderConstants.CONFIGURATION_ID)); + } + if (this.PersistedConfiguration != null) + { + LocalSettings = this.PersistedConfiguration.RefreshLocalSettingsIfNeeded(); + if (LocalSettings == null) + { + success = false; + } + } + else + { + success = false; + } + } + catch (Exception ex) + { + success = false; + Logger.LogException(Name, "while refreshing configuration", TraceCategory.Core, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitWriteLock(); + } + return success; + } + + /// + /// Search or validate incoming input or entity + /// + /// Information about current context and operation + /// Entities generated by AzureCP + protected List SearchOrValidate(OperationContext currentContext) + { + List azureADEntityList = null; + List pickerEntityList = new List(); + try + { + if (this.LocalSettings.AlwaysResolveUserInput) + { + // Completely bypass query to Azure AD + pickerEntityList = CreatePickerEntityForSpecificClaimTypes( + currentContext.Input, + currentContext.CurrentClaimTypeConfigList.FindAll(x => !x.UseMainClaimTypeOfDirectoryObject), + false); + Logger.Log($"[{Name}] Created {pickerEntityList.Count} entity(ies) without contacting Azure AD tenant(s) because AzureCP property AlwaysResolveUserInput is set to true.", + TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); + return pickerEntityList; + } + + if (currentContext.OperationType == OperationType.Search) + { + // Call async method in a task to avoid error "Asynchronous operations are not allowed in this context" error when permission is validated (POST from people picker) + // More info on the error: https://stackoverflow.com/questions/672237/running-an-asynchronous-operation-triggered-by-an-asp-net-web-page-request + Task azureADQueryTask = Task.Run(async () => + { + azureADEntityList = await SearchOrValidateInAzureADAsync(currentContext).ConfigureAwait(false); + }); + azureADQueryTask.Wait(); + pickerEntityList = this.ProcessAzureADResults(currentContext, azureADEntityList); + + // 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 pickerEntityList; + } + PickerEntity entity = CreatePickerEntityForSpecificClaimType( + inputWithoutPrefix, + ctConfigWithInputPrefixMatch, + true); + if (entity != null) + { + if (pickerEntityList == null) { pickerEntityList = new List(); } + pickerEntityList.Add(entity); + Logger.Log($"[{Name}] Created entity without contacting Azure AD tenant(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) + { + // Call async method in a task to avoid error "Asynchronous operations are not allowed in this context" error when permission is validated (POST from people picker) + // More info on the error: https://stackoverflow.com/questions/672237/running-an-asynchronous-operation-triggered-by-an-asp-net-web-page-request + Task azureADQueryTask = Task.Run(async () => + { + azureADEntityList = await SearchOrValidateInAzureADAsync(currentContext).ConfigureAwait(false); + }); + azureADQueryTask.Wait(); + if (azureADEntityList?.Count == 1) + { + // Got the expected count (1 DirectoryObject) + pickerEntityList = this.ProcessAzureADResults(currentContext, azureADEntityList); + } + //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) + { + pickerEntityList = new List(1) { entity }; + Logger.Log($"[{Name}] Validated entity without contacting Azure AD tenant(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) + { + Logger.LogException(Name, "in SearchOrValidate", TraceCategory.Claims_Picking, ex); + } + pickerEntityList = this.ValidateEntities(currentContext, pickerEntityList); + return pickerEntityList; + } + + /// + /// Override this method to inspect the entities generated by AzureCP, and remove some before they are returned to SharePoint. + /// + /// Entities generated by AzureCP + /// List of entities that AzureCP will return to SharePoint + protected virtual List ValidateEntities(OperationContext currentContext, List entities) + { + return entities; + } + + protected async Task> SearchOrValidateInAzureADAsync(OperationContext currentContext) + { + using (new SPMonitoredScope($"[{Name}] Total time spent to query Azure AD tenant(s)", 1000)) + { + List results = await this.EntityProvider.SearchOrValidateEntitiesAsync(currentContext).ConfigureAwait(false); + return results; + } + } + + protected virtual List ProcessAzureADResults(OperationContext currentContext, List usersAndGroups) + { + if (usersAndGroups == null || !usersAndGroups.Any()) + { + return null; + }; + + List ctConfigs = currentContext.CurrentClaimTypeConfigList; + //Really? + //if (currentContext.ExactSearch) + //{ + // ctConfigs = currentContext.CurrentClaimTypeConfigList.FindAll(x => !x.UseMainClaimTypeOfDirectoryObject); + //} + + List processedResults = new List(); + foreach (DirectoryObject userOrGroup in usersAndGroups) + { + DirectoryObject currentObject = null; + DirectoryObjectType objectType; + if (userOrGroup is User) + { + currentObject = userOrGroup; + objectType = DirectoryObjectType.User; + } + else + { + currentObject = userOrGroup; + objectType = DirectoryObjectType.Group; + + if (this.LocalSettings.FilterSecurityEnabledGroupsOnly) + { + Group group = (Group)userOrGroup; + // If Group.SecurityEnabled is not set, assume the group is not SecurityEnabled - verified per tests, it is not documentated in https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0 + bool isSecurityEnabled = group.SecurityEnabled ?? false; + if (!isSecurityEnabled) + { + continue; + } + } + } + + foreach (ClaimTypeConfig ctConfig in ctConfigs.Where(x => x.EntityType == objectType)) + { + // Get value with of current GraphProperty + string directoryObjectPropertyValue = GetPropertyValue(currentObject, ctConfig.EntityProperty.ToString()); + + if (ctConfig is IdentityClaimTypeConfig) + { + if (String.Equals(((User)currentObject).UserType, ClaimsProviderConstants.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) + { + // For Guest users, use the value set in property DirectoryObjectPropertyForGuestUsers + directoryObjectPropertyValue = GetPropertyValue(currentObject, ((IdentityClaimTypeConfig)ctConfig).DirectoryObjectPropertyForGuestUsers.ToString()); + } + } + + // Check if property exists (not null) and has a value (not String.Empty) + if (String.IsNullOrEmpty(directoryObjectPropertyValue)) { continue; } + + // Check if current value mathes input, otherwise go to next GraphProperty to check + if (currentContext.ExactSearch) + { + if (!String.Equals(directoryObjectPropertyValue, currentContext.Input, StringComparison.InvariantCultureIgnoreCase)) { continue; } + } + else + { + if (!directoryObjectPropertyValue.StartsWith(currentContext.Input, StringComparison.InvariantCultureIgnoreCase)) { continue; } + } + + // Current DirectoryObjectProperty value matches user input. Add current result to search results if it is not already present + string entityClaimValue = directoryObjectPropertyValue; + ClaimTypeConfig claimTypeConfigToCompare; + if (ctConfig.UseMainClaimTypeOfDirectoryObject) + { + if (objectType == DirectoryObjectType.User) + { + claimTypeConfigToCompare = this.LocalSettings.IdentityClaimTypeConfig; + if (String.Equals(((User)currentObject).UserType, ClaimsProviderConstants.GUEST_USERTYPE, StringComparison.InvariantCultureIgnoreCase)) + { + // For Guest users, use the value set in property DirectoryObjectPropertyForGuestUsers + entityClaimValue = GetPropertyValue(currentObject, this.LocalSettings.IdentityClaimTypeConfig.DirectoryObjectPropertyForGuestUsers.ToString()); + } + else + { + // Get the value of the DirectoryObjectProperty linked to current directory object + entityClaimValue = GetPropertyValue(currentObject, claimTypeConfigToCompare.EntityProperty.ToString()); + } + } + else + { + claimTypeConfigToCompare = this.LocalSettings.MainGroupClaimTypeConfig; + // Get the value of the DirectoryObjectProperty linked to current directory object + entityClaimValue = GetPropertyValue(currentObject, claimTypeConfigToCompare.EntityProperty.ToString()); + } + + if (String.IsNullOrEmpty(entityClaimValue)) { continue; } + } + else + { + claimTypeConfigToCompare = ctConfig; + } + + // if claim type and claim value already exists, skip + bool resultAlreadyExists = processedResults.Exists(x => + String.Equals(x.ClaimTypeConfig.ClaimType, claimTypeConfigToCompare.ClaimType, StringComparison.InvariantCultureIgnoreCase) && + String.Equals(x.PermissionValue, entityClaimValue, StringComparison.InvariantCultureIgnoreCase)); + if (resultAlreadyExists) { continue; } + + // Passed the checks, add it to the processedResults list + processedResults.Add(new ClaimsProviderEntityResult(currentObject, ctConfig, entityClaimValue, directoryObjectPropertyValue)); + + } + } + + List entities = new List(); + Logger.Log($"[{Name}] {processedResults.Count} entity(ies) to create after filtering", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Lookup); + foreach (ClaimsProviderEntityResult result in processedResults) + { + entities.Add(CreatePickerEntityHelper(result)); + } + return entities; + } + + protected virtual PickerEntity CreatePickerEntityHelper(ClaimsProviderEntityResult result) + { + PickerEntity entity = CreatePickerEntity(); + SPClaim claim; + string permissionValue = result.PermissionValue; + string permissionClaimType = result.ClaimTypeConfig.ClaimType; + bool isMappedClaimTypeConfig = false; + + if (String.Equals(result.ClaimTypeConfig.ClaimType, this.LocalSettings.IdentityClaimTypeConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase) + || result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject) + { + isMappedClaimTypeConfig = true; + } + + entity.EntityType = result.ClaimTypeConfig.SharePointEntityType; + if (result.ClaimTypeConfig.UseMainClaimTypeOfDirectoryObject) + { + string claimValueType; + if (result.ClaimTypeConfig.EntityType == DirectoryObjectType.User) + { + permissionClaimType = this.LocalSettings.IdentityClaimTypeConfig.ClaimType; + claimValueType = this.LocalSettings.IdentityClaimTypeConfig.ClaimValueType; + if (String.IsNullOrEmpty(entity.EntityType)) + { + entity.EntityType = SPClaimEntityTypes.User; + } + } + else + { + permissionClaimType = this.LocalSettings.MainGroupClaimTypeConfig.ClaimType; + claimValueType = this.LocalSettings.MainGroupClaimTypeConfig.ClaimValueType; + if (String.IsNullOrEmpty(entity.EntityType)) + { + entity.EntityType = ClaimsProviderConstants.GroupClaimEntityType; + } + } + permissionValue = FormatPermissionValue(permissionClaimType, permissionValue, isMappedClaimTypeConfig, result); + claim = CreateClaim( + permissionClaimType, + permissionValue, + claimValueType); + } + else + { + permissionValue = FormatPermissionValue(permissionClaimType, permissionValue, isMappedClaimTypeConfig, result); + claim = CreateClaim( + permissionClaimType, + permissionValue, + result.ClaimTypeConfig.ClaimValueType); + if (String.IsNullOrEmpty(entity.EntityType)) + { + entity.EntityType = result.ClaimTypeConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; + } + } + + entity.Claim = claim; + entity.IsResolved = true; + //entity.EntityGroupName = ""; + entity.Description = String.Format( + PickerEntityOnMouseOver, + result.ClaimTypeConfig.EntityProperty.ToString(), + result.DirectoryObjectPropertyValue); + + int nbMetadata = 0; + // If current result is a SharePoint group but was found on an AAD User object, then 1 to many User objects could match so no metadata from the current match should be set + if (!String.Equals(result.ClaimTypeConfig.SharePointEntityType, ClaimsProviderConstants.GroupClaimEntityType, StringComparison.InvariantCultureIgnoreCase) || + result.ClaimTypeConfig.EntityType != DirectoryObjectType.User) + { + // Populate metadata of new PickerEntity + foreach (ClaimTypeConfig ctConfig in this.LocalSettings.RuntimeMetadataConfig.Where(x => x.EntityType == result.ClaimTypeConfig.EntityType)) + { + // if there is actally a value in the GraphObject, then it can be set + string entityAttribValue = GetPropertyValue(result.DirectoryEntity, ctConfig.EntityProperty.ToString()); + if (!String.IsNullOrEmpty(entityAttribValue)) + { + entity.EntityData[ctConfig.EntityDataKey] = entityAttribValue; + nbMetadata++; + Logger.Log($"[{Name}] Set metadata '{ctConfig.EntityDataKey}' of new entity to '{entityAttribValue}'", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); + } + } + } + entity.DisplayText = FormatPermissionDisplayText(entity, isMappedClaimTypeConfig, result); + Logger.Log($"[{Name}] Created entity: display text: '{entity.DisplayText}', value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}', and filled with {nbMetadata.ToString()} metadata.", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); + return entity; + } + + protected virtual PickerEntity CreatePickerEntityForSpecificClaimType(string input, ClaimTypeConfig ctConfig, bool inputHasKeyword) + { + List entities = CreatePickerEntityForSpecificClaimTypes( + input, + new List() + { + ctConfig, + }, + inputHasKeyword); + return entities == null ? null : entities.First(); + } + + protected virtual List CreatePickerEntityForSpecificClaimTypes(string input, List ctConfigs, bool inputHasKeyword) + { + List entities = new List(); + foreach (var ctConfig in ctConfigs) + { + SPClaim claim = CreateClaim(ctConfig.ClaimType, input, ctConfig.ClaimValueType); + PickerEntity entity = CreatePickerEntity(); + entity.Claim = claim; + entity.IsResolved = true; + entity.EntityType = ctConfig.SharePointEntityType; + if (String.IsNullOrEmpty(entity.EntityType)) + { + entity.EntityType = ctConfig.EntityType == DirectoryObjectType.User ? SPClaimEntityTypes.User : ClaimsProviderConstants.GroupClaimEntityType; + } + //entity.EntityGroupName = ""; + entity.Description = String.Format(PickerEntityOnMouseOver, ctConfig.EntityProperty.ToString(), input); + + if (!String.IsNullOrEmpty(ctConfig.EntityDataKey)) + { + entity.EntityData[ctConfig.EntityDataKey] = entity.Claim.Value; + Logger.Log($"[{Name}] Added metadata '{ctConfig.EntityDataKey}' with value '{entity.EntityData[ctConfig.EntityDataKey]}' to new entity", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); + } + + ClaimsProviderEntityResult result = new ClaimsProviderEntityResult(null, ctConfig, input, input); + bool isIdentityClaimType = String.Equals(claim.ClaimType, this.LocalSettings.IdentityClaimTypeConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase); + entity.DisplayText = FormatPermissionDisplayText(entity, isIdentityClaimType, result); + + entities.Add(entity); + Logger.Log($"[{Name}] Created entity: display text: '{entity.DisplayText}', value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'.", TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Claims_Picking); + } + return entities.Count > 0 ? entities : null; + } + + /// + /// Override this method to customize value of permission created + /// + /// + /// + /// + /// + /// + protected virtual string FormatPermissionValue(string claimType, string claimValue, bool isIdentityClaimType, ClaimsProviderEntityResult result) + { + return claimValue; + } + + /// + /// Override this method to customize display text of permission created + /// + /// + /// + /// + /// + protected virtual string FormatPermissionDisplayText(PickerEntity entity, bool isMappedClaimTypeConfig, ClaimsProviderEntityResult result) + { + string entityDisplayText = this.LocalSettings.EntityDisplayTextPrefix; + if (result.ClaimTypeConfig.EntityPropertyToUseAsDisplayText != DirectoryObjectProperty.NotSet) + { + if (!isMappedClaimTypeConfig || result.ClaimTypeConfig.EntityType == DirectoryObjectType.Group) + { + entityDisplayText += "(" + result.ClaimTypeConfig.ClaimTypeDisplayName + ") "; + } + + string graphPropertyToDisplayValue = GetPropertyValue(result.DirectoryEntity, result.ClaimTypeConfig.EntityPropertyToUseAsDisplayText.ToString()); + if (!String.IsNullOrEmpty(graphPropertyToDisplayValue)) + { + entityDisplayText += graphPropertyToDisplayValue; + } + else + { + entityDisplayText += result.PermissionValue; + } + } + else + { + if (isMappedClaimTypeConfig) + { + entityDisplayText += result.DirectoryObjectPropertyValue; + } + else + { + entityDisplayText += String.Format( + PickerEntityDisplayText, + result.ClaimTypeConfig.ClaimTypeDisplayName, + result.PermissionValue); + } + } + return entityDisplayText; + } + + /// + /// Uses reflection to return the value of a public property for the given object + /// + /// + /// + /// Null if property does not exist, String.Empty if property exists but it has no value, actual value otherwise + public static string GetPropertyValue(object directoryObject, string propertyName) + { + if (directoryObject == null) + { + return null; + } + + if (propertyName.StartsWith("extensionAttribute")) + { + try + { + var returnString = string.Empty; + if (directoryObject is User) + { + var userobject = (User)directoryObject; + if (userobject.AdditionalData != null) + { + var obj = userobject.AdditionalData.FirstOrDefault(s => s.Key.EndsWith(propertyName)); + if (obj.Value != null) + { + returnString = obj.Value.ToString(); + } + } + } + else if (directoryObject is Group) + { + var groupobject = (Group)directoryObject; + if (groupobject.AdditionalData != null) + { + var obj = groupobject.AdditionalData.FirstOrDefault(s => s.Key.EndsWith(propertyName)); + if (obj.Value != null) + { + returnString = obj.Value.ToString(); + } + } + } + // Never return null for an extensionAttribute since we know it exists for both User and Group + return returnString == null ? String.Empty : returnString; + } + catch + { + return String.Empty; + } + } + + PropertyInfo pi = directoryObject.GetType().GetProperty(propertyName); + if (pi == null) + { + return null; // Property does not exist, return null + } + object propertyValue = pi.GetValue(directoryObject, null); + return propertyValue == null ? String.Empty : propertyValue.ToString(); + } + + protected override void FillSchema(SPProviderSchema schema) + { + schema.AddSchemaElement(new SPSchemaElement(PeopleEditorEntityDataKeys.DisplayName, "Display Name", SPSchemaElementType.Both)); + } + + protected override void FillClaimTypes(List claimTypes) + { + if (claimTypes == null) { return; } + bool configIsValid = ValidateLocalConfiguration(null); + if (configIsValid) + { + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + + foreach (var claimTypeSettings in this.LocalSettings.RuntimeClaimTypesList) + { + claimTypes.Add(claimTypeSettings.ClaimType); + } + } + catch (Exception ex) + { + Logger.LogException(Name, "in FillClaimTypes", TraceCategory.Core, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + } + + protected override void FillClaimValueTypes(List claimValueTypes) + { + claimValueTypes.Add(WIF4_5.ClaimValueTypes.String); + } + + protected override void FillEntityTypes(List entityTypes) + { + entityTypes.Add(SPClaimEntityTypes.User); + entityTypes.Add(ClaimsProviderConstants.GroupClaimEntityType); + } + + 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 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) + { + Logger.Log($"[{Name}] Not trying to augment '{decodedEntity.Value}' because his OriginalIssuer is '{decodedEntity.OriginalIssuer}'.", + TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Augmentation); + return; + } + + if (!ValidateLocalConfiguration(context)) { return; } + + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + // There can be multiple TrustedProvider on the farm, but AzureCP should only do augmentation if current entity is from TrustedProvider it is associated with + if (!String.Equals(decodedEntity.OriginalIssuer, this.OriginalIssuerName, StringComparison.InvariantCultureIgnoreCase)) { return; } + + if (!this.LocalSettings.EnableAugmentation) { return; } + + Logger.Log($"[{Name}] Starting augmentation for user '{decodedEntity.Value}'.", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); + ClaimTypeConfig groupClaimTypeSettings = this.LocalSettings.RuntimeClaimTypesList.FirstOrDefault(x => x.EntityType == DirectoryObjectType.Group); + if (groupClaimTypeSettings == null) + { + Logger.Log($"[{Name}] No claim type with EntityType 'Group' was found, please check claims mapping table.", + TraceSeverity.High, EventSeverity.Error, TraceCategory.Augmentation); + return; + } + + OperationContext currentContext = new OperationContext(this.LocalSettings, OperationType.Augmentation, null, decodedEntity, context, null, null, Int32.MaxValue); + Stopwatch timer = new Stopwatch(); + timer.Start(); + Task> groupsTask = this.EntityProvider.GetEntityGroupsAsync(currentContext, groupClaimTypeSettings.EntityProperty); + groupsTask.Wait(); + List groups = groupsTask.Result; + timer.Stop(); + if (groups?.Count > 0) + { + foreach (string group in groups) + { + claims.Add(CreateClaim(groupClaimTypeSettings.ClaimType, group, groupClaimTypeSettings.ClaimValueType)); + Logger.Log($"[{Name}] Added group '{group}' to user '{currentContext.IncomingEntity.Value}'", + TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Augmentation); + } + Logger.Log($"[{Name}] User '{currentContext.IncomingEntity.Value}' was augmented with {groups.Count} groups in {timer.ElapsedMilliseconds} ms", + TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); + } + else + { + Logger.Log($"[{Name}] No group found for user '{currentContext.IncomingEntity.Value}', search took {timer.ElapsedMilliseconds.ToString()} ms", + TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Augmentation); + } + } + catch (Exception ex) + { + Logger.LogException(Name, "in AugmentEntity", TraceCategory.Augmentation, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + + protected virtual new SPClaim CreateClaim(string type, string value, string valueType) + { + // SPClaimProvider.CreateClaim sets property OriginalIssuer to SPOriginalIssuerType.ClaimProvider, which is not correct + //return CreateClaim(type, value, valueType); + return new SPClaim(type, value, valueType, this.OriginalIssuerName); + } + + protected override void FillHierarchy(Uri context, string[] entityTypes, string hierarchyNodeID, int numberOfLevels, SPProviderHierarchyTree hierarchy) + { + List aadEntityTypes = new List(); + if (entityTypes.Contains(SPClaimEntityTypes.User)) { aadEntityTypes.Add(DirectoryObjectType.User); } + if (entityTypes.Contains(ClaimsProviderConstants.GroupClaimEntityType)) { aadEntityTypes.Add(DirectoryObjectType.Group); } + + if (!ValidateLocalConfiguration(context)) { return; } + + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + if (hierarchyNodeID == null) + { + // Root level + foreach (var azureObject in this.LocalSettings.RuntimeClaimTypesList.FindAll(x => !x.UseMainClaimTypeOfDirectoryObject && aadEntityTypes.Contains(x.EntityType))) + { + hierarchy.AddChild( + new Microsoft.SharePoint.WebControls.SPProviderHierarchyNode( + Name, + azureObject.ClaimTypeDisplayName, + azureObject.ClaimType, + true)); + } + } + } + catch (Exception ex) + { + Logger.LogException(Name, "in FillHierarchy", TraceCategory.Claims_Picking, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + + protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List resolved) + { + if (!ValidateLocalConfiguration(context)) { return; } + + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + OperationContext currentContext = new OperationContext(this.LocalSettings, OperationType.Search, resolveInput, null, context, entityTypes, null, 30); + List entities = SearchOrValidate(currentContext); + FillEntities(currentContext, ref entities); + if (entities == null || entities.Count == 0) { return; } + foreach (PickerEntity entity in entities) + { + resolved.Add(entity); + Logger.Log($"[{Name}] Added entity: display text: '{entity.DisplayText}', claim value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'", + TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Claims_Picking); + } + Logger.Log($"[{Name}] Returned {entities.Count} entities with input '{currentContext.Input}'", TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); + } + catch (Exception ex) + { + Logger.LogException(Name, "in FillResolve(string)", TraceCategory.Claims_Picking, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + + protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List resolved) + { + if (!ValidateLocalConfiguration(context)) { return; } + + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + // Ensure incoming claim should be validated by AzureCP + // Must be made after call to Initialize because SPTrustedLoginProvider name must be known + if (!String.Equals(resolveInput.OriginalIssuer, this.OriginalIssuerName, StringComparison.InvariantCultureIgnoreCase)) { return; } + + OperationContext currentContext = new OperationContext(this.LocalSettings, OperationType.Validation, resolveInput.Value, resolveInput, context, entityTypes, null, 1); + List entities = this.SearchOrValidate(currentContext); + if (entities?.Count == 1) + { + resolved.Add(entities[0]); + Logger.Log($"[{Name}] 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; + Logger.Log($"[{Name}] 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) + { + Logger.LogException(Name, "in FillResolve(SPClaim)", TraceCategory.Claims_Picking, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + + protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree) + { + if (!ValidateLocalConfiguration(context)) { return; } + + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + OperationContext currentContext = new OperationContext(this.LocalSettings, OperationType.Search, searchPattern, null, context, entityTypes, hierarchyNodeID, maxCount); + List entities = this.SearchOrValidate(currentContext); + FillEntities(currentContext, ref entities); + if (entities == null || entities.Count == 0) { return; } + SPProviderHierarchyNode matchNode = null; + foreach (PickerEntity entity in entities) + { + // Add current PickerEntity to the corresponding ClaimType in the hierarchy + if (searchTree.HasChild(entity.Claim.ClaimType)) + { + matchNode = searchTree.Children.First(x => x.HierarchyNodeID == entity.Claim.ClaimType); + } + else + { + ClaimTypeConfig ctConfig = this.LocalSettings.RuntimeClaimTypesList.FirstOrDefault(x => + !x.UseMainClaimTypeOfDirectoryObject && + String.Equals(x.ClaimType, entity.Claim.ClaimType, StringComparison.InvariantCultureIgnoreCase)); + + string nodeName = ctConfig != null ? ctConfig.ClaimTypeDisplayName : entity.Claim.ClaimType; + matchNode = new SPProviderHierarchyNode(Name, nodeName, entity.Claim.ClaimType, true); + searchTree.AddChild(matchNode); + } + matchNode.AddEntity(entity); + Logger.Log($"[{Name}] Added entity: display text: '{entity.DisplayText}', claim value: '{entity.Claim.Value}', claim type: '{entity.Claim.ClaimType}'", + TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Claims_Picking); + } + Logger.Log($"[{Name}] Returned {entities.Count} entities from input '{currentContext.Input}'", + TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Claims_Picking); + } + catch (Exception ex) + { + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + + /// + /// Override this method to change / remove entities created by AzureCP, or add new ones + /// + /// + /// List of entities created by LDAPCP + protected virtual void FillEntities(OperationContext currentContext, ref List resolved) + { + } + + /// + /// Return the identity claim type + /// + /// + public override string GetClaimTypeForUserKey() + { + // Initialization may fail because there is no yet configuration (fresh install) + // In this case, AzureCP should not return null because it causes null exceptions in SharePoint when users sign-in + bool configIsValid = ValidateLocalConfiguration(null); + if (configIsValid) + { + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + return this.PersistedConfiguration.SPTrust.IdentityClaimTypeInformation.MappedClaimType; + } + catch (Exception ex) + { + Logger.LogException(Name, "in GetClaimTypeForUserKey", TraceCategory.Rehydration, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + } + return String.Empty; + } + + /// + /// Return the user key (SPClaim with identity claim type) from the incoming entity + /// + /// + /// + protected override SPClaim GetUserKeyForEntity(SPClaim entity) + { + // Initialization may fail because there is no yet configuration (fresh install) + // In this case, AzureCP should not return null because it causes null exceptions in SharePoint when users sign-in + bool initSucceeded = ValidateLocalConfiguration(null); + + this.Lock_LocalConfigurationRefresh.EnterReadLock(); + try + { + // If initialization failed but SPTrust is not null, rest of the method can be executed normally + // Otherwise return the entity + if (!initSucceeded && this.PersistedConfiguration?.SPTrust == null) + { + return entity; + } + + // There are 2 scenarios: + // 1: OriginalIssuer is "SecurityTokenService": Value looks like "05.t|contoso.local|yvand@contoso.local", claim type is "http://schemas.microsoft.com/sharepoint/2009/08/claims/userid" and it must be decoded properly + // 2: OriginalIssuer is AzureCP: in this case incoming entity is valid and returned as is + if (String.Equals(entity.OriginalIssuer, this.PersistedConfiguration.SPTrust.Name, StringComparison.InvariantCultureIgnoreCase)) + { + return entity; + } + + SPClaimProviderManager cpm = SPClaimProviderManager.Local; + SPClaim curUser = SPClaimProviderManager.DecodeUserIdentifierClaim(entity); + + Logger.Log($"[{Name}] Returning user key for '{entity.Value}'", + TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Rehydration); + return CreateClaim(this.PersistedConfiguration.SPTrust.IdentityClaimTypeInformation.MappedClaimType, curUser.Value, curUser.ValueType); + } + catch (Exception ex) + { + Logger.LogException(Name, "in GetUserKeyForEntity", TraceCategory.Rehydration, ex); + } + finally + { + this.Lock_LocalConfigurationRefresh.ExitReadLock(); + } + return null; + } + } + + /// + /// User / group found in Azure AD, with additional information + /// + public class ClaimsProviderEntityResult + { + /// + /// Gets the entity returned by Azure AD + /// + public DirectoryObject DirectoryEntity { get; private set; } + + /// + /// Gets the relevant ClaimTypeConfig object to use for the property PickerEntity.Claim + /// + public ClaimTypeConfig ClaimTypeConfig { get; private set; } + + /// + /// Gets the DirectoryObject's attribute value to use as the actual permission value + /// + public string PermissionValue { get; private set; } + + /// + /// Gets the DirectoryObject's attribute value which matched the query + /// + public string DirectoryObjectPropertyValue { get; private set; } + + public ClaimsProviderEntityResult(DirectoryObject directoryEntity, ClaimTypeConfig claimTypeConfig, string permissionValue, string directoryObjectPropertyValue) + { + DirectoryEntity = directoryEntity; + ClaimTypeConfig = claimTypeConfig; + PermissionValue = permissionValue; + DirectoryObjectPropertyValue = directoryObjectPropertyValue; + } + } +} diff --git a/AzureCP/Yvand.ClaimsProviders/Configuration/AzureAD/AADEntityProviderConfig.cs b/AzureCP/Yvand.ClaimsProviders/Configuration/AzureAD/AADEntityProviderConfig.cs new file mode 100644 index 00000000..29547fb8 --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/Configuration/AzureAD/AADEntityProviderConfig.cs @@ -0,0 +1,195 @@ +using Microsoft.SharePoint.Administration; +using Microsoft.SharePoint.Administration.Claims; +using Microsoft.SharePoint.WebControls; +using System; +using System.Collections.Generic; + +namespace Yvand.ClaimsProviders.Config +{ + public interface IAADSettings : IEntityProviderSettings + { + List AzureTenants { get; } + string ProxyAddress { get; } + bool FilterSecurityEnabledGroupsOnly { get; } + } + + public class AADEntityProviderSettings : EntityProviderSettings, IAADSettings + { + public List AzureTenants { get; set; } + + public string ProxyAddress { get; set; } + + public bool FilterSecurityEnabledGroupsOnly { get; set; } + + public AADEntityProviderSettings() : base() { } + + public AADEntityProviderSettings(List runtimeClaimTypesList, IEnumerable runtimeMetadataConfig, IdentityClaimTypeConfig identityClaimTypeConfig, ClaimTypeConfig mainGroupClaimTypeConfig) + : base(runtimeClaimTypesList, runtimeMetadataConfig, identityClaimTypeConfig, mainGroupClaimTypeConfig) + { + } + + /// + /// Generate and return default claim types configuration list + /// + /// + public static ClaimTypeConfigCollection ReturnDefaultClaimTypesConfig(string claimsProviderName) + { + if (String.IsNullOrWhiteSpace(claimsProviderName)) + { + throw new ArgumentNullException(nameof(claimsProviderName)); + } + + SPTrustedLoginProvider spTrust = Utils.GetSPTrustAssociatedWithClaimsProvider(claimsProviderName); + if (spTrust == null) + { + Logger.Log($"No SPTrustedLoginProvider associated with claims provider '{claimsProviderName}' was found.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); + return null; + } + + ClaimTypeConfigCollection newCTConfigCollection = new ClaimTypeConfigCollection(spTrust) + { + // Identity claim type. "Name" (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name) is a reserved claim type in SharePoint that cannot be used in the SPTrust. + //new ClaimTypeConfig{EntityType = DirectoryObjectType.User, DirectoryObjectProperty = AzureADObjectProperty.UserPrincipalName, ClaimType = WIF4_5.ClaimTypes.Upn}, + new IdentityClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.UserPrincipalName, ClaimType = spTrust.IdentityClaimTypeInformation.MappedClaimType}, + + // Additional properties to find user and create entity with the identity claim type (UseMainClaimTypeOfDirectoryObject=true) + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.DisplayName, UseMainClaimTypeOfDirectoryObject = true, EntityDataKey = PeopleEditorEntityDataKeys.DisplayName}, + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.GivenName, UseMainClaimTypeOfDirectoryObject = true}, //Yvan + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.Surname, UseMainClaimTypeOfDirectoryObject = true}, //Duhamel + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.Mail, EntityDataKey = PeopleEditorEntityDataKeys.Email, UseMainClaimTypeOfDirectoryObject = true}, + + // 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, EntityProperty = DirectoryObjectProperty.MobilePhone, EntityDataKey = PeopleEditorEntityDataKeys.MobilePhone}, + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.JobTitle, EntityDataKey = PeopleEditorEntityDataKeys.JobTitle}, + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.Department, EntityDataKey = PeopleEditorEntityDataKeys.Department}, + new ClaimTypeConfig{EntityType = DirectoryObjectType.User, EntityProperty = DirectoryObjectProperty.OfficeLocation, EntityDataKey = PeopleEditorEntityDataKeys.Location}, + + // Group + new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, EntityProperty = DirectoryObjectProperty.Id, ClaimType = ClaimsProviderConstants.DefaultMainGroupClaimType, EntityPropertyToUseAsDisplayText = DirectoryObjectProperty.DisplayName}, + new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, EntityProperty = DirectoryObjectProperty.DisplayName, UseMainClaimTypeOfDirectoryObject = true, EntityDataKey = PeopleEditorEntityDataKeys.DisplayName}, + new ClaimTypeConfig{EntityType = DirectoryObjectType.Group, EntityProperty = DirectoryObjectProperty.Mail, EntityDataKey = PeopleEditorEntityDataKeys.Email}, + }; + return newCTConfigCollection; + } + } + + public class AADEntityProviderConfig : EntityProviderConfig + where TConfiguration : IAADSettings + { + public List AzureTenants + { + get => _AzureTenants; + set => _AzureTenants = value; + } + [Persisted] + private List _AzureTenants = new List(); + + public string ProxyAddress + { + get => _ProxyAddress; + set => _ProxyAddress = value; + } + [Persisted] + private string _ProxyAddress; + + /// + /// Set if only AAD groups with securityEnabled = true should be returned - https://docs.microsoft.com/en-us/graph/api/resources/groups-overview?view=graph-rest-1.0 + /// + public bool FilterSecurityEnabledGroupsOnly + { + get => _FilterSecurityEnabledGroupsOnly; + set => _FilterSecurityEnabledGroupsOnly = value; + } + [Persisted] + private bool _FilterSecurityEnabledGroupsOnly = false; + + public AADEntityProviderConfig() : base() { } + public AADEntityProviderConfig(string configurationName, SPPersistedObject parent, string claimsProviderName) : base(configurationName, parent, claimsProviderName) + { + } + + public override bool InitializeDefaultSettings() + { + return base.InitializeDefaultSettings(); + } + + protected override bool InitializeInternalRuntimeSettings() + { + bool success = base.InitializeInternalRuntimeSettings(); + foreach (var tenant in this.AzureTenants) + { + tenant.InitializeAuthentication(this.Timeout, this.ProxyAddress); + } + + return success; + } + + protected override TConfiguration GenerateLocalSettings() + { + IAADSettings entityProviderSettings = new AADEntityProviderSettings( + this.RuntimeClaimTypesList, + this.RuntimeMetadataConfig, + this.IdentityClaimTypeConfig, + this.MainGroupClaimTypeConfig) + { + AlwaysResolveUserInput = this.AlwaysResolveUserInput, + ClaimTypes = this.ClaimTypes, + CustomData = this.CustomData, + EnableAugmentation = this.EnableAugmentation, + EntityDisplayTextPrefix = this.EntityDisplayTextPrefix, + FilterExactMatchOnly = this.FilterExactMatchOnly, + Name = this.Name, + Timeout = this.Timeout, + Version = this.Version, + + // Properties specific to type IAADSettings + AzureTenants = this.AzureTenants, + ProxyAddress = this.ProxyAddress, + FilterSecurityEnabledGroupsOnly = this.FilterSecurityEnabledGroupsOnly, + }; + return (TConfiguration)entityProviderSettings; + + //TSettings baseEntityProviderSettings = base.GenerateLocalSettings(); + //AADEntityProviderSettings entityProviderSettings = baseEntityProviderSettings as AADEntityProviderSettings; + //entityProviderSettings.AzureTenants = this.AzureTenants; + //entityProviderSettings.ProxyAddress = this.ProxyAddress; + //entityProviderSettings.FilterSecurityEnabledGroupsOnly = this.FilterSecurityEnabledGroupsOnly; + //return (TSettings)(IAADSettings)entityProviderSettings; + } + + public override void ApplySettings(TConfiguration configuration, bool commitIfValid) + { + // Properties specific to type IAADSettings + this.AzureTenants = configuration.AzureTenants; + this.FilterSecurityEnabledGroupsOnly = configuration.FilterSecurityEnabledGroupsOnly; + this.ProxyAddress = configuration.ProxyAddress; + + base.ApplySettings(configuration, commitIfValid); + } + + /// + /// Generate and return default configuration + /// + /// + public static AADEntityProviderConfig ReturnDefaultConfiguration(string claimsProviderName) + { + AADEntityProviderConfig defaultConfig = new AADEntityProviderConfig(); + defaultConfig.ClaimsProviderName = claimsProviderName; + defaultConfig.ClaimTypes = AADEntityProviderSettings.ReturnDefaultClaimTypesConfig(claimsProviderName); + return defaultConfig; + } + + public override ClaimTypeConfigCollection ReturnDefaultClaimTypesConfig() + { + return AADEntityProviderSettings.ReturnDefaultClaimTypesConfig(this.ClaimsProviderName); + } + + public void ResetClaimTypesList() + { + ClaimTypes.Clear(); + ClaimTypes = AADEntityProviderSettings.ReturnDefaultClaimTypesConfig(this.ClaimsProviderName); + Logger.Log($"Claim types list of configuration '{Name}' was successfully reset to default configuration", + TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); + } + } +} diff --git a/AzureCP/Yvand.ClaimsProviders/Configuration/AzureAD/AzureTenant.cs b/AzureCP/Yvand.ClaimsProviders/Configuration/AzureAD/AzureTenant.cs new file mode 100644 index 00000000..e8facd80 --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/Configuration/AzureAD/AzureTenant.cs @@ -0,0 +1,321 @@ +using Azure.Core; +using Azure.Core.Pipeline; +using Azure.Identity; +using Microsoft.Graph; +using Microsoft.Identity.Client; +using Microsoft.Kiota.Http.HttpClientLibrary.Middleware; +using Microsoft.SharePoint.Administration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Yvand.ClaimsProviders.Config +{ + public class AzureTenant : SPAutoSerializingObject + { + public Guid Identifier + { + get => _Identifier; + set => _Identifier = value; + } + [Persisted] + private Guid _Identifier = Guid.NewGuid(); + + /// + /// Tenant Name or ID + /// + public string Name + { + get => _Name; + set => _Name = value; + } + [Persisted] + private string _Name; + + /// + /// Application (client) ID of the app registration created for AzureCP in the Azure AD tenant + /// + public string ClientId + { + get => _ClientId; + set => _ClientId = value; + } + [Persisted] + private string _ClientId; + + /// + /// Client secret of the app registration created for AzureCP in the Azure AD tenant + /// + public string ClientSecret + { + get => _ClientSecret; + set => _ClientSecret = value; + } + [Persisted] + private string _ClientSecret; + + public bool ExcludeMemberUsers + { + get => _ExcludeMemberUsers; + set => _ExcludeMemberUsers = value; + } + [Persisted] + private bool _ExcludeMemberUsers = false; + + public bool ExcludeGuestUsers + { + get => _ExcludeGuestUsers; + set => _ExcludeGuestUsers = value; + } + [Persisted] + private bool _ExcludeGuestUsers = false; + + /// + /// Client ID of AD Connect used in extension attribues + /// + [Persisted] + private Guid _ExtensionAttributesApplicationId; + + public Guid ExtensionAttributesApplicationId + { + get => _ExtensionAttributesApplicationId; + set => _ExtensionAttributesApplicationId = value; + } + + /// + /// Gets or set the client certificate with its private key, configured in the app registration for AzureCP + /// + public X509Certificate2 ClientCertificateWithPrivateKey + { + get + { + return _ClientCertificateWithPrivateKey; + } + set + { + if (value == null) { return; } + if (!value.HasPrivateKey) { throw new ArgumentException("The certificate cannot be imported because it does not have a private key"); } + _ClientCertificateWithPrivateKey = value; + try + { + // https://stackoverflow.com/questions/32354790/how-to-check-is-x509certificate2-exportable-or-not + _ClientCertificateWithPrivateKeyRawData = value.Export(X509ContentType.Pfx, ClaimsProviderConstants.ClientCertificatePrivateKeyPassword); + } + catch (CryptographicException ex) + { + // X509Certificate2.Export() is expected to fail if the private key is not exportable, which depends on the X509KeyStorageFlags used when creating the X509Certificate2 object + //ClaimsProviderLogging.LogException(AzureCP._ProviderInternalName, $"while setting the certificate for tenant '{this.Name}'. Is the private key of the certificate exportable?", TraceCategory.Core, ex); + } + } + } + private X509Certificate2 _ClientCertificateWithPrivateKey; + [Persisted] + private byte[] _ClientCertificateWithPrivateKeyRawData; + + public Uri AzureAuthority + { + get => new Uri(this._AzureAuthority); + set => _AzureAuthority = value.ToString(); + } + [Persisted] + private string _AzureAuthority = AzureAuthorityHosts.AzurePublicCloud.ToString(); + public AzureCloudInstance CloudInstance + { + get + { + if (AzureAuthority == null) { return AzureCloudInstance.AzurePublic; } + KeyValuePair kvp = ClaimsProviderConstants.AzureCloudEndpoints.FirstOrDefault(item => item.Value.Equals(this.AzureAuthority)); + return kvp.Equals(default(KeyValuePair)) ? AzureCloudInstance.AzurePublic : kvp.Key; + } + } + + public string AuthenticationMode + { + get + { + return String.IsNullOrWhiteSpace(this.ClientSecret) ? "Client certificate" : "Client secret"; + } + } + + public GraphServiceClient GraphService { get; private set; } + public string UserFilter { get; set; } + public string GroupFilter { get; set; } + public string[] UserSelect { get; set; } + public string[] GroupSelect { get; set; } + + public AzureTenant() { } + + protected override void OnDeserialization() + { + if (_ClientCertificateWithPrivateKeyRawData != null) + { + try + { + // Sets the local X509Certificate2 object from the persisted raw data stored in the configuration database + // EphemeralKeySet: Keep the private key in-memory, it won't be written to disk - https://www.pkisolutions.com/handling-x509keystorageflags-in-applications/ + _ClientCertificateWithPrivateKey = ImportPfxCertificateBlob(_ClientCertificateWithPrivateKeyRawData, ClaimsProviderConstants.ClientCertificatePrivateKeyPassword, X509KeyStorageFlags.EphemeralKeySet); + } + catch (CryptographicException ex) + { + // It may fail with CryptographicException: The system cannot find the file specified, but it does not have any impact + Logger.LogException(AzureCP.ClaimsProviderName, $"while deserializating the certificate for tenant '{this.Name}'.", TraceCategory.Core, ex); + } + } + } + + /// + /// Initializes the authentication to Microsoft Graph + /// + public void InitializeAuthentication(int timeout, string proxyAddress) + { + if (String.IsNullOrWhiteSpace(this.ClientSecret) && this.ClientCertificateWithPrivateKey == null) + { + Logger.Log($"[{AzureCP.ClaimsProviderName}] Cannot initialize authentication for tenant {this.Name} because both properties {nameof(ClientSecret)} and {nameof(ClientCertificateWithPrivateKey)} are not set.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); + return; + } + if (String.IsNullOrWhiteSpace(this.ClientId)) + { + Logger.Log($"[{AzureCP.ClaimsProviderName}] Cannot initialize authentication for tenant {this.Name} because the property {nameof(ClientId)} is not set.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); + return; + } + if (String.IsNullOrWhiteSpace(this.Name)) + { + Logger.Log($"[{AzureCP.ClaimsProviderName}] Cannot initialize authentication because the property {nameof(Name)} of current tenant is not set.", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Configuration); + return; + } + + //try + //{ + WebProxy webProxy = null; + HttpClientTransport clientTransportProxy = null; + if (!String.IsNullOrWhiteSpace(proxyAddress)) + { + webProxy = new WebProxy(new Uri(proxyAddress)); + HttpClientHandler clientProxy = new HttpClientHandler { Proxy = webProxy }; + clientTransportProxy = new HttpClientTransport(clientProxy); + } + + var handlers = GraphClientFactory.CreateDefaultHandlers(); +#if DEBUG + handlers.Add(new ChaosHandler()); +#endif + + ClientSecretCredentialOptions options = new ClientSecretCredentialOptions(); + options.AuthorityHost = this.AzureAuthority; + if (clientTransportProxy != null) { options.Transport = clientTransportProxy; } + + TokenCredential tokenCredential; + if (!String.IsNullOrWhiteSpace(this.ClientSecret)) + { + tokenCredential = new ClientSecretCredential(this.Name, this.ClientId, this.ClientSecret, options); + } + else + { + tokenCredential = new ClientCertificateCredential(this.Name, this.ClientId, this.ClientCertificateWithPrivateKey, options); + } + + var scopes = new[] { "https://graph.microsoft.com/.default" }; + HttpClient httpClient = GraphClientFactory.Create(handlers: handlers, proxy: webProxy); + if (timeout > 0 && timeout < Int32.MaxValue) + { + httpClient.Timeout = TimeSpan.FromMilliseconds(timeout); + } + + // https://learn.microsoft.com/en-us/graph/sdks/customize-client?tabs=csharp + var authProvider = new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider( + credential: tokenCredential, + scopes: new[] { "https://graph.microsoft.com/.default", + }); + this.GraphService = new GraphServiceClient(httpClient, authProvider); + //} + //catch (Exception ex) + //{ + // ClaimsProviderLogging.LogException(AzureCPSE.ClaimsProviderName, $"while setting client context for tenant '{this.Name}'.", TraceCategory.Core, ex); + //} + } + + /// + /// Returns a copy of the current object. This copy does not have any member of the base SharePoint base class set + /// + /// + public AzureTenant CopyConfiguration() + { + AzureTenant copy = new AzureTenant(); + copy = (AzureTenant)Utils.CopyPersistedFields(typeof(AzureTenant), this, copy); + return copy; + } + + public AzureTenant CopyPublicProperties() + { + AzureTenant copy = new AzureTenant(); + // 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; + } + + /// + /// Sets the credentials used to connect to the Azure AD tenant + /// + /// Application (client) ID + /// Application (client) secret + public void SetCredentials(string clientId, string clientSecret) + { + this.ClientId = clientId; + this.ClientSecret = clientSecret; + this.ClientCertificateWithPrivateKey = null; + } + + /// + /// Sets the credentials used to connect to the Azure AD tenant + /// + /// Application (client) secret + /// Client certificate with its private key + public void SetCredentials(string clientId, X509Certificate2 clientCertificateWithPrivateKey) + { + this.ClientId = clientId; + this.ClientSecret = String.Empty; + this.ClientCertificateWithPrivateKey = clientCertificateWithPrivateKey; + } + + /// + /// Imports the input blob into a pfx X509Certificate2 object with its private key + /// + /// + /// + /// + /// + public static X509Certificate2 ImportPfxCertificateBlob(byte[] blob, string certificatePassword, X509KeyStorageFlags keyStorageFlags) + { + if (X509Certificate2.GetCertContentType(blob) != X509ContentType.Pfx) + { + return null; + } + + if (String.IsNullOrWhiteSpace(certificatePassword)) + { + // If passwordless, import the private key as documented in https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b + return new X509Certificate2(blob, (string)null, keyStorageFlags); + } + else + { + return new X509Certificate2(blob, certificatePassword, keyStorageFlags); + } + } + } +} diff --git a/AzureCP/ClaimTypeConfig.cs b/AzureCP/Yvand.ClaimsProviders/Configuration/ClaimTypeConfig.cs similarity index 81% rename from AzureCP/ClaimTypeConfig.cs rename to AzureCP/Yvand.ClaimsProviders/Configuration/ClaimTypeConfig.cs index 345c9828..9f675936 100644 --- a/AzureCP/ClaimTypeConfig.cs +++ b/AzureCP/Yvand.ClaimsProviders/Configuration/ClaimTypeConfig.cs @@ -6,19 +6,24 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reflection; -using WIF = System.Security.Claims; +using WIF4_5 = System.Security.Claims; -namespace azurecp +namespace Yvand.ClaimsProviders.Config { public class IdentityClaimTypeConfig : ClaimTypeConfig { - public AzureADObjectProperty DirectoryObjectPropertyForGuestUsers + public DirectoryObjectProperty DirectoryObjectPropertyForGuestUsers { - get { return (AzureADObjectProperty)Enum.ToObject(typeof(AzureADObjectProperty), _DirectoryObjectPropertyForGuestUsers); } - set { _DirectoryObjectPropertyForGuestUsers = (int)value; } + get + { + return String.IsNullOrWhiteSpace(_DirectoryObjectPropertyForGuestUsers) ? + DirectoryObjectProperty.NotSet : + (DirectoryObjectProperty)Enum.Parse(typeof(DirectoryObjectProperty), _DirectoryObjectPropertyForGuestUsers); + } + set { _DirectoryObjectPropertyForGuestUsers = value.ToString(); } } [Persisted] - private int _DirectoryObjectPropertyForGuestUsers = (int)AzureADObjectProperty.Mail; + private string _DirectoryObjectPropertyForGuestUsers = DirectoryObjectProperty.Mail.ToString(); public IdentityClaimTypeConfig() { @@ -35,8 +40,8 @@ public static IdentityClaimTypeConfig ConvertClaimTypeConfig(ClaimTypeConfig ctC identityCTConfig.ClaimType = ctConfig.ClaimType; identityCTConfig.ClaimTypeDisplayName = ctConfig.ClaimTypeDisplayName; identityCTConfig.ClaimValueType = ctConfig.ClaimValueType; - identityCTConfig.DirectoryObjectProperty = ctConfig.DirectoryObjectProperty; - identityCTConfig.DirectoryObjectPropertyToShowAsDisplayText = ctConfig.DirectoryObjectPropertyToShowAsDisplayText; + identityCTConfig.EntityProperty = ctConfig.EntityProperty; + identityCTConfig.EntityPropertyToUseAsDisplayText = ctConfig.EntityPropertyToUseAsDisplayText; identityCTConfig.EntityDataKey = ctConfig.EntityDataKey; identityCTConfig.EntityType = ctConfig.EntityType; identityCTConfig.FilterExactMatchOnly = ctConfig.FilterExactMatchOnly; @@ -54,24 +59,33 @@ public class ClaimTypeConfig : SPAutoSerializingObject, IEquatable /// Azure AD attribute mapped to the claim type /// - public AzureADObjectProperty DirectoryObjectProperty + public DirectoryObjectProperty EntityProperty { - get { return (AzureADObjectProperty)Enum.ToObject(typeof(AzureADObjectProperty), _DirectoryObjectProperty); } - set { _DirectoryObjectProperty = (int)value; } + get { + return String.IsNullOrWhiteSpace(_EntityProperty) ? + DirectoryObjectProperty.NotSet : + (DirectoryObjectProperty)Enum.Parse(typeof(DirectoryObjectProperty), _EntityProperty); + } + set { _EntityProperty = value.ToString(); } } [Persisted] - private int _DirectoryObjectProperty; + private string _EntityProperty; public DirectoryObjectType EntityType { - get { return (DirectoryObjectType)Enum.ToObject(typeof(DirectoryObjectType), _DirectoryObjectType); } - set { _DirectoryObjectType = (int)value; } + get + { + return String.IsNullOrWhiteSpace(_EntityType) ? + DirectoryObjectType.User : + (DirectoryObjectType)Enum.Parse(typeof(DirectoryObjectType), _EntityType); + } + set { _EntityType = value.ToString(); } } [Persisted] - private int _DirectoryObjectType; + private string _EntityType; /// - /// Set if this will create a User or a Group permission. Values allowed are "User" or "FormsRole" + /// Gets or sets this property to define if the entity created is a User (SPClaimEntityTypes.User) or a Group (SPClaimEntityTypes.FormsRole). Accepted values are "User" or "FormsRole" /// public string SharePointEntityType { @@ -99,7 +113,7 @@ internal bool SupportsWildcard { get { - if (this.DirectoryObjectProperty == AzureADObjectProperty.Id) + if (this.EntityProperty == DirectoryObjectProperty.Id) { return false; } @@ -115,11 +129,11 @@ internal bool SupportsWildcard /// public bool UseMainClaimTypeOfDirectoryObject { - get { return _CreateAsIdentityClaim; } - set { _CreateAsIdentityClaim = value; } + get { return _UseMainClaimTypeOfDirectoryObject; } + set { _UseMainClaimTypeOfDirectoryObject = value; } } [Persisted] - private bool _CreateAsIdentityClaim = false; + private bool _UseMainClaimTypeOfDirectoryObject = false; /// /// Can contain a member of class PeopleEditorEntityDataKey http://msdn.microsoft.com/en-us/library/office/microsoft.sharepoint.webcontrols.peopleeditorentitydatakeys_members(v=office.15).aspx @@ -153,9 +167,7 @@ public string ClaimValueType set { _ClaimValueType = value; } } [Persisted] - private string _ClaimValueType = WIF.ClaimValueTypes.String; - - + private string _ClaimValueType = WIF4_5.ClaimValueTypes.String; /// /// If set, its value can be used as a prefix in the people picker to create a permission without actually quyerying Azure AD @@ -168,16 +180,21 @@ public string PrefixToBypassLookup [Persisted] private string _PrefixToBypassLookup; - public AzureADObjectProperty DirectoryObjectPropertyToShowAsDisplayText + public DirectoryObjectProperty EntityPropertyToUseAsDisplayText { - get { return (AzureADObjectProperty)Enum.ToObject(typeof(AzureADObjectProperty), _DirectoryObjectPropertyToShowAsDisplayText); } - set { _DirectoryObjectPropertyToShowAsDisplayText = (int)value; } + get + { + return String.IsNullOrWhiteSpace(_EntityPropertyToUseAsDisplayText) ? + DirectoryObjectProperty.NotSet : + (DirectoryObjectProperty)Enum.Parse(typeof(DirectoryObjectProperty), _EntityPropertyToUseAsDisplayText); + } + set { _EntityPropertyToUseAsDisplayText = value.ToString(); } } [Persisted] - private int _DirectoryObjectPropertyToShowAsDisplayText; + private string _EntityPropertyToUseAsDisplayText; /// - /// Set to only return values that exactly match the input + /// Gets or sets a Boolean value specifying whether claims provider should only return values that match exactly the input /// public bool FilterExactMatchOnly { @@ -197,24 +214,40 @@ public ClaimTypeConfig CopyConfiguration() if (this is IdentityClaimTypeConfig) { copy = new IdentityClaimTypeConfig(); - FieldInfo[] fieldsToCopyFromInheritedClass = typeof(IdentityClaimTypeConfig).GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (FieldInfo field in fieldsToCopyFromInheritedClass) - { - field.SetValue(copy, field.GetValue(this)); - } + copy = (ClaimTypeConfig)Utils.CopyPersistedFields(typeof(ClaimTypeConfig), this, copy); + copy = (IdentityClaimTypeConfig)Utils.CopyPersistedFields(typeof(IdentityClaimTypeConfig), this, copy); } else { copy = new ClaimTypeConfig(); - } - - // Copy non-inherited private fields - FieldInfo[] fieldsToCopy = typeof(ClaimTypeConfig).GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (FieldInfo field in fieldsToCopy) - { - field.SetValue(copy, field.GetValue(this)); + copy = (ClaimTypeConfig)Utils.CopyPersistedFields(typeof(ClaimTypeConfig), this, copy); } return copy; + + + + //ClaimTypeConfig copy; + //if (this is IdentityClaimTypeConfig) + //{ + // copy = new IdentityClaimTypeConfig(); + // FieldInfo[] fieldsToCopyFromInheritedClass = typeof(IdentityClaimTypeConfig).GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + // foreach (FieldInfo field in fieldsToCopyFromInheritedClass) + // { + // field.SetValue(copy, field.GetValue(this)); + // } + //} + //else + //{ + // copy = new ClaimTypeConfig(); + //} + + //// Copy non-inherited private fields + //FieldInfo[] fieldsToCopy = typeof(ClaimTypeConfig).GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + //foreach (FieldInfo field in fieldsToCopy) + //{ + // field.SetValue(copy, field.GetValue(this)); + //} + //return copy; } /// @@ -265,30 +298,38 @@ public int Count get { // If innerCol is null, it means that collection is not correctly set in the persisted object, very likely because it was migrated from a previons version of AzureCP - // But this may not be the right place to handle this here: this should be handled in upper layer to clean the persisted object - //if (innerCol == null) - //{ - // ClaimTypeConfigCollection newConfig = AzureCPConfig.ReturnDefaultClaimTypesConfig(); - // this.innerCol = newConfig.innerCol; - //} + if (innerCol == null) + { + return 0; + } return innerCol.Count; } } public bool IsReadOnly => false; + public IdentityClaimTypeConfig IdentityClaim + { + get + { + IdentityClaimTypeConfig ctConfig = (IdentityClaimTypeConfig)innerCol.FirstOrDefault(x => x is IdentityClaimTypeConfig); + return ctConfig; + } + set + { + IdentityClaimTypeConfig ctConfig = (IdentityClaimTypeConfig)innerCol.FirstOrDefault(x => x is IdentityClaimTypeConfig); + ctConfig = value; + } + } + /// /// If set, more checks can be done when collection is changed /// - public SPTrustedLoginProvider SPTrust - { - get => _SPTrust; - set => _SPTrust = value; - } - private SPTrustedLoginProvider _SPTrust; + public SPTrustedLoginProvider SPTrust { get; private set; } - public ClaimTypeConfigCollection() + public ClaimTypeConfigCollection(SPTrustedLoginProvider spTrust) { + this.SPTrust = spTrust; } internal ClaimTypeConfigCollection(ref Collection innerCol) @@ -309,7 +350,7 @@ public void Add(ClaimTypeConfig item) internal void Add(ClaimTypeConfig item, bool strictChecks) { - if (item.DirectoryObjectProperty == AzureADObjectProperty.NotSet) + if (item.EntityProperty == DirectoryObjectProperty.NotSet) { throw new InvalidOperationException($"Property DirectoryObjectProperty is required"); } @@ -341,14 +382,14 @@ internal void Add(ClaimTypeConfig item, bool strictChecks) if (Contains(item, new ClaimTypeConfigSameDirectoryConfiguration())) { - throw new InvalidOperationException($"An item with property '{item.DirectoryObjectProperty}' already exists for the object type '{item.EntityType}'"); + throw new InvalidOperationException($"An item with property '{item.EntityProperty}' already exists for the object type '{item.EntityType}'"); } if (Contains(item)) { if (String.IsNullOrEmpty(item.ClaimType)) { - throw new InvalidOperationException($"This configuration with DirectoryObjectProperty '{item.DirectoryObjectProperty}' and EntityType '{item.EntityType}' already exists in the collection"); + throw new InvalidOperationException($"This configuration with DirectoryObjectProperty '{item.EntityProperty}' and EntityType '{item.EntityType}' already exists in the collection"); } else { @@ -397,8 +438,8 @@ internal void Add(ClaimTypeConfig item, bool strictChecks) /// 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 (String.IsNullOrEmpty(oldClaimType)) { throw new ArgumentNullException(nameof(oldClaimType)); } + if (newItem == null) { throw new ArgumentNullException(nameof(newItem)); } // If SPTrustedLoginProvider is set, additional checks can be done if (SPTrust != null) @@ -421,7 +462,7 @@ public void Update(string oldClaimType, ClaimTypeConfig newItem) } // Create a temp collection that is a copy of current collection - ClaimTypeConfigCollection testUpdateCollection = new ClaimTypeConfigCollection(); + ClaimTypeConfigCollection testUpdateCollection = new ClaimTypeConfigCollection(this.SPTrust); foreach (ClaimTypeConfig curCTConfig in innerCol) { testUpdateCollection.Add(curCTConfig.CopyConfiguration(), false); @@ -432,7 +473,7 @@ public void Update(string oldClaimType, ClaimTypeConfig newItem) ctConfigToUpdate.ApplyConfiguration(newItem); // Test change in testUpdateCollection by adding all items in a new temp collection - ClaimTypeConfigCollection testNewItemCollection = new ClaimTypeConfigCollection(); + ClaimTypeConfigCollection testNewItemCollection = new ClaimTypeConfigCollection(this.SPTrust); foreach (ClaimTypeConfig curCTConfig in testUpdateCollection) { // ClaimTypeConfigCollection.Add() may thrown an exception if newItem is not valid for any reason @@ -448,9 +489,9 @@ public void Update(string oldClaimType, ClaimTypeConfig newItem) /// /// new DirectoryObjectProperty /// True if the identity ClaimTypeConfig was successfully updated - public bool UpdateUserIdentifier(AzureADObjectProperty newIdentifier) + public bool UpdateUserIdentifier(DirectoryObjectProperty newIdentifier) { - if (newIdentifier == AzureADObjectProperty.NotSet) { throw new ArgumentNullException("newIdentifier"); } + if (newIdentifier == DirectoryObjectProperty.NotSet) { throw new ArgumentNullException(nameof(newIdentifier)); } bool identifierUpdated = false; IdentityClaimTypeConfig identityClaimType = innerCol.FirstOrDefault(x => x is IdentityClaimTypeConfig) as IdentityClaimTypeConfig; @@ -459,7 +500,7 @@ public bool UpdateUserIdentifier(AzureADObjectProperty newIdentifier) return identifierUpdated; } - if (identityClaimType.DirectoryObjectProperty == newIdentifier) + if (identityClaimType.EntityProperty == newIdentifier) { return identifierUpdated; } @@ -469,14 +510,14 @@ public bool UpdateUserIdentifier(AzureADObjectProperty newIdentifier) { ClaimTypeConfig curCT = (ClaimTypeConfig)innerCol[i]; if (curCT.EntityType == DirectoryObjectType.User && - curCT.DirectoryObjectProperty == newIdentifier) + curCT.EntityProperty == newIdentifier) { innerCol.RemoveAt(i); break; // There can be only 1 potential duplicate } } - identityClaimType.DirectoryObjectProperty = newIdentifier; + identityClaimType.EntityProperty = newIdentifier; identifierUpdated = true; return identifierUpdated; } @@ -486,9 +527,9 @@ public bool UpdateUserIdentifier(AzureADObjectProperty newIdentifier) /// /// new DirectoryObjectPropertyForGuestUsers /// - public bool UpdateIdentifierForGuestUsers(AzureADObjectProperty newIdentifier) + public bool UpdateIdentifierForGuestUsers(DirectoryObjectProperty newIdentifier) { - if (newIdentifier == AzureADObjectProperty.NotSet) { throw new ArgumentNullException("newIdentifier"); } + if (newIdentifier == DirectoryObjectProperty.NotSet) { throw new ArgumentNullException(nameof(newIdentifier)); } bool identifierUpdated = false; IdentityClaimTypeConfig identityClaimType = innerCol.FirstOrDefault(x => x is IdentityClaimTypeConfig) as IdentityClaimTypeConfig; @@ -545,7 +586,7 @@ public bool Contains(ClaimTypeConfig item, EqualityComparer com public void CopyTo(ClaimTypeConfig[] array, int arrayIndex) { - if (array == null) { throw new ArgumentNullException("The array cannot be null."); } + if (array == null) { throw new ArgumentNullException(nameof(array)); } 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."); } @@ -580,7 +621,7 @@ public bool Remove(string claimType) { if (String.IsNullOrEmpty(claimType)) { - throw new ArgumentNullException("claimType"); + throw new ArgumentNullException(nameof(claimType)); } if (SPTrust != null && String.Equals(claimType, SPTrust.IdentityClaimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase)) { @@ -612,7 +653,7 @@ IEnumerator IEnumerable.GetEnumerator() public ClaimTypeConfig GetByClaimType(string claimType) { - if (String.IsNullOrEmpty(claimType)) { throw new ArgumentNullException("claimType"); } + if (String.IsNullOrEmpty(claimType)) { throw new ArgumentNullException(nameof(claimType)); } ClaimTypeConfig ctConfig = innerCol.FirstOrDefault(x => String.Equals(claimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase)); return ctConfig; } @@ -674,7 +715,7 @@ public class ClaimTypeConfigSameConfig : EqualityComparer public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig newCTConfig) { if (String.Equals(existingCTConfig.ClaimType, newCTConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase) && - existingCTConfig.DirectoryObjectProperty == newCTConfig.DirectoryObjectProperty && + existingCTConfig.EntityProperty == newCTConfig.EntityProperty && existingCTConfig.EntityType == newCTConfig.EntityType) { return true; @@ -687,7 +728,7 @@ public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig ne public override int GetHashCode(ClaimTypeConfig ct) { - string hCode = ct.ClaimType + ct.DirectoryObjectProperty + ct.EntityType; + string hCode = ct.ClaimType + ct.EntityProperty + ct.EntityType; return hCode.GetHashCode(); } } @@ -712,7 +753,7 @@ public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig ne public override int GetHashCode(ClaimTypeConfig ct) { - string hCode = ct.ClaimType + ct.DirectoryObjectProperty + ct.EntityType; + string hCode = ct.ClaimType + ct.EntityProperty + ct.EntityType; return hCode.GetHashCode(); } } @@ -738,7 +779,7 @@ public override bool Equals(ClaimTypeConfig existingCTConfig, ClaimTypeConfig ne public override int GetHashCode(ClaimTypeConfig ct) { - string hCode = ct.ClaimType + ct.DirectoryObjectProperty + ct.EntityType; + string hCode = ct.ClaimType + ct.EntityProperty + ct.EntityType; return hCode.GetHashCode(); } } @@ -802,7 +843,7 @@ public class ClaimTypeConfigSameDirectoryConfiguration : EqualityComparer "4EA86A04-7030-4853-BF97-F778DE32A274"; + public static string CONFIGURATION_NAME => "AzureCPSEConfig"; + /// + /// List of Microsoft Graph service root endpoints based on National Cloud as described on https://docs.microsoft.com/en-us/graph/deployments + /// + public static List> AzureCloudEndpoints => new List>() + { + new KeyValuePair(AzureCloudInstance.AzurePublic, AzureAuthorityHosts.AzurePublicCloud), + new KeyValuePair(AzureCloudInstance.AzureChina, AzureAuthorityHosts.AzureChina), + new KeyValuePair(AzureCloudInstance.AzureGermany, AzureAuthorityHosts.AzureGermany), + new KeyValuePair(AzureCloudInstance.AzureUsGovernment, AzureAuthorityHosts.AzureGovernment), + }; + public static string GroupClaimEntityType { get; set; } = SPClaimEntityTypes.FormsRole; + public static bool EnforceOnly1ClaimTypeForGroup => true; // In AzureCP, only 1 claim type can be used to create group permissions + public static string DefaultMainGroupClaimType => WIF4_5.ClaimTypes.Role; + public static string PUBLICSITEURL => "https://azurecp.yvand.net/"; + public static string GUEST_USERTYPE => "Guest"; + public static string MEMBER_USERTYPE => "Member"; + public static string ClientCertificatePrivateKeyPassword => "YVANDwRrEHVHQ57ge?uda"; + private static object Lock_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 (Lock_SetClaimsProviderVersion) + { + if (!String.IsNullOrEmpty(_ClaimsProviderVersion)) + { + return _ClaimsProviderVersion; + } + + try + { + _ClaimsProviderVersion = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(AzureCP)).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; + } + } + } + +#if DEBUG + public static int DEFAULT_TIMEOUT => 10000; +#else + public static int DEFAULT_TIMEOUT => 4000; // 4 secs +#endif + } + + public enum DirectoryObjectProperty + { + NotSet, + AccountEnabled, + Department, + DisplayName, + GivenName, + Id, + JobTitle, + Mail, + MobilePhone, + OfficeLocation, + Surname, + UserPrincipalName, + UserType, + // https://github.com/Yvand/AzureCP/issues/77: Include all other String properties of class User - https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties + AgeGroup, + City, + CompanyName, + ConsentProvidedForMinor, + Country, + EmployeeId, + FaxNumber, + LegalAgeGroupClassification, + MailNickname, + OnPremisesDistinguishedName, + OnPremisesImmutableId, + OnPremisesSecurityIdentifier, + OnPremisesDomainName, + OnPremisesSamAccountName, + OnPremisesUserPrincipalName, + PasswordPolicies, + PostalCode, + PreferredLanguage, + State, + StreetAddress, + UsageLocation, + AboutMe, + MySite, + PreferredName, + ODataType, + extensionAttribute1, + extensionAttribute2, + extensionAttribute3, + extensionAttribute4, + extensionAttribute5, + extensionAttribute6, + extensionAttribute7, + extensionAttribute8, + extensionAttribute9, + extensionAttribute10, + extensionAttribute11, + extensionAttribute12, + extensionAttribute13, + extensionAttribute14, + extensionAttribute15 + } + + public enum DirectoryObjectType + { + User, + Group + } + + public enum OperationType + { + Search, + Validation, + Augmentation, + } + + /// + /// Contains information about current operation + /// + public class OperationContext + { + public IAADSettings Configuration { get; private set; } + /// + /// Indicates what kind of operation SharePoint is requesting + /// + public OperationType OperationType { get; private set; } + + /// + /// Set only if request is a validation or an augmentation, to the incoming entity provided by SharePoint + /// + public SPClaim IncomingEntity { get; private set; } + + /// + /// User submitting the query in the poeple picker, retrieved from HttpContext. Can be null + /// + public SPClaim UserInHttpContext { get; private set; } + + /// + /// Uri provided by SharePoint + /// + public Uri UriContext { get; private set; } + + /// + /// EntityTypes expected by SharePoint in the entities returned + /// + public DirectoryObjectType[] DirectoryObjectTypes { get; private set; } + + public string HierarchyNodeID { get; private set; } + public int MaxCount { get; } + + /// + /// If request is a validation: contains the value of the SPClaim. If request is a search: contains the input + /// + public string Input { get; private set; } + + public bool InputHasKeyword { get; private set; } + + /// + /// Indicates if search operation should return only results that exactly match the Input + /// + public bool ExactSearch { get; private set; } + + /// + /// 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; private set; } + + /// + /// 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; private set; } + + public List AzureTenants { get; private set; } + + public OperationContext(IAADSettings currentConfiguration, OperationType currentRequestType, string input, SPClaim incomingEntity, Uri context, string[] entityTypes, string hierarchyNodeID, int maxCount) + { + this.Configuration = currentConfiguration; + + this.OperationType = currentRequestType; + this.Input = input; + this.IncomingEntity = incomingEntity; + this.UriContext = context; + this.HierarchyNodeID = hierarchyNodeID; + this.MaxCount = maxCount; + + // currentConfiguration.AzureTenants must be cloned locally to ensure its properties ($select / $filter) won't be updated by multiple threads + this.AzureTenants = new List(currentConfiguration.AzureTenants.Count); + foreach (AzureTenant tenant in currentConfiguration.AzureTenants) + { + AzureTenants.Add(tenant.CopyPublicProperties()); + } + + 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(currentConfiguration.RuntimeClaimTypesList); + } + else if (currentRequestType == OperationType.Search) + { + this.InitializeSearch(currentConfiguration.RuntimeClaimTypesList, currentConfiguration.FilterExactMatchOnly); + } + else if (currentRequestType == OperationType.Augmentation) + { + this.InitializeAugmentation(currentConfiguration.RuntimeClaimTypesList); + } + } + + /// + /// Validation is when SharePoint expects exactly 1 PickerEntity from the incoming SPClaim + /// + /// + protected void InitializeValidation(List runtimeClaimTypesList) + { + if (this.IncomingEntity == null) { throw new ArgumentNullException(nameof(this.IncomingEntity)); } + this.IncomingEntityClaimTypeConfig = runtimeClaimTypesList.FirstOrDefault(x => + String.Equals(x.ClaimType, this.IncomingEntity.ClaimType, StringComparison.InvariantCultureIgnoreCase) && + !x.UseMainClaimTypeOfDirectoryObject); + + if (this.IncomingEntityClaimTypeConfig == null) + { + Logger.Log($"[{AzureCP.ClaimsProviderName}] 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($"[{AzureCP.ClaimsProviderName}] Unable validate entity \"{this.IncomingEntity.Value}\" because its claim type \"{this.IncomingEntity.ClaimType}\" was not found in the ClaimTypes list of current configuration."); + } + + this.CurrentClaimTypeConfigList = new List(1) + { + this.IncomingEntityClaimTypeConfig + }; + this.ExactSearch = true; + this.Input = this.IncomingEntity.Value; + } + + /// + /// Search is when SharePoint expects a list of any PickerEntity that match input provided + /// + /// + protected void InitializeSearch(List runtimeClaimTypesList, 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 = runtimeClaimTypesList.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 = runtimeClaimTypesList.FindAll(x => this.DirectoryObjectTypes.Contains(x.EntityType)); + } + } + + protected void InitializeAugmentation(List runtimeClaimTypesList) + { + if (this.IncomingEntity == null) { throw new ArgumentNullException(nameof(this.IncomingEntity)); } + this.IncomingEntityClaimTypeConfig = runtimeClaimTypesList.FirstOrDefault(x => + String.Equals(x.ClaimType, this.IncomingEntity.ClaimType, StringComparison.InvariantCultureIgnoreCase) && + !x.UseMainClaimTypeOfDirectoryObject); + + if (this.IncomingEntityClaimTypeConfig == null) + { + Logger.Log($"[{AzureCP.ClaimsProviderName}] 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($"[{AzureCP.ClaimsProviderName}] Unable to augment entity \"{this.IncomingEntity.Value}\" because its claim type \"{this.IncomingEntity.ClaimType}\" was not found in the ClaimTypes list of current configuration."); + } + } + } +} diff --git a/AzureCP/Yvand.ClaimsProviders/Configuration/EntityProviderConfig.cs b/AzureCP/Yvand.ClaimsProviders/Configuration/EntityProviderConfig.cs new file mode 100644 index 00000000..cf992fe0 --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/Configuration/EntityProviderConfig.cs @@ -0,0 +1,566 @@ +using Microsoft.SharePoint.Administration; +using Microsoft.SharePoint.Administration.Claims; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace Yvand.ClaimsProviders.Config +{ + public interface IEntityProviderSettings + { + long Version { get; } + string Name { get; } + ClaimTypeConfigCollection ClaimTypes { get; } + bool AlwaysResolveUserInput { get; } + bool FilterExactMatchOnly { get; } + bool EnableAugmentation { get; } + string EntityDisplayTextPrefix { get; } + int Timeout { get; } + string CustomData { get; } + + // Copy of the internal runtime settings, which external classes can only access through an object implementing this interface + List RuntimeClaimTypesList { get; } + IEnumerable RuntimeMetadataConfig { get; } + IdentityClaimTypeConfig IdentityClaimTypeConfig { get; } + ClaimTypeConfig MainGroupClaimTypeConfig { get; } + } + + public class EntityProviderSettings : IEntityProviderSettings + { + public long Version { get; set; } + + public string Name { get; set; } + + public ClaimTypeConfigCollection ClaimTypes { get; set; } + + public bool AlwaysResolveUserInput { get; set; } + + public bool FilterExactMatchOnly { get; set; } + + public bool EnableAugmentation { get; set; } + + public string EntityDisplayTextPrefix { get; set; } + + public int Timeout { get; set; } + + public string CustomData { get; set; } + + public List RuntimeClaimTypesList { get; } + + public IEnumerable RuntimeMetadataConfig { get; } + + public IdentityClaimTypeConfig IdentityClaimTypeConfig { get; } + + public ClaimTypeConfig MainGroupClaimTypeConfig { get; } + + public EntityProviderSettings() { } + + public EntityProviderSettings(List runtimeClaimTypesList, IEnumerable runtimeMetadataConfig, IdentityClaimTypeConfig identityClaimTypeConfig, ClaimTypeConfig mainGroupClaimTypeConfig) + { + RuntimeClaimTypesList = runtimeClaimTypesList; + RuntimeMetadataConfig = runtimeMetadataConfig; + IdentityClaimTypeConfig = identityClaimTypeConfig; + MainGroupClaimTypeConfig = mainGroupClaimTypeConfig; + } + } + + public class EntityProviderConfig : SPPersistedObject + where TSettings : IEntityProviderSettings + { + /// + /// Gets the local settings, based on the global settings stored in a persisted object + /// + public TSettings LocalSettings { get; private set; } + + /// + /// Gets the current version of the local settings + /// + protected long LocalSettingsVersion { get; private set; } = 0; + + #region "Public runtime settings" + /// + /// gets or sets the claim types and their mapping with a DirectoryObject property + /// + public ClaimTypeConfigCollection ClaimTypes + { + get + { + if (_ClaimTypes == null) + { + _ClaimTypes = new ClaimTypeConfigCollection(ref this._ClaimTypesCollection); + } + return _ClaimTypes; + } + set + { + _ClaimTypes = value; + _ClaimTypesCollection = value == null ? null : value.innerCol; + } + } + [Persisted] + private Collection _ClaimTypesCollection; + private ClaimTypeConfigCollection _ClaimTypes; + + /// + /// Gets or sets whether to skip Azure AD lookup and consider any input as valid. + /// This can be useful to keep people picker working even if connectivity with the Azure tenant is lost. + /// + public bool AlwaysResolveUserInput + { + get => _AlwaysResolveUserInput; + set => _AlwaysResolveUserInput = value; + } + [Persisted] + private bool _AlwaysResolveUserInput; + + /// + /// Gets or sets whether to return only results that match exactly the user input (case-insensitive). + /// + public bool FilterExactMatchOnly + { + get => _FilterExactMatchOnly; + set => _FilterExactMatchOnly = value; + } + [Persisted] + private bool _FilterExactMatchOnly; + + /// + /// Gets or sets whether to return the Azure AD groups that the user is a member of. + /// + public bool EnableAugmentation + { + get => _EnableAugmentation; + set => _EnableAugmentation = value; + } + [Persisted] + private bool _EnableAugmentation = true; + + /// + /// Gets or sets a string that will appear as a prefix of the text of each result, in the people picker. + /// + public string EntityDisplayTextPrefix + { + get => _EntityDisplayTextPrefix; + set => _EntityDisplayTextPrefix = value; + } + [Persisted] + private string _EntityDisplayTextPrefix; + + /// + /// Gets or sets the timeout before giving up the query to Azure AD. + /// + public int Timeout + { + get + { +#if DEBUG + return _Timeout * 100; +#endif + return _Timeout; + } + set => _Timeout = value; + } + [Persisted] + private int _Timeout = ClaimsProviderConstants.DEFAULT_TIMEOUT; + + /// + /// Gets or sets the name of the claims provider using this settings + /// + public string ClaimsProviderName + { + get => _ClaimsProviderName; + set => _ClaimsProviderName = value; + } + [Persisted] + private string _ClaimsProviderName; + + [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; + #endregion + + #region "Public runtime properties" + private SPTrustedLoginProvider _SPTrust; + /// + /// Gets the SharePoint trust that has its property ClaimProviderName equals to + /// + public SPTrustedLoginProvider SPTrust + { + get + { + if (this._SPTrust == null) + { + this._SPTrust = Utils.GetSPTrustAssociatedWithClaimsProvider(this.ClaimsProviderName); + } + return this._SPTrust; + } + } + #endregion + + #region "Internal runtime settings" + protected List RuntimeClaimTypesList { get; private set; } + protected IEnumerable RuntimeMetadataConfig { get; private set; } + protected IdentityClaimTypeConfig IdentityClaimTypeConfig { get; private set; } + protected ClaimTypeConfig MainGroupClaimTypeConfig { get; private set; } + #endregion + + public EntityProviderConfig() { } + public EntityProviderConfig(string persistedObjectName, SPPersistedObject parent, string claimsProviderName) : base(persistedObjectName, parent) + { + this.ClaimsProviderName = claimsProviderName; + this.Initialize(); + } + + private void Initialize() + { + this.InitializeDefaultSettings(); + } + + public virtual bool InitializeDefaultSettings() + { + this.ClaimTypes = ReturnDefaultClaimTypesConfig(); + return true; + } + + /// + /// Sets the internal runtime settings properties + /// + /// True if successful, false if not + protected virtual bool InitializeInternalRuntimeSettings() + { + if (this.ClaimTypes?.Count <= 0) + { + Logger.Log($"[{this.ClaimsProviderName}] Cannot continue because configuration '{this.Name}' has 0 claim configured.", + TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); + return false; + } + + bool identityClaimTypeFound = false; + bool groupClaimTypeFound = false; + List claimTypesSetInTrust = new List(); + // Parse the ClaimTypeInformation collection set in the SPTrustedLoginProvider + foreach (SPTrustedClaimTypeInformation claimTypeInformation in this.SPTrust.ClaimTypeInformation) + { + // Search if current claim type in trust exists in ClaimTypeConfigCollection + ClaimTypeConfig claimTypeConfig = this.ClaimTypes.FirstOrDefault(x => + String.Equals(x.ClaimType, claimTypeInformation.MappedClaimType, StringComparison.InvariantCultureIgnoreCase) && + !x.UseMainClaimTypeOfDirectoryObject && + x.EntityProperty != DirectoryObjectProperty.NotSet); + + if (claimTypeConfig == null) + { + continue; + } + ClaimTypeConfig localClaimTypeConfig = claimTypeConfig.CopyConfiguration(); + localClaimTypeConfig.ClaimTypeDisplayName = claimTypeInformation.DisplayName; + claimTypesSetInTrust.Add(localClaimTypeConfig); + if (String.Equals(this.SPTrust.IdentityClaimTypeInformation.MappedClaimType, localClaimTypeConfig.ClaimType, StringComparison.InvariantCultureIgnoreCase)) + { + // Identity claim type found, set IdentityClaimTypeConfig property + identityClaimTypeFound = true; + this.IdentityClaimTypeConfig = IdentityClaimTypeConfig.ConvertClaimTypeConfig(localClaimTypeConfig); + } + else if (!groupClaimTypeFound && localClaimTypeConfig.EntityType == DirectoryObjectType.Group) + { + groupClaimTypeFound = true; + this.MainGroupClaimTypeConfig = localClaimTypeConfig; + } + } + + if (!identityClaimTypeFound) + { + Logger.Log($"[{this.ClaimsProviderName}] Cannot continue because identity claim type '{this.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 this.ClaimTypes.Where(x => x.UseMainClaimTypeOfDirectoryObject)) + { + ClaimTypeConfig localClaimTypeConfig = claimTypeConfig.CopyConfiguration(); + if (localClaimTypeConfig.EntityType == DirectoryObjectType.User) + { + localClaimTypeConfig.ClaimType = this.IdentityClaimTypeConfig.ClaimType; + localClaimTypeConfig.EntityPropertyToUseAsDisplayText = this.IdentityClaimTypeConfig.EntityPropertyToUseAsDisplayText; + } + else + { + // If not a user, it must be a group + if (this.MainGroupClaimTypeConfig == null) + { + continue; + } + localClaimTypeConfig.ClaimType = this.MainGroupClaimTypeConfig.ClaimType; + localClaimTypeConfig.EntityPropertyToUseAsDisplayText = this.MainGroupClaimTypeConfig.EntityPropertyToUseAsDisplayText; + localClaimTypeConfig.ClaimTypeDisplayName = this.MainGroupClaimTypeConfig.ClaimTypeDisplayName; + } + additionalClaimTypeConfigList.Add(localClaimTypeConfig); + } + + this.RuntimeClaimTypesList = new List(claimTypesSetInTrust.Count + additionalClaimTypeConfigList.Count); + this.RuntimeClaimTypesList.AddRange(claimTypesSetInTrust); + this.RuntimeClaimTypesList.AddRange(additionalClaimTypeConfigList); + + // Get all PickerEntity metadata with a DirectoryObjectProperty set + this.RuntimeMetadataConfig = this.ClaimTypes.Where(x => + !String.IsNullOrEmpty(x.EntityDataKey) && + x.EntityProperty != DirectoryObjectProperty.NotSet); + + return true; + } + + /// + /// Ensures that property is valid and up to date + /// + /// The property if is valid, null otherwise + public TSettings RefreshLocalSettingsIfNeeded() + { + Guid configurationId = this.Id; + EntityProviderConfig globalConfiguration = GetGlobalConfiguration(configurationId); + + if (globalConfiguration == null) + { + Logger.Log($"[{ClaimsProviderName}] Cannot continue because configuration '{configurationId}' was not found in configuration database, visit AzureCP admin pages in central administration to create it.", + TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); + this.LocalSettings = default(TSettings); + return default(TSettings); + } + + if (this.LocalSettingsVersion == globalConfiguration.Version) + { + Logger.Log($"[{ClaimsProviderName}] Configuration '{configurationId}' is up to date with version {this.LocalSettingsVersion}.", + TraceSeverity.VerboseEx, EventSeverity.Information, TraceCategory.Core); + return this.LocalSettings; + } + + Logger.Log($"[{ClaimsProviderName}] Configuration '{globalConfiguration.Name}' has new version {globalConfiguration.Version}, refreshing local copy", + TraceSeverity.Medium, EventSeverity.Information, TraceCategory.Core); + + globalConfiguration.ClaimsProviderName = this.ClaimsProviderName; + bool success = globalConfiguration.InitializeInternalRuntimeSettings(); + if (!success) + { + return default; + } + this.IdentityClaimTypeConfig = globalConfiguration.IdentityClaimTypeConfig; + this.MainGroupClaimTypeConfig = globalConfiguration.MainGroupClaimTypeConfig; + this.RuntimeClaimTypesList = globalConfiguration.RuntimeClaimTypesList; + this.MainGroupClaimTypeConfig = globalConfiguration.MainGroupClaimTypeConfig; + this.LocalSettings = (TSettings)globalConfiguration.GenerateLocalSettings(); +#if !DEBUGx + this.LocalSettingsVersion = globalConfiguration.Version; +#endif + + if (this.LocalSettings.ClaimTypes == null || this.LocalSettings.ClaimTypes.Count == 0) + { + Logger.Log($"[{ClaimsProviderName}] Configuration '{this.LocalSettings.Name}' was found but collection ClaimTypes is empty. Visit AzureCP admin pages in central administration to create it.", + TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); + } + return this.LocalSettings; + } + + /// + /// If it is valid, commits the current settings to the SharePoint settings database + /// + public override void Update() + { + this.ValidateConfiguration(); + base.Update(); + Logger.Log($"Successfully updated configuration '{this.Name}' with Id {this.Id}", TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); + } + + /// + /// If it is valid, commits the current settings to the SharePoint settings database + /// + /// If true, the call will not throw if the object already exists. + public override void Update(bool ensure) + { + this.ValidateConfiguration(); + base.Update(ensure); + Logger.Log($"Successfully updated configuration '{this.Name}' with Id {this.Id}", TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); + } + + /// + /// Ensures that the current settings is valid and can be safely saved and used + /// + /// + public virtual void ValidateConfiguration() + { + // In case ClaimTypes collection was modified, test if it is still valid before committed changes to database + try + { + ClaimTypeConfigCollection testUpdateCollection = new ClaimTypeConfigCollection(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); + } + } + + /// + /// Removes the current persisted object from the SharePoint configuration database + /// + public override void Delete() + { + base.Delete(); + Logger.Log($"Successfully deleted configuration '{this.Name}' with Id {this.Id}", TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); + } + + /// + /// 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; + } + + // This method fires 3 times in a raw just when the configurationis updated, and anyway it bypassws the logic to update only if needed (and safely in regards to thread safety) + //protected override void OnDeserialization() + //{ + // base.OnDeserialization(); + // this.InitializeInternalRuntimeSettings(); + //} + + /// + /// Returns a read-only settings, copied from the current settings. + /// + /// + protected virtual TSettings GenerateLocalSettings() + { + IEntityProviderSettings entityProviderSettings = new EntityProviderSettings( + this.RuntimeClaimTypesList, + this.RuntimeMetadataConfig, + this.IdentityClaimTypeConfig, + this.MainGroupClaimTypeConfig) + { + AlwaysResolveUserInput = this.AlwaysResolveUserInput, + ClaimTypes = this.ClaimTypes, + CustomData = this.CustomData, + EnableAugmentation = this.EnableAugmentation, + EntityDisplayTextPrefix = this.EntityDisplayTextPrefix, + FilterExactMatchOnly = this.FilterExactMatchOnly, + Name = this.Name, + Timeout = this.Timeout, + Version = this.Version, + }; + return (TSettings)entityProviderSettings; + } + + /// + /// Applies the settings passed in parameter to the current settings + /// + /// + public virtual void ApplySettings(TSettings settings, bool commitIfValid) + { + this.ClaimTypes = new ClaimTypeConfigCollection(this.SPTrust); + foreach (ClaimTypeConfig claimTypeConfig in settings.ClaimTypes) + { + this.ClaimTypes.Add(claimTypeConfig.CopyConfiguration(), false); + } + this.AlwaysResolveUserInput = settings.AlwaysResolveUserInput; + this.FilterExactMatchOnly = settings.FilterExactMatchOnly; + this.EnableAugmentation = settings.EnableAugmentation; + this.EntityDisplayTextPrefix = settings.EntityDisplayTextPrefix; + this.Timeout = settings.Timeout; + this.CustomData = settings.CustomData; + + if(commitIfValid) + { + this.Update(); + } + } + + //public virtual void ResetCurrentConfiguration() + //{ + // throw new NotImplementedException(); + //} + + public virtual ClaimTypeConfigCollection ReturnDefaultClaimTypesConfig() + { + throw new NotImplementedException(); + } + + /// + /// Returns the global configuration, stored as a persisted object in the SharePoint configuration database + /// + /// The ID of the configuration + /// Set to true to initialize the property + /// + public static EntityProviderConfig GetGlobalConfiguration(Guid configurationId, bool initializeLocalSettings = false) + { + SPFarm parent = SPFarm.Local; + try + { + //IEntityProviderSettings settings = (IEntityProviderSettings)parent.GetObject(configurationName, parent.Id, typeof(EntityProviderConfiguration)); + //Conf settings = (Conf)parent.GetObject(configurationName, parent.Id, T); + //Conf settings = (Conf)parent.GetObject(configurationName, parent.Id, typeof(Conf)); + EntityProviderConfig configuration = (EntityProviderConfig)parent.GetObject(configurationId); + if (configuration != null && initializeLocalSettings == true) + { + configuration.RefreshLocalSettingsIfNeeded(); + } + return configuration; + } + catch (Exception ex) + { + Logger.LogException(String.Empty, $"while retrieving configuration ID '{configurationId}'", TraceCategory.Configuration, ex); + } + return null; + } + + public static void DeleteGlobalConfiguration(Guid configurationId) + { + EntityProviderConfig configuration = (EntityProviderConfig)GetGlobalConfiguration(configurationId); + if (configuration == null) + { + Logger.Log($"Configuration ID '{configurationId}' was not found in configuration database", TraceSeverity.Medium, EventSeverity.Error, TraceCategory.Core); + return; + } + configuration.Delete(); + Logger.Log($"Configuration ID '{configurationId}' was successfully deleted from configuration database", TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); + } + + public static EntityProviderConfig CreateGlobalConfiguration(Guid configurationID, string configurationName, string claimsProviderName, Type T) + { + if (String.IsNullOrWhiteSpace(claimsProviderName)) + { + throw new ArgumentNullException(nameof(claimsProviderName)); + } + + // Ensure it doesn't already exists and delete it if so + EntityProviderConfig existingConfig = GetGlobalConfiguration(configurationID); + if (existingConfig != null) + { + DeleteGlobalConfiguration(configurationID); + } + + Logger.Log($"Creating configuration '{configurationName}' with Id {configurationID}...", TraceSeverity.VerboseEx, EventSeverity.Error, TraceCategory.Core); + + ConstructorInfo ctorWithParameters = T.GetConstructor(new[] { typeof(string), typeof(SPFarm), typeof(string) }); + EntityProviderConfig config = (EntityProviderConfig)ctorWithParameters.Invoke(new object[] { configurationName, SPFarm.Local, claimsProviderName }); + + config.Id = configurationID; + // If parameter ensure is true, the call will not throw if the object already exists. + config.Update(true); + Logger.Log($"Created configuration '{configurationName}' with Id {config.Id}", TraceSeverity.High, EventSeverity.Information, TraceCategory.Core); + return config; + } + } +} diff --git a/AzureCP/Yvand.ClaimsProviders/EntityProviderBase.cs b/AzureCP/Yvand.ClaimsProviders/EntityProviderBase.cs new file mode 100644 index 00000000..20a3d910 --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/EntityProviderBase.cs @@ -0,0 +1,35 @@ +using Microsoft.Graph.Models; +using System.Collections.Generic; +using System.Threading.Tasks; +using Yvand.ClaimsProviders.Config; + +namespace Yvand.ClaimsProviders +{ + public abstract class EntityProviderBase + { + /// + /// Gets the name of the claims provider using this class + /// + public string ClaimsProviderName { get; } + + /// + /// Returns a list of users and groups + /// + /// + /// + public abstract Task> SearchOrValidateEntitiesAsync(OperationContext currentContext); + + /// + /// Returns the groups the user is member of + /// + /// + /// + /// + public abstract Task> GetEntityGroupsAsync(OperationContext currentContext, DirectoryObjectProperty groupClaimTypeConfig); + + public EntityProviderBase(string claimsProviderName) + { + this.ClaimsProviderName = claimsProviderName; + } + } +} diff --git a/AzureCP/AzureCPLogging.cs b/AzureCP/Yvand.ClaimsProviders/Logger.cs similarity index 71% rename from AzureCP/AzureCPLogging.cs rename to AzureCP/Yvand.ClaimsProviders/Logger.cs index e0550a82..b5054e0f 100644 --- a/AzureCP/AzureCPLogging.cs +++ b/AzureCP/Yvand.ClaimsProviders/Logger.cs @@ -6,52 +6,52 @@ using System.Linq; using System.Text; -namespace azurecp +namespace Yvand.ClaimsProviders { + public enum TraceCategory + { + [CategoryName("Core"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Core, + [CategoryName("Configuration"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Configuration, + [CategoryName("Lookup"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Lookup, + [CategoryName("Claims Picking"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Claims_Picking, + [CategoryName("Rehydration"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Rehydration, + [CategoryName("Augmentation"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Augmentation, + [CategoryName("Debug"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Debug, + [CategoryName("Custom"), + DefaultTraceSeverity(TraceSeverity.Medium), + DefaultEventSeverity(EventSeverity.Error)] + Custom, + } + /// /// Implemented as documented in http://www.sbrickey.com/Tech/Blog/Post/Custom_Logging_in_SharePoint_2010 /// [System.Runtime.InteropServices.GuidAttribute("3DD2C709-C860-4A20-8AF2-0FDDAA9C406B")] - public class ClaimsProviderLogging : SPDiagnosticsServiceBase + public class Logger : SPDiagnosticsServiceBase { public readonly static string DiagnosticsAreaName = "AzureCP"; - public enum TraceCategory - { - [CategoryName("Core"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Core, - [CategoryName("Configuration"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Configuration, - [CategoryName("Lookup"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Lookup, - [CategoryName("Claims Picking"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Claims_Picking, - [CategoryName("Rehydration"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Rehydration, - [CategoryName("Augmentation"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Augmentation, - [CategoryName("Debug"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Debug, - [CategoryName("Custom"), - DefaultTraceSeverity(TraceSeverity.Medium), - DefaultEventSeverity(EventSeverity.Error)] - Custom, - } - public static void Log(string message, TraceSeverity traceSeverity, EventSeverity eventSeverity, TraceCategory category) { try @@ -64,13 +64,13 @@ public static void Log(string message, TraceSeverity traceSeverity, EventSeverit } } - public static void LogException(string ProviderInternalName, string faultyAction, TraceCategory category, Exception ex) + public static void LogException(string ClaimsProviderName, string customMessage, TraceCategory category, Exception ex) { try { if (ex is AggregateException) { - StringBuilder message = new StringBuilder($"[{ProviderInternalName}] Unexpected error(s) occurred {faultyAction}:"); + StringBuilder message = new StringBuilder($"[{ClaimsProviderName}] Unexpected error(s) {customMessage}:"); string excetpionMessage = Environment.NewLine + "[EXCEPTION {0}]: {1}: {2}. Callstack: {3}"; var aggEx = ex as AggregateException; int count = 1; @@ -91,14 +91,14 @@ public static void LogException(string ProviderInternalName, string faultyAction } else { - string message = "[{0}] Unexpected error occurred {1}: {2}: {3}, Callstack: {4}"; + string message = "[{0}] Unexpected error {1}: {2}: {3}, Callstack: {4}"; if (ex.InnerException != null) { - message = String.Format(message, ProviderInternalName, faultyAction, ex.InnerException.GetType().FullName, ex.InnerException.Message, ex.InnerException.StackTrace); + message = String.Format(message, ClaimsProviderName, customMessage, ex.InnerException.GetType().FullName, ex.InnerException.Message, ex.InnerException.StackTrace); } else { - message = String.Format(message, ProviderInternalName, faultyAction, ex.GetType().FullName, ex.Message, ex.StackTrace); + message = String.Format(message, ClaimsProviderName, customMessage, ex.GetType().FullName, ex.Message, ex.StackTrace); } WriteTrace(category, TraceSeverity.Unexpected, message); } @@ -129,30 +129,30 @@ public static void LogDebug(string message) } } - public static ClaimsProviderLogging Local + public static Logger Local { get { - var LogSvc = SPDiagnosticsServiceBase.GetLocal(); + var LogSvc = SPDiagnosticsServiceBase.GetLocal(); // if the Logging Service is registered, just return it. if (LogSvc != null) { return LogSvc; } - ClaimsProviderLogging svc = null; + Logger svc = null; SPSecurity.RunWithElevatedPrivileges(delegate () { // otherwise instantiate and register the new instance, which requires farm administrator privileges - svc = new ClaimsProviderLogging(); + svc = new Logger(); //svc.Update(); }); return svc; } } - public ClaimsProviderLogging() : base(DiagnosticsAreaName, SPFarm.Local) { } - public ClaimsProviderLogging(string name, SPFarm farm) : base(name, farm) { } + public Logger() : base(DiagnosticsAreaName, SPFarm.Local) { } + public Logger(string name, SPFarm farm) : base(name, farm) { } protected override IEnumerable ProvideAreas() { yield return Area; } public override string DisplayName { get { return DiagnosticsAreaName; } } @@ -180,7 +180,7 @@ public static string FormatException(Exception ex) public static void Unregister() { SPFarm.Local.Services - .OfType() + .OfType() .ToList() .ForEach(s => { @@ -263,24 +263,25 @@ private static EventSeverity GetCategoryDefaultEventSeverity(TraceCategory cat) } #endregion - #region Attributes - private class CategoryNameAttribute : Attribute - { - public string Name { get; private set; } - public CategoryNameAttribute(string Name) { this.Name = Name; } - } + + } + #region Attributes + class CategoryNameAttribute : Attribute + { + public string Name { get; private set; } + public CategoryNameAttribute(string Name) { this.Name = Name; } + } - private class DefaultTraceSeverityAttribute : Attribute - { - public TraceSeverity Severity { get; private set; } - public DefaultTraceSeverityAttribute(TraceSeverity severity) { this.Severity = severity; } - } + class DefaultTraceSeverityAttribute : Attribute + { + public TraceSeverity Severity { get; private set; } + public DefaultTraceSeverityAttribute(TraceSeverity severity) { this.Severity = severity; } + } - private class DefaultEventSeverityAttribute : Attribute - { - public EventSeverity Severity { get; private set; } - public DefaultEventSeverityAttribute(EventSeverity severity) { this.Severity = severity; } - } - #endregion + class DefaultEventSeverityAttribute : Attribute + { + public EventSeverity Severity { get; private set; } + public DefaultEventSeverityAttribute(EventSeverity severity) { this.Severity = severity; } } + #endregion } diff --git a/AzureCP/Yvand.ClaimsProviders/Utils.cs b/AzureCP/Yvand.ClaimsProviders/Utils.cs new file mode 100644 index 00000000..977091df --- /dev/null +++ b/AzureCP/Yvand.ClaimsProviders/Utils.cs @@ -0,0 +1,99 @@ +using Microsoft.SharePoint.Administration; +using Microsoft.SharePoint.Administration.Claims; +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Yvand.ClaimsProviders.Config; +using static Yvand.ClaimsProviders.Logger; + +namespace Yvand.ClaimsProviders +{ + public static class Utils + { + /// + /// 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 GetSPTrustAssociatedWithClaimsProvider(string claimProviderName) + { + var lp = SPSecurityTokenServiceManager.Local.TrustedLoginProviders.Where(x => String.Equals(x.ClaimProviderName, claimProviderName, StringComparison.OrdinalIgnoreCase)); + + if (lp != null && lp.Count() == 1) + { + return lp.First(); + } + + if (lp != null && lp.Count() > 1) + { + Logger.Log($"[{claimProviderName}] Cannot continue because '{claimProviderName}' is set with multiple SPTrustedIdentityTokenIssuer", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Core); + } + Logger.Log($"[{claimProviderName}] Cannot continue because '{claimProviderName}' is not set with any SPTrustedIdentityTokenIssuer.\r\nVisit {ClaimsProviderConstants.PUBLICSITEURL} for more information.", TraceSeverity.High, EventSeverity.Warning, TraceCategory.Core); + return null; + } + + /// + /// Check if AzureCP should process input (and show results) based on current URL (context) + /// + /// The context, as a URI + /// + public static bool ShouldRun(Uri context, string claimProviderName) + { + 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 AzureCP only if current web app uses it + // It is not possible to exclude zones where AzureCP is not used because: + // Consider following scenario: default zone is WinClaims, intranet zone is Federated: + // In intranet zone, when creating permission, AzureCP will be called 2 times. The 2nd time (in FillResolve (SPClaim)), the context will always be the URL of the 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(SPTrustedAuthenticationProvider)) + { + // Check if the current SPTrustedAuthenticationProvider is associated with the claim provider + if (String.Equals(prov.ClaimProviderName, claimProviderName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + } + return false; + } + + /// + /// Copy in target all the fields of source which have the decoration [Persisted] set on the specified type (fields inherited from parent types are ignored) + /// + /// + /// + /// + /// The target object with fields decorated with [Persisted] set from the source object + public static object CopyPersistedFields(Type T, object source, object target) + { + List persistedFields = T + .GetRuntimeFields() + .Where(field => field.GetCustomAttributes(typeof(PersistedAttribute), inherit: false).Any()) + .ToList(); + + foreach(FieldInfo field in persistedFields) + { + field.SetValue(target, field.GetValue(source)); + } + return target; + } + } +} diff --git a/testEnvironments.json b/testEnvironments.json deleted file mode 100644 index a110b57d..00000000 --- a/testEnvironments.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1", - "environments": [ - // See https://aka.ms/remotetesting for more details - // about how to configure remote environments. - //{ - // "name": "WSL Ubuntu", - // "type": "wsl", - // "wslDistribution": "Ubuntu" - //}, - //{ - // "name": "Docker dotnet/sdk", - // "type": "docker", - // "dockerImage": "mcr.microsoft.com/dotnet/sdk" - //} - ] -} \ No newline at end of file