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