From 6986ea85b3d7d1d28353fa9b6c9c6374b06985cb Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Thu, 4 Jul 2024 17:32:19 +0200 Subject: [PATCH 01/23] Update Yvand.EntraCP.nuspec --- Yvand.EntraCP/Yvand.EntraCP.nuspec | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Yvand.EntraCP/Yvand.EntraCP.nuspec b/Yvand.EntraCP/Yvand.EntraCP.nuspec index f6cf9f7..90553e3 100644 --- a/Yvand.EntraCP/Yvand.EntraCP.nuspec +++ b/Yvand.EntraCP/Yvand.EntraCP.nuspec @@ -8,18 +8,20 @@ $description$ true Apache-2.0 - https://github.com/Yvand/AzureCP + https://github.com/Yvand/EntraCP $copyright$ SharePoint ClaimsProvider - https://github.com/Yvand/AzureCP/blob/master/CHANGELOG.md - + https://github.com/Yvand/EntraCP/blob/master/CHANGELOG.md + - - + + + + \ No newline at end of file From bfc6265079dfc5ad8ee021516110a7bfb8f528b8 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Wed, 17 Jul 2024 13:26:07 +0200 Subject: [PATCH 02/23] Fix missing group members when only users members of some groups can be found (#273) * Update Populate-EntraIDTenant.ps1 * Update EntraIDEntityProvider.cs * add pagination to get all group members * fix init of lists * improve tests * update tests * return entities cloned from their source list * Update CHANGELOG.md --- CHANGELOG.md | 6 ++ .../BasicConfigurationTests.cs | 32 ++++---- Yvand.EntraCP.Tests/BypassDirectoryTests.cs | 26 +++---- .../ClaimsProviderTestsBase.cs | 2 +- Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs | 24 +++--- .../FilterUsersBasedOnGroupsTests.cs | 27 +++---- Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs | 8 +- Yvand.EntraCP.Tests/RequireExactMatchTests.cs | 4 +- .../SecurityEnabledGroupsTests.cs | 8 +- .../Setup/Populate-EntraIDTenant.ps1 | 6 +- Yvand.EntraCP.Tests/UnitTestsHelper.cs | 78 ++++++++++++++++--- .../EntraIDEntityProvider.cs | 49 +++++++----- 12 files changed, 167 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8d326..7834e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change log for ~~AzureCP~~ EntraCP +## EntraCP v27.0 - Unreleased + +* Ensure that all group members are retrieved when only users members of specified groups can be found in SharePoint +* Update the script that provisions tenant with test users and groups, to be more reliable and provision 999 users (instead of 50), so tests are more realistics +* Improve tests + ## EntraCP v26.0.20240627.35 enhancements & bug-fixes - Published in June 27, 2024 * Fix an NullReferenceException in a very rare scenario where ClaimsPrincipal.Identity is null diff --git a/Yvand.EntraCP.Tests/BasicConfigurationTests.cs b/Yvand.EntraCP.Tests/BasicConfigurationTests.cs index 433603f..34b7611 100644 --- a/Yvand.EntraCP.Tests/BasicConfigurationTests.cs +++ b/Yvand.EntraCP.Tests/BasicConfigurationTests.cs @@ -20,31 +20,31 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllTestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test] - public void TestRandomTestGroups([Random(0, UnitTestsHelper.TestGroupsCount - 1, 5)] int idx) - { - EntraIdTestGroup group = EntraIdTestGroupsSource.Groups[idx]; - TestSearchAndValidateForEntraIDGroup(group); - } + //[Test] + //public void TestRandomTestGroups([Random(0, UnitTestsHelper.TotalNumberTestGroups - 1, 5)] int idx) + //{ + // EntraIdTestGroup group = EntraIdTestGroupsSource.Groups[idx]; + // TestSearchAndValidateForEntraIDGroup(group); + //} - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllTestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } - [Test] - public void TestRandomTestUsers([Random(0, UnitTestsHelper.TestUsersCount - 1, 5)] int idx) - { - var user = EntraIdTestUsersSource.Users[idx]; - base.TestSearchAndValidateForEntraIDUser(user); - } + //[Test] + //public void TestRandomTestUsers([Random(0, UnitTestsHelper.TotalNumberTestUsers - 1, 5)] int idx) + //{ + // var user = EntraIdTestUsersSource.Users[idx]; + // base.TestSearchAndValidateForEntraIDUser(user); + //} [Test] [Repeat(5)] diff --git a/Yvand.EntraCP.Tests/BypassDirectoryTests.cs b/Yvand.EntraCP.Tests/BypassDirectoryTests.cs index 474cf07..c2eb08b 100644 --- a/Yvand.EntraCP.Tests/BypassDirectoryTests.cs +++ b/Yvand.EntraCP.Tests/BypassDirectoryTests.cs @@ -9,8 +9,8 @@ namespace Yvand.EntraClaimsProvider.Tests [Parallelizable(ParallelScope.Children)] public class BypassDirectoryOnClaimTypesTests : ClaimsProviderTestsBase { - string PrefixBypassUserSearch = "bypass-user:"; - string PrefixBypassGroupSearch = "bypass-group:"; + const string PrefixBypassUserSearch = "bypass-user:"; + const string PrefixBypassGroupSearch = "bypass-group:"; public override void InitializeSettings() { base.InitializeSettings(); @@ -26,8 +26,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); user.UserPrincipalName = user.DisplayName; @@ -36,8 +36,8 @@ public void TestAllEntraIDUsers(EntraIdTestUser user) base.TestSearchAndValidateForEntraIDUser(user); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); group.Id = group.DisplayName; @@ -45,9 +45,9 @@ public void TestAllEntraIDGroups(EntraIdTestGroup group) TestSearchAndValidateForEntraIDGroup(group); } - [TestCase("bypass-user:externalUser@contoso.com", 1, "externalUser@contoso.com")] - [TestCase("bypass-user:", 0, "")] - [TestCase("bypass-group:", 0, "")] + [TestCase(PrefixBypassUserSearch + "externalUser@contoso.com", 1, "externalUser@contoso.com")] + [TestCase(PrefixBypassUserSearch, 0, "")] + [TestCase(PrefixBypassGroupSearch, 0, "")] public void TestBypassDirectoryByClaimType(string inputValue, int expectedCount, string expectedClaimValue) { TestSearchOperation(inputValue, expectedCount, expectedClaimValue); @@ -77,14 +77,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs index 8492a7a..8df4411 100644 --- a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs +++ b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs @@ -219,7 +219,7 @@ public void TestSearchAndValidateForEntraIDUser(EntraIdTestUser entity) public virtual void TestAugmentationOfGoldUsersAgainstRandomGroups() { Random rnd = new Random(); - int randomIdx = rnd.Next(0, UnitTestsHelper.TestGroupsCount - 1); + int randomIdx = rnd.Next(0, EntraIdTestGroupsSource.Groups.Count - 1); Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOfGoldUsersAgainstRandomGroups: Get group in EntraIdTestGroupsSource.Groups at index {randomIdx}."); EntraIdTestGroup randomGroup = null; try diff --git a/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs b/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs index 1b1b07d..f56b4b2 100644 --- a/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs +++ b/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs @@ -21,14 +21,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -60,14 +60,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -99,14 +99,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs b/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs index 0b2a49a..ecbb743 100644 --- a/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs +++ b/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Yvand.EntraClaimsProvider.Tests { @@ -12,7 +13,7 @@ public class FilterUsersBasedOnSingleGroupTests : ClaimsProviderTestsBase public override void InitializeSettings() { base.InitializeSettings(); - Settings.RestrictSearchableUsersByGroups = EntraIdTestGroupsSource.ASecurityEnabledGroup.Id; + Settings.RestrictSearchableUsersByGroups = EntraIdTestGroupsSource.GetSomeGroups(true, 1).ToArray()[0].Id; base.ApplySettings(); } @@ -22,8 +23,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllTestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -47,17 +48,9 @@ public override void InitializeSettings() { base.InitializeSettings(); - // Pick the Id of 18 (max possible) random groups, and it in property RestrictSearchableUsersByGroups - List groupIdsList = new List(); - Random rnd = new Random(); - for (int groupsCount = 1; groupsCount <= 18; groupsCount++) - { - int randomIdx = rnd.Next(0, UnitTestsHelper.TestGroupsCount - 1); - groupIdsList.Add(EntraIdTestGroupsSource.Groups[randomIdx].Id); - } - Settings.RestrictSearchableUsersByGroups = String.Join(",", groupIdsList); + // Pick the Id of 18 (max possible) random groups, and set them in property RestrictSearchableUsersByGroups + Settings.RestrictSearchableUsersByGroups = String.Join(",", EntraIdTestGroupsSource.GetSomeGroups(true, 18).Select(x => x.Id).ToArray()); Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Set property RestrictSearchableUsersByGroups: \"{Settings.RestrictSearchableUsersByGroups}\"."); - base.ApplySettings(); } @@ -67,8 +60,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllTestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -92,9 +85,7 @@ public class DebugFilterUsersBasedOnMultipleGroupsTests : ClaimsProviderTestsBas public override void InitializeSettings() { base.InitializeSettings(); - Settings.RestrictSearchableUsersByGroups = "dbcdaf68-5949-4a15-b2f8-f385b41a9fca,72860e7b-93d5-46f7-ab56-480112f76548,461e8865-0f39-4199-86c6-0c8e25ad57ea,33780f8d-8345-4402-bc6a-d9c7e5d40d36,2c6b2fde-4f89-417a-9d8c-5e459e1520cf,7059e0ae-0cbc-4f4d-9d87-fef7289a7f50,dbcdaf68-5949-4a15-b2f8-f385b41a9fca,3cc758d6-7198-470f-878a-e5cd41a35e02,b05ddc92-b639-4ba2-95b3-9fdbc1bff2f2,40a7290c-9b67-4b7c-98ff-0c9871544423,71aa60d9-c4d1-4eaf-b398-3099b12afd88,0f51d30e-e03c-44ea-8d13-df0ca0df7a16,0f51d30e-e03c-44ea-8d13-df0ca0df7a16,982369ed-e88f-4b21-9b89-29067a0fa326,a40d2ded-2ab0-463f-b000-b3351ca6341d,ce5a4725-e719-4c2d-89bc-1c356facde99,de600b84-29aa-470c-b6ca-1459591728fb,152456d1-73a0-46d6-ac02-0403f2b5593e"; - Settings.RestrictSearchableUsersByGroups = "db41d655-d796-43fb-9e23-351ee8b5bdb0,461e8865-0f39-4199-86c6-0c8e25ad57ea,21dd6198-c447-48dd-9ea8-347f804c4dec,dcf1e533-6d55-4b00-9788-f0d81e287c8a,21dd6198-c447-48dd-9ea8-347f804c4dec,719006f9-8eb0-48fd-8f95-556f07b0123b,d6896744-f16b-4802-9f13-0e2c9fa06274,dcf1e533-6d55-4b00-9788-f0d81e287c8a,2ae8ff19-0e4f-45cc-98ea-84f1c53e60f2,bea2607f-513a-4324-a6a1-620ee1c0ced4,bcd82b83-97c5-4c1c-9cce-58643a286298,34fe3af4-7ed9-4b5e-a64e-c0b230d5dfb4,136f71a2-c57c-4a3f-8aec-4d694e442b87,dbf5a5c3-5f51-42d6-a519-258c76960f75,33780f8d-8345-4402-bc6a-d9c7e5d40d36,a40d2ded-2ab0-463f-b000-b3351ca6341d,21dd6198-c447-48dd-9ea8-347f804c4dec,34fe3af4-7ed9-4b5e-a64e-c0b230d5dfb4"; - Settings.RestrictSearchableUsersByGroups = "71aa60d9-c4d1-4eaf-b398-3099b12afd88,56e7a2e2-f565-450e-94cd-0d7d314217d1,c9a94341-89b5-4109-a501-2a14027b5bf0,8962bad6-ceca-43ff-a4be-9258ff81af2f,36d78c5a-80f2-4f3d-8f37-3a347d000d56,152456d1-73a0-46d6-ac02-0403f2b5593e,d6896744-f16b-4802-9f13-0e2c9fa06274,de600b84-29aa-470c-b6ca-1459591728fb,d51f225f-b484-4898-b425-5d48553aad16,36d78c5a-80f2-4f3d-8f37-3a347d000d56,0f51d30e-e03c-44ea-8d13-df0ca0df7a16,1eb5e51e-0bea-40c3-9ffb-0b85dbe2f9bf,dcf1e533-6d55-4b00-9788-f0d81e287c8a,c9a94341-89b5-4109-a501-2a14027b5bf0,3cc758d6-7198-470f-878a-e5cd41a35e02,dcf1e533-6d55-4b00-9788-f0d81e287c8a,21dd6198-c447-48dd-9ea8-347f804c4dec,253fe6d4-8e07-49a1-8a74-143d265eefbe"; + Settings.RestrictSearchableUsersByGroups = String.Join(",", EntraIdTestGroupsSource.GetSomeGroups(true, 18).Select(x => x.Id).ToArray()); Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Set property RestrictSearchableUsersByGroups: \"{Settings.RestrictSearchableUsersByGroups}\"."); base.ApplySettings(); } diff --git a/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs b/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs index b6cd6ad..6bb3e3e 100644 --- a/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs +++ b/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs @@ -24,14 +24,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/RequireExactMatchTests.cs b/Yvand.EntraCP.Tests/RequireExactMatchTests.cs index 12bb558..f0b7f7d 100644 --- a/Yvand.EntraCP.Tests/RequireExactMatchTests.cs +++ b/Yvand.EntraCP.Tests/RequireExactMatchTests.cs @@ -21,8 +21,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetTestData), null)] - public void TestAllEntraIDUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] + public void TestUsers(EntraIdTestUser user) { // Input is not the full UPN value: it should not return any result TestSearchOperation(user.UserPrincipalName.Substring(0, 5), 0, String.Empty); diff --git a/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs b/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs index 4b7220d..57163ac 100644 --- a/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs +++ b/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs @@ -24,8 +24,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } @@ -57,8 +57,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetTestData), new object[] { true })] - public void TestAllEntraIDGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] + public void TestGroups(EntraIdTestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } diff --git a/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 b/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 index 907d32c..d15c29c 100644 --- a/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 +++ b/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Users +#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Users, Microsoft.Graph.Groups <# .SYNOPSIS @@ -94,7 +94,7 @@ $passwordProfile = @{ } # Bulk add users -$totalUsers = 50 +$totalUsers = 1000 for ($i = 1; $i -le $totalUsers; $i++) { $accountName = "$($memberUsersNamePrefix)$("{0:D3}" -f $i)" $userPrincipalName = "$($accountName)@$($tenantName)" @@ -124,7 +124,7 @@ foreach ($guestUser in $guestUsersList) { } # groups -$allMemberUsersInEntra = Get-MgUser -ConsistencyLevel eventual -Count userCount -Filter "startsWith(DisplayName, '$($memberUsersNamePrefix)')" -OrderBy UserPrincipalName +$allMemberUsersInEntra = Get-MgUser -ConsistencyLevel eventual -Count userCount -Filter "startsWith(DisplayName, '$($memberUsersNamePrefix)')" -OrderBy UserPrincipalName -Top $totalUsers $usersMemberOfAllGroups = [System.Linq.Enumerable]::Where($usersWithSpecificSettings, [Func[object, bool]] { param($x) $x.IsMemberOfAllGroups -eq $true }) # Bulk add groups diff --git a/Yvand.EntraCP.Tests/UnitTestsHelper.cs b/Yvand.EntraCP.Tests/UnitTestsHelper.cs index df7e782..51dbe68 100644 --- a/Yvand.EntraCP.Tests/UnitTestsHelper.cs +++ b/Yvand.EntraCP.Tests/UnitTestsHelper.cs @@ -2,6 +2,7 @@ using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; using Microsoft.SharePoint.Administration.Claims; +using Microsoft.Web.Hosting.Administration; using Newtonsoft.Json; using NUnit.Framework; using System; @@ -36,8 +37,8 @@ public class UnitTestsHelper public static string DataFile_EntraId_TestGroups => TestContext.Parameters["DataFile_EntraId_TestGroups"]; public static string TestUsersAccountNamePrefix => TestContext.Parameters["UserAccountNamePrefix"]; public static string TestGroupsAccountNamePrefix => TestContext.Parameters["GroupAccountNamePrefix"]; - public const int TestUsersCount = 50 + 3; // 50 members + 3 guests - public const int TestGroupsCount = 50; + public const int MaxNumberOfUsersToTest = 100; + public const int MaxNumberOfGroupsToTest = 100; static TextWriterTraceListener Logger { get; set; } public static EntraIDProviderConfiguration PersistedConfiguration; private static IEntraIDProviderSettings OriginalSettings; @@ -186,12 +187,17 @@ public static void Cleanup() } } - public class EntraIdTestGroup + public class EntraIdTestGroup : ICloneable { public string Id; public string DisplayName; public string GroupType; public bool SecurityEnabled = true; + + public object Clone() + { + return this.MemberwiseClone(); + } } public class EntraIdTestGroupSettings : EntraIdTestGroup @@ -202,28 +208,29 @@ public class EntraIdTestGroupSettings : EntraIdTestGroup public class EntraIdTestGroupsSource { private static object _LockInitGroupsList = new object(); + private static bool listInitialized = false; private static List _Groups; public static List Groups { get { - if (_Groups != null) { return _Groups; } + if (listInitialized) { return _Groups; } lock (_LockInitGroupsList) { if (_Groups != null) { return _Groups; } _Groups = new List(); - foreach (EntraIdTestGroup group in GetTestData(false)) + foreach (EntraIdTestGroup group in ReadDataSource(false)) { _Groups.Add(group); } + listInitialized = true; Trace.TraceInformation($"{DateTime.Now:s} [{typeof(EntraIdTestGroupsSource).Name}] Initialized List of {nameof(Groups)} with {_Groups.Count} items."); return _Groups; } } } - public static EntraIdTestGroup ASecurityEnabledGroup => Groups.First(x => x.SecurityEnabled); - public static EntraIdTestGroup ANonSecurityEnabledGroup => Groups.First(x => !x.SecurityEnabled); + private static Random RandomNumber = new Random(); private static object _LockInitGroupsSettingsList = new object(); private static List _GroupsSettings; @@ -256,7 +263,7 @@ public static List GroupsSettings } } - public static IEnumerable GetTestData(bool securityEnabledGroupsOnly = false) + private static IEnumerable ReadDataSource(bool securityEnabledGroupsOnly = false) { string csvPath = UnitTestsHelper.DataFile_EntraId_TestGroups; DataTable dt = DataTable.New.ReadCsv(csvPath); @@ -274,6 +281,25 @@ public static IEnumerable GetTestData(bool securityEnabledGrou yield return registrationData; } } + + public static IEnumerable GetSomeGroups(bool securityEnabledGroupsOnly, int count) + { + if (count > Groups.Count) + { + count = Groups.Count; + } + + List userIdxs = new List(count); + for (int i = 0; i < count; i++) + { + userIdxs.Add(RandomNumber.Next(0, Groups.Count - 1)); + } + + foreach (int userIdx in userIdxs) + { + yield return Groups[userIdx].Clone() as EntraIdTestGroup; + } + } } public enum UserType @@ -282,7 +308,7 @@ public enum UserType Guest } - public class EntraIdTestUser + public class EntraIdTestUser : ICloneable { public string Id; public string DisplayName; @@ -290,6 +316,11 @@ public class EntraIdTestUser public UserType UserType; public string Mail; public string GivenName; + + public object Clone() + { + return this.MemberwiseClone(); + } } public class EntraIdTestUserSettings : EntraIdTestUser @@ -300,26 +331,30 @@ public class EntraIdTestUserSettings : EntraIdTestUser public class EntraIdTestUsersSource { private static object _LockInitList = new object(); + private static bool listInitialized = false; private static List _Users; public static List Users { get { - if (_Users != null) { return _Users; } + if (listInitialized) { return _Users; } lock (_LockInitList) { if (_Users != null) { return _Users; } _Users = new List(); - foreach (EntraIdTestUser user in GetTestData()) + foreach (EntraIdTestUser user in ReadDataSource()) { _Users.Add(user); } + listInitialized = true; Trace.TraceInformation($"{DateTime.Now:s} [{typeof(EntraIdTestUsersSource).Name}] Initialized List of {nameof(Users)} with {_Users.Count} items."); return _Users; } } } + private static Random RandomNumber = new Random(); + public static EntraIdTestUser AGuestUser => Users.FirstOrDefault(x => x.UserType == UserType.Guest); public static IEnumerable AllGuestUsers => Users.Where(x => x.UserType == UserType.Guest); @@ -349,7 +384,7 @@ public static List UsersWithSpecificSettings } } - public static IEnumerable GetTestData() + private static IEnumerable ReadDataSource() { string csvPath = UnitTestsHelper.DataFile_EntraId_TestUsers; DataTable dt = DataTable.New.ReadCsv(csvPath); @@ -365,5 +400,24 @@ public static IEnumerable GetTestData() yield return registrationData; } } + + public static IEnumerable GetSomeUsers(int count) + { + if (count > Users.Count) + { + count = Users.Count; + } + + List userIdxs = new List(count); + for (int i = 0; i < count; i++) + { + userIdxs.Add(RandomNumber.Next(0, Users.Count - 1)); + } + + foreach (int userIdx in userIdxs) + { + yield return Users[userIdx].Clone() as EntraIdTestUser; + } + } } } \ No newline at end of file diff --git a/Yvand.EntraCP/Yvand.EntraClaimsProvider/EntraIDEntityProvider.cs b/Yvand.EntraCP/Yvand.EntraClaimsProvider/EntraIDEntityProvider.cs index bd8c2e4..f8d4eb7 100644 --- a/Yvand.EntraCP/Yvand.EntraClaimsProvider/EntraIDEntityProvider.cs +++ b/Yvand.EntraCP/Yvand.EntraClaimsProvider/EntraIDEntityProvider.cs @@ -461,11 +461,11 @@ protected virtual async Task> QueryEntraIDTenantAsync(Oper } // List of groups that users must be member of, to be returned to SharePoint - string[] restrictSearchableUsersByGroupsRequestsId = null; - string restrictSearchableUsersByGroups = this.Settings.RestrictSearchableUsersByGroups; + string[] allowedGroupMembersOfGroupsRequestsId = null; + string allowedGroupsIDs = this.Settings.RestrictSearchableUsersByGroups; //restrictSearchableUsersByGroups = "c9a94341-89b5-4109-a501-2a14027b5bf0"; // testEntraCPGroup_005 - everyone member //restrictSearchableUsersByGroups = "cd5f135c-9fe5-4ec2-90d9-114e9ad2e236"; // testEntraCPGroup_004 - testEntraCPUser_001 and testEntraCPUser_010 members - if (!String.IsNullOrWhiteSpace(restrictSearchableUsersByGroups) && cachedTenantData.SearchableUsersId == null) + if (!String.IsNullOrWhiteSpace(allowedGroupsIDs) && cachedTenantData.SearchableUsersId == null) { await cachedTenantData.WriteDataLock.WaitAsync().ConfigureAwait(false); lockToWriteInCachedDataWasTaken = true; @@ -476,23 +476,25 @@ protected virtual async Task> QueryEntraIDTenantAsync(Oper } else { - string[] groupsId = restrictSearchableUsersByGroups.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - restrictSearchableUsersByGroupsRequestsId = new string[groupsId.Length]; + string[] groupsId = allowedGroupsIDs.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + allowedGroupMembersOfGroupsRequestsId = new string[groupsId.Length]; int groupIdx = 0; - foreach (string groupId in groupsId) + foreach (string allowedGroupId in groupsId) { - RequestInformation usersMembersOfGroupRequest = tenant.GraphService.Groups[groupId].Members.GraphUser.ToGetRequestInformation(conf => + RequestInformation allowedGroupMembersRequest = tenant.GraphService.Groups[allowedGroupId].Members.GraphUser.ToGetRequestInformation(conf => { conf.QueryParameters = new Microsoft.Graph.Groups.Item.Members.GraphUser.GraphUserRequestBuilder.GraphUserRequestBuilderGetQueryParameters { Select = new string[] { "Id" }, + // max items count per page is 999: https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0&tabs=http#optional-query-parameters + Top = 100, }; conf.Options = new List { retryHandlerOption, }; }); - restrictSearchableUsersByGroupsRequestsId[groupIdx] = await batchRequestContent.AddBatchRequestStepAsync(usersMembersOfGroupRequest).ConfigureAwait(false); + allowedGroupMembersOfGroupsRequestsId[groupIdx] = await batchRequestContent.AddBatchRequestStepAsync(allowedGroupMembersRequest).ConfigureAwait(false); groupIdx++; } } @@ -532,28 +534,39 @@ protected virtual async Task> QueryEntraIDTenantAsync(Oper } } - if (restrictSearchableUsersByGroupsRequestsId != null) + if (allowedGroupMembersOfGroupsRequestsId != null) { // only need 1 list that contains unique user ids cachedTenantData.SearchableUsersId = new List(); - foreach (string restrictSearchableUsersByGroupsRequestId in restrictSearchableUsersByGroupsRequestsId) + foreach (string allowedGroupMembersOfGroupRequestId in allowedGroupMembersOfGroupsRequestsId) { - HttpStatusCode restrictSearchableUsersByGroupsResponseStatus; - UserCollectionResponse restrictSearchableUsersByGroupsResponse = null; - if (requestsStatusInBatchResponse.TryGetValue(restrictSearchableUsersByGroupsRequestId, out restrictSearchableUsersByGroupsResponseStatus)) + HttpStatusCode allowedGroupMembersOfGroupResponseStatus; + //UserCollectionResponse restrictSearchableUsersByGroupsResponse = null; + if (requestsStatusInBatchResponse.TryGetValue(allowedGroupMembersOfGroupRequestId, out allowedGroupMembersOfGroupResponseStatus)) { - if (restrictSearchableUsersByGroupsResponseStatus == HttpStatusCode.OK) + if (allowedGroupMembersOfGroupResponseStatus == HttpStatusCode.OK) { - restrictSearchableUsersByGroupsResponse = await batchResponse.GetResponseByIdAsync(restrictSearchableUsersByGroupsRequestId).ConfigureAwait(false); - cachedTenantData.SearchableUsersId.AddRange(restrictSearchableUsersByGroupsResponse.Value.Where(x => !cachedTenantData.SearchableUsersId.Contains(x.Id)).Select(x => x.Id).ToList()); + UserCollectionResponse allowedGroupMembersOfGroupResponse = await batchResponse.GetResponseByIdAsync(allowedGroupMembersOfGroupRequestId).ConfigureAwait(false); + PageIterator allowedGroupMembersPageIterator = PageIterator.CreatePageIterator( + tenant.GraphService, + allowedGroupMembersOfGroupResponse, + (user) => + { + if (!cachedTenantData.SearchableUsersId.Contains(user.Id)) + { + cachedTenantData.SearchableUsersId.Add(user.Id); + } + return true; + }); + await allowedGroupMembersPageIterator.IterateAsync().ConfigureAwait(false); } - else if (restrictSearchableUsersByGroupsResponseStatus == HttpStatusCode.NotFound) + else if (allowedGroupMembersOfGroupResponseStatus == HttpStatusCode.NotFound) { Logger.Log($"[{ClaimsProviderName}] Request inside the batch to get the members of a group on tenant \"{tenant.Name}\" returned nothing (the group was not found).", TraceSeverity.Verbose, EventSeverity.Information, TraceCategory.Lookup); } else { - Logger.Log($"[{ClaimsProviderName}] Request inside the batch to get the members of a group on tenant \"{tenant.Name}\" returned unexpected status '{restrictSearchableUsersByGroupsResponseStatus}'", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Lookup); + Logger.Log($"[{ClaimsProviderName}] Request inside the batch to get the members of a group on tenant \"{tenant.Name}\" returned unexpected status '{allowedGroupMembersOfGroupResponseStatus}'", TraceSeverity.Unexpected, EventSeverity.Error, TraceCategory.Lookup); } } } From 2718c1fb10da819881718f68d287c4e16be8d111 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Mon, 22 Jul 2024 09:03:12 +0200 Subject: [PATCH 03/23] Update tests (#276) * start work * in progress * continue work * progress * work * rename types * Update UnitTestsHelper.cs * Update UnitTestsHelper.cs * Update UnitTestsHelper.cs * work --- .../BasicConfigurationTests.cs | 10 +- Yvand.EntraCP.Tests/BypassDirectoryTests.cs | 16 +- .../ClaimsProviderTestsBase.cs | 28 +- Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs | 24 +- .../FilterUsersBasedOnGroupsTests.cs | 24 +- Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs | 8 +- Yvand.EntraCP.Tests/RequireExactMatchTests.cs | 4 +- .../SecurityEnabledGroupsTests.cs | 8 +- Yvand.EntraCP.Tests/UnitTestsHelper.cs | 304 ++++++++---------- 9 files changed, 205 insertions(+), 221 deletions(-) diff --git a/Yvand.EntraCP.Tests/BasicConfigurationTests.cs b/Yvand.EntraCP.Tests/BasicConfigurationTests.cs index 34b7611..2907abc 100644 --- a/Yvand.EntraCP.Tests/BasicConfigurationTests.cs +++ b/Yvand.EntraCP.Tests/BasicConfigurationTests.cs @@ -20,8 +20,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } @@ -33,8 +33,8 @@ public void TestGroups(EntraIdTestGroup group) // TestSearchAndValidateForEntraIDGroup(group); //} - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -58,7 +58,7 @@ public override void TestAugmentationOfGoldUsersAgainstRandomGroups() [TestCase("testEntraCPUser_020")] public void DebugTestUser(string upnPrefix) { - EntraIdTestUser user = EntraIdTestUsersSource.Users.Find(x => x.UserPrincipalName.StartsWith(upnPrefix)); + TestUser user = TestEntitySourceManager.AllTestUsers.First(x => x.UserPrincipalName.StartsWith(upnPrefix)); base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/BypassDirectoryTests.cs b/Yvand.EntraCP.Tests/BypassDirectoryTests.cs index c2eb08b..0d3689c 100644 --- a/Yvand.EntraCP.Tests/BypassDirectoryTests.cs +++ b/Yvand.EntraCP.Tests/BypassDirectoryTests.cs @@ -26,8 +26,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); user.UserPrincipalName = user.DisplayName; @@ -36,8 +36,8 @@ public void TestUsers(EntraIdTestUser user) base.TestSearchAndValidateForEntraIDUser(user); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); group.Id = group.DisplayName; @@ -77,14 +77,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs index 8df4411..4c2e010 100644 --- a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs +++ b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs @@ -49,8 +49,8 @@ public string GroupIdentifierClaimType private object _LockVerifyIfCurrentUserShouldBeFound = new object(); private object _LockInitGroupsWhichUsersMustBeMemberOfAny = new object(); - private List _GroupsWhichUsersMustBeMemberOfAny; - protected List GroupsWhichUsersMustBeMemberOfAny + private List _GroupsWhichUsersMustBeMemberOfAny; + protected List GroupsWhichUsersMustBeMemberOfAny { get { @@ -58,15 +58,15 @@ protected List GroupsWhichUsersMustBeMemberOfAny lock (_LockInitGroupsWhichUsersMustBeMemberOfAny) { if (_GroupsWhichUsersMustBeMemberOfAny != null) { return _GroupsWhichUsersMustBeMemberOfAny; } - _GroupsWhichUsersMustBeMemberOfAny = new List(); + _GroupsWhichUsersMustBeMemberOfAny = new List(); string groupsWhichUsersMustBeMemberOfAny = Settings.RestrictSearchableUsersByGroups; if (!String.IsNullOrWhiteSpace(groupsWhichUsersMustBeMemberOfAny)) { string[] groupIds = groupsWhichUsersMustBeMemberOfAny.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string groupId in groupIds) { - EntraIdTestGroupSettings groupSettings = EntraIdTestGroupsSource.GroupsSettings.FirstOrDefault(x => x.Id == groupId); - if (groupSettings == null) { groupSettings = new EntraIdTestGroupSettings(); } + TestGroup groupSettings = TestEntitySourceManager.GroupsWithCustomSettings.FirstOrDefault(x => x.Id == groupId); + if (groupSettings == null) { groupSettings = new TestGroup(); } _GroupsWhichUsersMustBeMemberOfAny.Add(groupSettings); } } @@ -116,7 +116,7 @@ public virtual void CheckSettingsTest() } } - public void TestSearchAndValidateForEntraIDGroup(EntraIdTestGroup entity) + public void TestSearchAndValidateForEntraIDGroup(TestGroup entity) { string inputValue = entity.DisplayName; int expectedCount = 1; @@ -137,7 +137,7 @@ public void TestSearchAndValidateForEntraIDGroup(EntraIdTestGroup entity) TestValidationOperation(GroupIdentifierClaimType, entity.Id, shouldValidate); } - public void TestSearchAndValidateForEntraIDUser(EntraIdTestUser entity) + public void TestSearchAndValidateForEntraIDUser(TestUser entity) { int expectedCount = 1; string inputValue = entity.DisplayName; @@ -172,8 +172,8 @@ public void TestSearchAndValidateForEntraIDUser(EntraIdTestUser entity) if (!groupWithAllTestUsersAreMembersFound) { - EntraIdTestUserSettings userSettings = EntraIdTestUsersSource.UsersWithSpecificSettings.FirstOrDefault(x => String.Equals(x.UserPrincipalName, entity.UserPrincipalName, StringComparison.InvariantCultureIgnoreCase)); - if (userSettings == null) { userSettings = new EntraIdTestUserSettings(); } + TestUser userSettings = TestEntitySourceManager.UsersWithCustomSettings.FirstOrDefault(x => String.Equals(x.UserPrincipalName, entity.UserPrincipalName, StringComparison.InvariantCultureIgnoreCase)); + if (userSettings == null) { userSettings = new TestUser(); } if (!userSettings.IsMemberOfAllGroups) { shouldValidate = false; @@ -219,22 +219,22 @@ public void TestSearchAndValidateForEntraIDUser(EntraIdTestUser entity) public virtual void TestAugmentationOfGoldUsersAgainstRandomGroups() { Random rnd = new Random(); - int randomIdx = rnd.Next(0, EntraIdTestGroupsSource.Groups.Count - 1); + int randomIdx = rnd.Next(0, TestEntitySourceManager.AllTestGroups.Count - 1); Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOfGoldUsersAgainstRandomGroups: Get group in EntraIdTestGroupsSource.Groups at index {randomIdx}."); - EntraIdTestGroup randomGroup = null; + TestGroup randomGroup = null; try { - randomGroup = EntraIdTestGroupsSource.Groups[randomIdx]; + randomGroup = TestEntitySourceManager.AllTestGroups[randomIdx]; } catch (ArgumentOutOfRangeException) { - string errorMessage = $"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOfGoldUsersAgainstRandomGroups: Could not get group in EntraIdTestGroupsSource.Groups at index {randomIdx}. EntraIdTestGroupsSource.Groups has {EntraIdTestGroupsSource.Groups.Count} items."; + string errorMessage = $"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOfGoldUsersAgainstRandomGroups: Could not get group in EntraIdTestGroupsSource.Groups at index {randomIdx}. EntraIdTestGroupsSource.Groups has {TestEntitySourceManager.AllTestGroups.Count} items."; Trace.TraceError(errorMessage); throw new ArgumentOutOfRangeException(errorMessage); } bool shouldBeMember = Settings.FilterSecurityEnabledGroupsOnly && !randomGroup.SecurityEnabled ? false : true; - foreach (string userPrincipalName in EntraIdTestUsersSource.UsersWithSpecificSettings.Where(x => x.IsMemberOfAllGroups).Select(x => x.UserPrincipalName)) + foreach (string userPrincipalName in TestEntitySourceManager.UsersWithCustomSettings.Where(x => x.IsMemberOfAllGroups).Select(x => x.UserPrincipalName)) { TestAugmentationOperation(userPrincipalName, shouldBeMember, randomGroup.Id); } diff --git a/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs b/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs index f56b4b2..b9ad505 100644 --- a/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs +++ b/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs @@ -21,14 +21,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -60,14 +60,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -99,14 +99,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs b/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs index ecbb743..3e8647b 100644 --- a/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs +++ b/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs @@ -13,7 +13,7 @@ public class FilterUsersBasedOnSingleGroupTests : ClaimsProviderTestsBase public override void InitializeSettings() { base.InitializeSettings(); - Settings.RestrictSearchableUsersByGroups = EntraIdTestGroupsSource.GetSomeGroups(true, 1).ToArray()[0].Id; + Settings.RestrictSearchableUsersByGroups = TestEntitySourceManager.GetOneGroup(true).Id; base.ApplySettings(); } @@ -23,8 +23,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -34,7 +34,7 @@ public void TestUsers(EntraIdTestUser user) [TestCase("testEntraCPUser_020")] public void DebugTestUser(string upnPrefix) { - EntraIdTestUser user = EntraIdTestUsersSource.Users.Find(x => x.UserPrincipalName.StartsWith(upnPrefix)); + TestUser user = TestEntitySourceManager.AllTestUsers.Find(x => x.UserPrincipalName.StartsWith(upnPrefix)); base.TestSearchAndValidateForEntraIDUser(user); } #endif @@ -49,7 +49,9 @@ public override void InitializeSettings() base.InitializeSettings(); // Pick the Id of 18 (max possible) random groups, and set them in property RestrictSearchableUsersByGroups - Settings.RestrictSearchableUsersByGroups = String.Join(",", EntraIdTestGroupsSource.GetSomeGroups(true, 18).Select(x => x.Id).ToArray()); + Settings.RestrictSearchableUsersByGroups = String.Join(",", TestEntitySourceManager.GetSomeGroups(18, true).Select(x => x.Id).ToArray()); + //Settings.RestrictSearchableUsersByGroups = "3c1c6c1a-2565-4cfd-b5f8-8ec732f93077,3c98541c-9601-47c0-aeea-fc0679b9d756,807c95cd-88de-49d9-a06e-12ce2329dfb7,807c95cd-88de-49d9-a06e-12ce2329dfb7,1beb24dd-0fae-46cb-b321-dd0baf5c9ecc,01572e9f-4a9a-4dd1-9314-05972d87d1c2,89d4f192-8eb0-4011-ada7-4a1d4f678b1c,bdd53ff1-866c-442b-b6d5-ac43b4306aa7,2d407401-192c-4a25-9f0e-3693cfad6f27,1c607c55-f1a0-408c-ae52-306cd89de742,1090383f-7ea5-4a16-9ba8-0551a061d7f9,874b1dcf-aa82-428a-b107-b71a09c3d452,1090383f-7ea5-4a16-9ba8-0551a061d7f9,1831bd90-e413-4b86-a8ab-5d26d8a75498,04ec1e1c-196d-4b85-85b2-c3b982644114,043e997e-0b2c-412c-b8f0-13253251569c,40d53a73-130b-48e1-946b-5fec5ec35d4f,3c98541c-9601-47c0-aeea-fc0679b9d756"; + //Settings.RestrictSearchableUsersByGroups = "3c98541c-9601-47c0-aeea-fc0679b9d756"; Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Set property RestrictSearchableUsersByGroups: \"{Settings.RestrictSearchableUsersByGroups}\"."); base.ApplySettings(); } @@ -60,8 +62,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } @@ -71,7 +73,7 @@ public void TestUsers(EntraIdTestUser user) [TestCase("testEntraCPUser_020")] public void DebugTestUser(string upnPrefix) { - EntraIdTestUser user = EntraIdTestUsersSource.Users.Find(x => x.UserPrincipalName.StartsWith(upnPrefix)); + TestUser user = TestEntitySourceManager.FindUser(upnPrefix); base.TestSearchAndValidateForEntraIDUser(user); } #endif @@ -85,7 +87,7 @@ public class DebugFilterUsersBasedOnMultipleGroupsTests : ClaimsProviderTestsBas public override void InitializeSettings() { base.InitializeSettings(); - Settings.RestrictSearchableUsersByGroups = String.Join(",", EntraIdTestGroupsSource.GetSomeGroups(true, 18).Select(x => x.Id).ToArray()); + Settings.RestrictSearchableUsersByGroups = String.Join(",", TestEntitySourceManager.GetSomeGroups(18, true).Select(x => x.Id).ToArray()); Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Set property RestrictSearchableUsersByGroups: \"{Settings.RestrictSearchableUsersByGroups}\"."); base.ApplySettings(); } @@ -94,14 +96,14 @@ public override void InitializeSettings() [TestCase("testEntraCPUser_020")] public void DebugTestUser(string upnPrefix) { - EntraIdTestUser user = EntraIdTestUsersSource.Users.Find(x => x.UserPrincipalName.StartsWith(upnPrefix)); + TestUser user = TestEntitySourceManager.FindUser(upnPrefix); base.TestSearchAndValidateForEntraIDUser(user); } [Test] public void DebugGuestUser() { - EntraIdTestUser user = EntraIdTestUsersSource.Users.Find(x => x.Mail.StartsWith("testEntraCPGuestUser_001")); + TestUser user = TestEntitySourceManager.AllTestUsers.Find(x => x.Mail.StartsWith("testEntraCPGuestUser_001")); base.TestSearchAndValidateForEntraIDUser(user); } } diff --git a/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs b/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs index 6bb3e3e..0b4984b 100644 --- a/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs +++ b/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs @@ -24,14 +24,14 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { base.TestSearchAndValidateForEntraIDUser(user); } diff --git a/Yvand.EntraCP.Tests/RequireExactMatchTests.cs b/Yvand.EntraCP.Tests/RequireExactMatchTests.cs index f0b7f7d..aa156c2 100644 --- a/Yvand.EntraCP.Tests/RequireExactMatchTests.cs +++ b/Yvand.EntraCP.Tests/RequireExactMatchTests.cs @@ -21,8 +21,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestUsersSource), nameof(EntraIdTestUsersSource.GetSomeUsers), new object[] { UnitTestsHelper.MaxNumberOfUsersToTest })] - public void TestUsers(EntraIdTestUser user) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { // Input is not the full UPN value: it should not return any result TestSearchOperation(user.UserPrincipalName.Substring(0, 5), 0, String.Empty); diff --git a/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs b/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs index 57163ac..0f395c6 100644 --- a/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs +++ b/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs @@ -24,8 +24,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } @@ -57,8 +57,8 @@ public override void CheckSettingsTest() base.CheckSettingsTest(); } - [Test, TestCaseSource(typeof(EntraIdTestGroupsSource), nameof(EntraIdTestGroupsSource.GetSomeGroups), new object[] { true, UnitTestsHelper.MaxNumberOfGroupsToTest })] - public void TestGroups(EntraIdTestGroup group) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { TestSearchAndValidateForEntraIDGroup(group); } diff --git a/Yvand.EntraCP.Tests/UnitTestsHelper.cs b/Yvand.EntraCP.Tests/UnitTestsHelper.cs index 51dbe68..ac486ad 100644 --- a/Yvand.EntraCP.Tests/UnitTestsHelper.cs +++ b/Yvand.EntraCP.Tests/UnitTestsHelper.cs @@ -2,7 +2,6 @@ using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; using Microsoft.SharePoint.Administration.Claims; -using Microsoft.Web.Hosting.Administration; using Newtonsoft.Json; using NUnit.Framework; using System; @@ -11,8 +10,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Security.Claims; using Yvand.EntraClaimsProvider.Configuration; namespace Yvand.EntraClaimsProvider.Tests @@ -37,8 +34,7 @@ public class UnitTestsHelper public static string DataFile_EntraId_TestGroups => TestContext.Parameters["DataFile_EntraId_TestGroups"]; public static string TestUsersAccountNamePrefix => TestContext.Parameters["UserAccountNamePrefix"]; public static string TestGroupsAccountNamePrefix => TestContext.Parameters["GroupAccountNamePrefix"]; - public const int MaxNumberOfUsersToTest = 100; - public const int MaxNumberOfGroupsToTest = 100; + static TextWriterTraceListener Logger { get; set; } public static EntraIDProviderConfiguration PersistedConfiguration; private static IEntraIDProviderSettings OriginalSettings; @@ -187,237 +183,223 @@ public static void Cleanup() } } - public class EntraIdTestGroup : ICloneable + public abstract class TestEntity : ICloneable { public string Id; public string DisplayName; - public string GroupType; - public bool SecurityEnabled = true; public object Clone() { return this.MemberwiseClone(); } + + public abstract void SetEntityFromDataSourceRow(Row row); + } + + public class TestUser : TestEntity + { + public string UserPrincipalName; + public UserType UserType; + public string Mail; + public string GivenName; + public bool IsMemberOfAllGroups = false; + + public override void SetEntityFromDataSourceRow(Row row) + { + Id = row["id"]; + DisplayName = row["displayName"]; + UserPrincipalName = row["userPrincipalName"]; + UserType = String.Equals(row["userType"], ClaimsProviderConstants.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase) ? UserType.Member : UserType.Guest; + Mail = row["mail"]; + GivenName = row["givenName"]; + } } - public class EntraIdTestGroupSettings : EntraIdTestGroup + public class TestGroup : TestEntity { + public string GroupType; + public bool SecurityEnabled = true; public bool AllTestUsersAreMembers = false; + + public override void SetEntityFromDataSourceRow(Row row) + { + Id = row["id"]; + DisplayName = row["displayName"]; + GroupType = row["groupType"]; + SecurityEnabled = Convert.ToBoolean(row["SecurityEnabled"]); + } } - public class EntraIdTestGroupsSource + public enum UserType + { + Member, + Guest + } + + public class TestEntitySource where T : TestEntity, new() { - private static object _LockInitGroupsList = new object(); - private static bool listInitialized = false; - private static List _Groups; - public static List Groups + private object _LockInitEntitiesList = new object(); + private List _Entities; + public List Entities { get { - if (listInitialized) { return _Groups; } - lock (_LockInitGroupsList) + if (_Entities != null) { return _Entities; } + lock (_LockInitEntitiesList) { - if (_Groups != null) { return _Groups; } - _Groups = new List(); - foreach (EntraIdTestGroup group in ReadDataSource(false)) + if (_Entities != null) { return _Entities; } + _Entities = new List(); + foreach (T entity in ReadDataSource()) { - _Groups.Add(group); + _Entities.Add(entity); } - listInitialized = true; - Trace.TraceInformation($"{DateTime.Now:s} [{typeof(EntraIdTestGroupsSource).Name}] Initialized List of {nameof(Groups)} with {_Groups.Count} items."); - return _Groups; + Trace.TraceInformation($"{DateTime.Now:s} [{typeof(T).Name}] Initialized List of {nameof(Entities)} with {Entities.Count} items."); + return _Entities; } } } - private static Random RandomNumber = new Random(); + private Random RandomNumber = new Random(); + private string DataSourceFilePath; - private static object _LockInitGroupsSettingsList = new object(); - private static List _GroupsSettings; - public static List GroupsSettings + public TestEntitySource(string dataSourceFilePath) { - get - { - if (_GroupsSettings != null) { return _GroupsSettings; } - lock (_LockInitGroupsSettingsList) - { - if (_GroupsSettings != null) { return _GroupsSettings; } - _GroupsSettings = new List - { - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}001" , SecurityEnabled = false, AllTestUsersAreMembers = true}, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}005" , SecurityEnabled = true, AllTestUsersAreMembers = true }, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}008" , SecurityEnabled = false, AllTestUsersAreMembers = false }, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}018" , SecurityEnabled = false, AllTestUsersAreMembers = true }, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}025" , SecurityEnabled = true, AllTestUsersAreMembers = true }, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}028" , SecurityEnabled = false, AllTestUsersAreMembers = false, }, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}038" , SecurityEnabled = false, AllTestUsersAreMembers = true, }, - new EntraIdTestGroupSettings { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}048" , SecurityEnabled = false, AllTestUsersAreMembers = false, }, - }; - foreach (EntraIdTestGroupSettings groupsSetting in _GroupsSettings) - { - groupsSetting.Id = Groups.First(x => x.DisplayName == groupsSetting.DisplayName).Id; - } - Trace.TraceInformation($"{DateTime.Now:s} [{typeof(EntraIdTestGroupSettings).Name}] Initialized List of {nameof(GroupsSettings)} with {_GroupsSettings.Count} items."); - } - return _GroupsSettings; - } + DataSourceFilePath = dataSourceFilePath; } - private static IEnumerable ReadDataSource(bool securityEnabledGroupsOnly = false) + private IEnumerable ReadDataSource() { - string csvPath = UnitTestsHelper.DataFile_EntraId_TestGroups; - DataTable dt = DataTable.New.ReadCsv(csvPath); + DataTable dt = DataTable.New.ReadCsv(DataSourceFilePath); foreach (Row row in dt.Rows) { - var registrationData = new EntraIdTestGroup(); - registrationData.Id = row["id"]; - registrationData.DisplayName = row["displayName"]; - registrationData.GroupType = row["groupType"]; - registrationData.SecurityEnabled = Convert.ToBoolean(row["SecurityEnabled"]); - if (securityEnabledGroupsOnly && !registrationData.SecurityEnabled) - { - continue; - } - yield return registrationData; + T entity = new T(); + entity.SetEntityFromDataSourceRow(row); + yield return entity; } } - public static IEnumerable GetSomeGroups(bool securityEnabledGroupsOnly, int count) + public IEnumerable GetSomeEntities(int count, Func filter = null) { - if (count > Groups.Count) + if (count > Entities.Count) { - count = Groups.Count; + count = Entities.Count; } - List userIdxs = new List(count); + List entitiesIdxs = new List(count); for (int i = 0; i < count; i++) { - userIdxs.Add(RandomNumber.Next(0, Groups.Count - 1)); + entitiesIdxs.Add(RandomNumber.Next(0, Entities.Where(filter ?? (x => true)).Count() - 1)); } - foreach (int userIdx in userIdxs) + foreach (int userIdx in entitiesIdxs) { - yield return Groups[userIdx].Clone() as EntraIdTestGroup; + yield return Entities[userIdx].Clone() as T; } } } - public enum UserType - { - Member, - Guest - } - - public class EntraIdTestUser : ICloneable + public class TestEntitySourceManager { - public string Id; - public string DisplayName; - public string UserPrincipalName; - public UserType UserType; - public string Mail; - public string GivenName; - - public object Clone() + private static TestUser[] UsersWithCustomSettingsDefinition = new[] { - return this.MemberwiseClone(); - } - } - - public class EntraIdTestUserSettings : EntraIdTestUser - { - public bool IsMemberOfAllGroups = false; - } - - public class EntraIdTestUsersSource - { - private static object _LockInitList = new object(); - private static bool listInitialized = false; - private static List _Users; - public static List Users + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}001@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}010@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}011@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}012@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}013@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}014@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}015@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, + }; + private static TestGroup[] GroupsWithCustomSettingsDefinition = new[] + { + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}001" , SecurityEnabled = false, AllTestUsersAreMembers = true}, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}005" , SecurityEnabled = true, AllTestUsersAreMembers = true }, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}008" , SecurityEnabled = false, AllTestUsersAreMembers = false }, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}018" , SecurityEnabled = false, AllTestUsersAreMembers = true }, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}025" , SecurityEnabled = true, AllTestUsersAreMembers = true }, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}028" , SecurityEnabled = false, AllTestUsersAreMembers = false, }, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}038" , SecurityEnabled = false, AllTestUsersAreMembers = true, }, + new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}048" , SecurityEnabled = false, AllTestUsersAreMembers = false, }, + }; + + private static object _LockInitUsersWithCustomSettings = new object(); + private static List _UsersWithCustomSettings; + public static List UsersWithCustomSettings { get { - if (listInitialized) { return _Users; } - lock (_LockInitList) + if (_UsersWithCustomSettings != null) { return _UsersWithCustomSettings; } + lock (_LockInitGroupsWithCustomSettings) { - if (_Users != null) { return _Users; } - _Users = new List(); - foreach (EntraIdTestUser user in ReadDataSource()) + if (_UsersWithCustomSettings != null) { return _UsersWithCustomSettings; } + _UsersWithCustomSettings = new List(); + foreach (TestUser userDefinition in UsersWithCustomSettingsDefinition) { - _Users.Add(user); + TestUser user = AllTestUsers.First(x => String.Equals(x.UserPrincipalName, userDefinition.UserPrincipalName, StringComparison.OrdinalIgnoreCase)); + user.IsMemberOfAllGroups = userDefinition.IsMemberOfAllGroups; + _UsersWithCustomSettings.Add(user); } - listInitialized = true; - Trace.TraceInformation($"{DateTime.Now:s} [{typeof(EntraIdTestUsersSource).Name}] Initialized List of {nameof(Users)} with {_Users.Count} items."); - return _Users; } + return _UsersWithCustomSettings; } } - private static Random RandomNumber = new Random(); - - public static EntraIdTestUser AGuestUser => Users.FirstOrDefault(x => x.UserType == UserType.Guest); - public static IEnumerable AllGuestUsers => Users.Where(x => x.UserType == UserType.Guest); - - private static object _LockInitUsersWithSpecificSettingsList = new object(); - private static List _UsersWithSpecificSettings; - public static List UsersWithSpecificSettings + private static object _LockInitGroupsWithCustomSettings = new object(); + private static List _GroupsWithCustomSettings; + public static List GroupsWithCustomSettings { get { - if (_UsersWithSpecificSettings != null) { return _UsersWithSpecificSettings; } - lock (_LockInitUsersWithSpecificSettingsList) + if (_GroupsWithCustomSettings != null) { return _GroupsWithCustomSettings; } + lock (_LockInitGroupsWithCustomSettings) { - if (_UsersWithSpecificSettings != null) { return _UsersWithSpecificSettings; } - _UsersWithSpecificSettings = new List + if (_GroupsWithCustomSettings != null) { return _GroupsWithCustomSettings; } + _GroupsWithCustomSettings = new List(); + foreach (TestGroup groupDefinition in GroupsWithCustomSettingsDefinition) { - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}001@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}010@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}011@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}012@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}013@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}014@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new EntraIdTestUserSettings { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}015@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - }; + TestGroup group = AllTestGroups.First(x => x.DisplayName == groupDefinition.DisplayName); + group.SecurityEnabled = groupDefinition.SecurityEnabled; + group.AllTestUsersAreMembers = groupDefinition.AllTestUsersAreMembers; + _GroupsWithCustomSettings.Add(group); + } } - Trace.TraceInformation($"{DateTime.Now:s} [{typeof(EntraIdTestUserSettings).Name}] Initialized List of {nameof(UsersWithSpecificSettings)} with {_UsersWithSpecificSettings.Count} items."); - return _UsersWithSpecificSettings; + return _GroupsWithCustomSettings; } } - private static IEnumerable ReadDataSource() + private static TestEntitySource TestUsersSource = new TestEntitySource(UnitTestsHelper.DataFile_EntraId_TestUsers); + public static List AllTestUsers { - string csvPath = UnitTestsHelper.DataFile_EntraId_TestUsers; - DataTable dt = DataTable.New.ReadCsv(csvPath); - foreach (Row row in dt.Rows) - { - var registrationData = new EntraIdTestUser(); - registrationData.Id = row["id"]; - registrationData.DisplayName = row["displayName"]; - registrationData.UserPrincipalName = row["userPrincipalName"]; - registrationData.UserType = String.Equals(row["userType"], ClaimsProviderConstants.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase) ? UserType.Member : UserType.Guest; - registrationData.Mail = row["mail"]; - registrationData.GivenName = row["givenName"]; - yield return registrationData; - } + get => TestUsersSource.Entities; } + private static TestEntitySource TestGroupsSource = new TestEntitySource(UnitTestsHelper.DataFile_EntraId_TestGroups); + public static List AllTestGroups + { + get => TestGroupsSource.Entities; + } + public const int MaxNumberOfUsersToTest = 100; + public const int MaxNumberOfGroupsToTest = 100; - public static IEnumerable GetSomeUsers(int count) + public static IEnumerable GetSomeUsers(int count) { - if (count > Users.Count) - { - count = Users.Count; - } + return TestUsersSource.GetSomeEntities(count, null); + } - List userIdxs = new List(count); - for (int i = 0; i < count; i++) - { - userIdxs.Add(RandomNumber.Next(0, Users.Count - 1)); - } + public static TestUser FindUser(string upnPrefix) + { + return TestUsersSource.Entities.First(x => x.UserPrincipalName.StartsWith(upnPrefix)).Clone() as TestUser; + } - foreach (int userIdx in userIdxs) - { - yield return Users[userIdx].Clone() as EntraIdTestUser; - } + public static IEnumerable GetSomeGroups(int count, bool securityEnabledOnly) + { + Func securityEnabledOnlyFilter = x => x.SecurityEnabled == securityEnabledOnly; + return TestGroupsSource.GetSomeEntities(count, securityEnabledOnlyFilter); + } + + public static TestGroup GetOneGroup(bool securityEnabledOnly) + { + Func securityEnabledOnlyFilter = x => x.SecurityEnabled == securityEnabledOnly; + return TestGroupsSource.GetSomeEntities(1, securityEnabledOnlyFilter).First(); } } } \ No newline at end of file From 6bac3a05bfadcc7eef29c4c4983f71f3db048fd6 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 23 Jul 2024 10:02:43 +0200 Subject: [PATCH 04/23] Add sample of a custom claims provider (#278) * publish sample * Add README files * update README --- CHANGELOG.md | 1 + .../ADMIN/SharePointProjectItem.spdata | 4 + .../CustomClaimsProvider.csproj | 139 ++++++++++++++++++ .../CustomClaimsProvider.sln | 27 ++++ .../CustomClaimsProvider/EntraCP_Custom.cs | 37 +++++ ...EntraCP.Custom.ClaimsProvider.Template.xml | 3 + .../EntraCP.Custom.ClaimsProvider.feature | 2 + .../EntraCP.Custom.EventReceiver.cs | 74 ++++++++++ .../Package/Package.Template.xml | 3 + .../Package/Package.package | 47 ++++++ .../Properties/AssemblyInfo.cs | 38 +++++ samples/CustomClaimsProvider/README.md | 5 + samples/README.md | 4 + samples/Yvand.EntraCP.dll | Bin 0 -> 149504 bytes 14 files changed, 384 insertions(+) create mode 100644 samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata create mode 100644 samples/CustomClaimsProvider/CustomClaimsProvider.csproj create mode 100644 samples/CustomClaimsProvider/CustomClaimsProvider.sln create mode 100644 samples/CustomClaimsProvider/EntraCP_Custom.cs create mode 100644 samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml create mode 100644 samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature create mode 100644 samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs create mode 100644 samples/CustomClaimsProvider/Package/Package.Template.xml create mode 100644 samples/CustomClaimsProvider/Package/Package.package create mode 100644 samples/CustomClaimsProvider/Properties/AssemblyInfo.cs create mode 100644 samples/CustomClaimsProvider/README.md create mode 100644 samples/README.md create mode 100644 samples/Yvand.EntraCP.dll diff --git a/CHANGELOG.md b/CHANGELOG.md index 7834e22..622ef5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Ensure that all group members are retrieved when only users members of specified groups can be found in SharePoint * Update the script that provisions tenant with test users and groups, to be more reliable and provision 999 users (instead of 50), so tests are more realistics * Improve tests +* Publish a sample project that developers can use to create a custom version of EntraCP, for specific needs ## EntraCP v26.0.20240627.35 enhancements & bug-fixes - Published in June 27, 2024 diff --git a/samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata b/samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata new file mode 100644 index 0000000..42b38d5 --- /dev/null +++ b/samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/CustomClaimsProvider/CustomClaimsProvider.csproj b/samples/CustomClaimsProvider/CustomClaimsProvider.csproj new file mode 100644 index 0000000..ab675b3 --- /dev/null +++ b/samples/CustomClaimsProvider/CustomClaimsProvider.csproj @@ -0,0 +1,139 @@ + + + + Debug + AnyCPU + {CC278266-3F09-4908-BCE8-725D2AA9153E} + Library + Properties + CustomClaimsProvider + CustomClaimsProvider + v4.8 + 19.0 + 512 + {C1CDDADD-2546-481F-9697-4EA41081F2FC};{14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + 14.1 + False + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + x64 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + key.snk + + + + + + + False + ..\Yvand.EntraCP.dll + + + + + + EntraCP.Custom.ClaimsProvider.feature + + + + + + {956d496d-b13e-4306-9550-b8f56d869023} + + + {f3dbfba8-995d-41a4-a06b-b3f188a81106} + + + + {034f2ce9-76ca-4b10-a136-85143ef303c9} + + + Package.package + + + + + EntraCP.Custom.ClaimsProvider.feature + + + + + 1.12.0 + + + 5.56.0 + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\gacutil.exe" /f /i "$(TargetPath)" +copy /Y "$(TargetDir)Azure.Core.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Azure.Identity.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Bcl.AsyncInterfaces.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Graph.Core.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Graph.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Identity.Client.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Identity.Client.Extensions.Msal.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.IdentityModel.Abstractions.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.IdentityModel.JsonWebTokens.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.IdentityModel.Logging.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.IdentityModel.Protocols.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.IdentityModel.Protocols.OpenIdConnect.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.IdentityModel.Tokens.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Microsoft.Kiota.Abstractions.dll" $(ProjectDir)\bin +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)Std.UriTemplate.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Buffers.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.ClientModel.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Diagnostics.DiagnosticSource.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.IdentityModel.Tokens.Jwt.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.IO.FileSystem.AccessControl.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Memory.Data.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Memory.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Net.Http.WinHttpHandler.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Numerics.Vectors.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Runtime.CompilerServices.Unsafe.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Security.AccessControl.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Security.Cryptography.ProtectedData.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Security.Principal.Windows.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Text.Encodings.Web.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Text.Json.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.Threading.Tasks.Extensions.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)System.ValueTuple.dll" $(ProjectDir)\bin +copy /Y "$(TargetDir)Yvand.EntraCP.dll" $(ProjectDir)\bin + + \ No newline at end of file diff --git a/samples/CustomClaimsProvider/CustomClaimsProvider.sln b/samples/CustomClaimsProvider/CustomClaimsProvider.sln new file mode 100644 index 0000000..b6bde26 --- /dev/null +++ b/samples/CustomClaimsProvider/CustomClaimsProvider.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35013.160 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomClaimsProvider", "CustomClaimsProvider.csproj", "{CC278266-3F09-4908-BCE8-725D2AA9153E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CC278266-3F09-4908-BCE8-725D2AA9153E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC278266-3F09-4908-BCE8-725D2AA9153E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC278266-3F09-4908-BCE8-725D2AA9153E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {CC278266-3F09-4908-BCE8-725D2AA9153E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC278266-3F09-4908-BCE8-725D2AA9153E}.Release|Any CPU.Build.0 = Release|Any CPU + {CC278266-3F09-4908-BCE8-725D2AA9153E}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {75E70229-6D16-4FD1-B776-1D3CF6994E65} + EndGlobalSection +EndGlobal diff --git a/samples/CustomClaimsProvider/EntraCP_Custom.cs b/samples/CustomClaimsProvider/EntraCP_Custom.cs new file mode 100644 index 0000000..e994989 --- /dev/null +++ b/samples/CustomClaimsProvider/EntraCP_Custom.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Yvand.EntraClaimsProvider; +using Yvand.EntraClaimsProvider.Configuration; + +namespace CustomClaimsProvider +{ + public class EntraCP_Custom : EntraCP + { + /// + /// Sets the name of the claims provider, also set in (Get-SPTrustedIdentityTokenIssuer).ClaimProviderName property + /// + public new const string ClaimsProviderName = "EntraCP_Custom"; + + /// + /// Do not remove or change this property + /// + public override string Name => ClaimsProviderName; + + public EntraCP_Custom(string displayName) : base(displayName) + { + } + + public override IEntraIDProviderSettings GetSettings() + { + ClaimsProviderSettings settings = ClaimsProviderSettings.GetDefaultSettings(ClaimsProviderName); + EntraIDTenant tenant = new EntraIDTenant + { + AzureCloud = AzureCloudName.AzureGlobal, + Name = "TENANTNAME.onmicrosoft.com", + ClientId = "CLIENTID", + ClientSecret = "CLIENTSECRET", + }; + settings.EntraIDTenants = new List() { tenant }; + return settings; + } + } +} diff --git a/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml b/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml new file mode 100644 index 0000000..c27273d --- /dev/null +++ b/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature b/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature new file mode 100644 index 0000000..2736cb5 --- /dev/null +++ b/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs b/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs new file mode 100644 index 0000000..12a70a3 --- /dev/null +++ b/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs @@ -0,0 +1,74 @@ +using Microsoft.SharePoint; +using Microsoft.SharePoint.Administration; +using Microsoft.SharePoint.Administration.Claims; +using System; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using Yvand.EntraClaimsProvider.Logging; + +namespace CustomClaimsProvider.Features +{ + /// + /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade. + /// + /// + /// The GUID attached to this class may be used during packaging and should not be modified. + /// + + [Guid("09a62d43-b866-4ff1-bd8b-a194c6dcf80c")] + public class EntraCPCustomEventReceiver : SPClaimProviderFeatureReceiver + { + public override string ClaimProviderAssembly => typeof(EntraCP_Custom).Assembly.FullName; + + public override string ClaimProviderDescription => EntraCP_Custom.ClaimsProviderName; + + public override string ClaimProviderDisplayName => EntraCP_Custom.ClaimsProviderName; + + public override string ClaimProviderType => typeof(EntraCP_Custom).FullName; + + public override void FeatureActivated(SPFeatureReceiverProperties properties) + { + ExecBaseFeatureActivated(properties); + } + + private void ExecBaseFeatureActivated(Microsoft.SharePoint.SPFeatureReceiverProperties properties) + { + // Wrapper function for base FeatureActivated. + // Used because base keywork can lead to unverifiable code inside lambda expression + base.FeatureActivated(properties); + SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate () + { + try + { + Logger svc = Logger.Local; + Logger.Log($"[{EntraCP_Custom.ClaimsProviderName}] Activating farm-scoped feature for claims provider \"{EntraCP_Custom.ClaimsProviderName}\"", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + } + catch (Exception ex) + { + Logger.LogException((string)EntraCP_Custom.ClaimsProviderName, $"activating farm-scoped feature for claims provider \"{EntraCP_Custom.ClaimsProviderName}\"", TraceCategory.Configuration, ex); + } + }); + } + + public override void FeatureUninstalling(SPFeatureReceiverProperties properties) + { + } + + public override void FeatureDeactivating(SPFeatureReceiverProperties properties) + { + SPSecurity.RunWithElevatedPrivileges((SPSecurity.CodeToRunElevated)delegate () + { + try + { + Logger.Log($"[{EntraCP_Custom.ClaimsProviderName}] Deactivating farm-scoped feature for claims provider \"{EntraCP_Custom.ClaimsProviderName}\": Removing claims provider from the farm (but not its configuration)", TraceSeverity.High, EventSeverity.Information, TraceCategory.Configuration); + base.RemoveClaimProvider((string)EntraCP_Custom.ClaimsProviderName); + } + catch (Exception ex) + { + Logger.LogException((string)EntraCP_Custom.ClaimsProviderName, $"deactivating farm-scoped feature for claims provider \"{EntraCP_Custom.ClaimsProviderName}\"", TraceCategory.Configuration, ex); + } + }); + } + + } +} diff --git a/samples/CustomClaimsProvider/Package/Package.Template.xml b/samples/CustomClaimsProvider/Package/Package.Template.xml new file mode 100644 index 0000000..640ff0f --- /dev/null +++ b/samples/CustomClaimsProvider/Package/Package.Template.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/samples/CustomClaimsProvider/Package/Package.package b/samples/CustomClaimsProvider/Package/Package.package new file mode 100644 index 0000000..b012b45 --- /dev/null +++ b/samples/CustomClaimsProvider/Package/Package.package @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/CustomClaimsProvider/Properties/AssemblyInfo.cs b/samples/CustomClaimsProvider/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..bebe4c0 --- /dev/null +++ b/samples/CustomClaimsProvider/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EntraCP.Custom")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EntraCP.Custom")] +[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("cc278266-3f09-4908-bce8-725d2aa9153e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/samples/CustomClaimsProvider/README.md b/samples/CustomClaimsProvider/README.md new file mode 100644 index 0000000..94bb58c --- /dev/null +++ b/samples/CustomClaimsProvider/README.md @@ -0,0 +1,5 @@ +# Sample with a hard-coded configuration + +This project shows how to create a claims provider that inherits EntraCP. It uses a simple, hard-coded configuration to specify the tenant. + +Do NOT deploy this solution in a SharePoint farm that already has EntraCP deployed, unless both use **exactly** the same versions of NuGet dependencies. If they use different versions, that may cause errors when loading DLLs, due to mismatches with the assembly bindings in the machine.config file. diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..e0a0728 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,4 @@ +# Sample projects for developers + +This folder contains Visual Studio projects that developers can use to create their custom clainms providers based on EntraCP. +This is useful only for specific needs. diff --git a/samples/Yvand.EntraCP.dll b/samples/Yvand.EntraCP.dll new file mode 100644 index 0000000000000000000000000000000000000000..89a103d52eb2519bd946ba8f865d31d0d952e032 GIT binary patch literal 149504 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 Z$NMrc0Iknuw?Ddf8D;;m$p3d3_ Date: Tue, 23 Jul 2024 12:07:58 +0200 Subject: [PATCH 05/23] Add a Bruno collection to replay requests to Microsoft Graph (#280) * initial commit * rename files * set auth to inherit * remove tests * update doc * Update README.md * Update get access token.bru * Update CHANGELOG.md --- CHANGELOG.md | 1 + graph-requests-samples/.env.example | 3 ++ graph-requests-samples/.gitignore | 3 ++ graph-requests-samples/README.md | 13 ++++++ ...ay search users and groups using batch.bru | 36 +++++++++++++++ .../1- get user by userprincipalname.bru | 23 ++++++++++ .../augmentation/2- get group membership.bru | 15 +++++++ .../authentication/get access token.bru | 22 ++++++++++ graph-requests-samples/bruno.json | 9 ++++ graph-requests-samples/collection.bru | 15 +++++++ .../environments/entracp-environment.bru | 7 +++ .../search/list members of a group.bru | 19 ++++++++ ...ay search users and groups using batch.bru | 36 +++++++++++++++ .../search/run a batch request.bru | 44 +++++++++++++++++++ .../search/search groups.bru | 16 +++++++ .../search/search users copy.bru | 27 ++++++++++++ .../search/search users.bru | 20 +++++++++ .../search/show a group.bru | 19 ++++++++ .../list extension properties.bru | 11 +++++ ...msmappingpolicy to a service principal.bru | 18 ++++++++ ...assigned to a service principal copy 2.bru | 11 +++++ ...aimsmappingpolicy for saml claims copy.bru | 24 ++++++++++ .../delete a claimsmappingpolicy.bru | 24 ++++++++++ ...st claimsmappingpolicies in the tenant.bru | 11 +++++ ...aimsmappingpolicy for saml claims copy.bru | 24 ++++++++++ ...s with their extension attribute value.bru | 21 +++++++++ ...r to set the extension attribute value.bru | 22 ++++++++++ .../unit tests/list test groups.bru | 20 +++++++++ .../unit tests/list test users.bru | 21 +++++++++ .../unit tests/show group by id.bru | 19 ++++++++ .../unit tests/show group members.bru | 19 ++++++++ .../validation/validate group.bru | 17 +++++++ .../validation/validate user.bru | 21 +++++++++ 33 files changed, 611 insertions(+) create mode 100644 graph-requests-samples/.env.example create mode 100644 graph-requests-samples/.gitignore create mode 100644 graph-requests-samples/README.md create mode 100644 graph-requests-samples/Search/replay search users and groups using batch.bru create mode 100644 graph-requests-samples/augmentation/1- get user by userprincipalname.bru create mode 100644 graph-requests-samples/augmentation/2- get group membership.bru create mode 100644 graph-requests-samples/authentication/get access token.bru create mode 100644 graph-requests-samples/bruno.json create mode 100644 graph-requests-samples/collection.bru create mode 100644 graph-requests-samples/environments/entracp-environment.bru create mode 100644 graph-requests-samples/search/list members of a group.bru create mode 100644 graph-requests-samples/search/replay search users and groups using batch.bru create mode 100644 graph-requests-samples/search/run a batch request.bru create mode 100644 graph-requests-samples/search/search groups.bru create mode 100644 graph-requests-samples/search/search users copy.bru create mode 100644 graph-requests-samples/search/search users.bru create mode 100644 graph-requests-samples/search/show a group.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/extension properties/list extension properties.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/service principal policies/assign a claimsmappingpolicy to a service principal.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/service principal policies/list claimsmappingpolicies assigned to a service principal copy 2.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/tenant policies/create a claimsmappingpolicy for saml claims copy.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/tenant policies/delete a claimsmappingpolicy.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/tenant policies/list claimsmappingpolicies in the tenant.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/tenant policies/update a claimsmappingpolicy for saml claims copy.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/users/list users with their extension attribute value.bru create mode 100644 graph-requests-samples/unit tests/extension attribute/users/update user to set the extension attribute value.bru create mode 100644 graph-requests-samples/unit tests/list test groups.bru create mode 100644 graph-requests-samples/unit tests/list test users.bru create mode 100644 graph-requests-samples/unit tests/show group by id.bru create mode 100644 graph-requests-samples/unit tests/show group members.bru create mode 100644 graph-requests-samples/validation/validate group.bru create mode 100644 graph-requests-samples/validation/validate user.bru diff --git a/CHANGELOG.md b/CHANGELOG.md index 622ef5e..4faa1ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Update the script that provisions tenant with test users and groups, to be more reliable and provision 999 users (instead of 50), so tests are more realistics * Improve tests * Publish a sample project that developers can use to create a custom version of EntraCP, for specific needs +* Add a [Bruno](https://www.usebruno.com/) collection to replay the requests sent to Microsoft Graph by EntraCP ## EntraCP v26.0.20240627.35 enhancements & bug-fixes - Published in June 27, 2024 diff --git a/graph-requests-samples/.env.example b/graph-requests-samples/.env.example new file mode 100644 index 0000000..1393d3d --- /dev/null +++ b/graph-requests-samples/.env.example @@ -0,0 +1,3 @@ +TENANTPREFIX= +CLIENTID= +CLIENTSECRET= \ No newline at end of file diff --git a/graph-requests-samples/.gitignore b/graph-requests-samples/.gitignore new file mode 100644 index 0000000..27d3867 --- /dev/null +++ b/graph-requests-samples/.gitignore @@ -0,0 +1,3 @@ +*.env* +!*.env.example +environments/yvand* \ No newline at end of file diff --git a/graph-requests-samples/README.md b/graph-requests-samples/README.md new file mode 100644 index 0000000..21e8e23 --- /dev/null +++ b/graph-requests-samples/README.md @@ -0,0 +1,13 @@ +# Replay the requests sent to Microsoft Graph by EntraCP + +This is the root folder of a [Bruno](https://www.usebruno.com/) collection (an alternative API debugger to Postman), that contains the typical requests sent to Microsoft Graph by EntraCP. + +To use it: + +* Clone this folder +* Rename/copy the file `.env.example` to `.env` and edit it with your tenant data +* Install [Bruno](https://www.usebruno.com/) and open this collection in Bruno +* Load environment `entracp-environment` +* Open request authentication > get access token. Click tab `Auth` and `Get Access Token` + +You can not replay the requests. diff --git a/graph-requests-samples/Search/replay search users and groups using batch.bru b/graph-requests-samples/Search/replay search users and groups using batch.bru new file mode 100644 index 0000000..7a3873e --- /dev/null +++ b/graph-requests-samples/Search/replay search users and groups using batch.bru @@ -0,0 +1,36 @@ +meta { + name: replay search users and groups using batch + type: http + seq: 8 +} + +post { + url: https://graph.microsoft.com/v1.0/$batch + body: json + auth: inherit +} + +body:json { + { + "requests": [ + { + "id": "9d99b7fa-428a-4025-acd5-8947f35dbffc", + "url": "/users?%24top=30\u0026%24filter=%28%20%28startswith%28UserPrincipalName%2C%20%27admini%27%29%20and%20UserType%20eq%20%27Member%27%29%20or%20%28startswith%28Mail%2C%20%27admini%27%29%20and%20UserType%20eq%20%27Guest%27%29%20%29%20or%20startswith%28DisplayName%2C%20%27admini%27%29%20or%20startswith%28GivenName%2C%20%27admini%27%29%20or%20startswith%28Surname%2C%20%27admini%27%29%20or%20startswith%28Mail%2C%20%27admini%27%29\u0026%24count=true\u0026%24select=UserType,Mail,UserPrincipalName,DisplayName,GivenName,Surname,Mail,DisplayName,Mail,MobilePhone,JobTitle,Department,OfficeLocation", + "method": "GET", + "headers": { + "Accept": "application/json", + "ConsistencyLevel": "eventual" + } + }, + { + "id": "7937d970-ad05-42af-96bf-559fe776a986", + "url": "/groups?%24top=30\u0026%24filter=startswith%28DisplayName%2C%20%27admini%27%29\u0026%24count=true\u0026%24select=Id,securityEnabled,DisplayName,DisplayName,Mail", + "method": "GET", + "headers": { + "Accept": "application/json", + "ConsistencyLevel": "eventual" + } + } + ] + } +} diff --git a/graph-requests-samples/augmentation/1- get user by userprincipalname.bru b/graph-requests-samples/augmentation/1- get user by userprincipalname.bru new file mode 100644 index 0000000..039724d --- /dev/null +++ b/graph-requests-samples/augmentation/1- get user by userprincipalname.bru @@ -0,0 +1,23 @@ +meta { + name: 1- Get user by UserPrincipalName + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/users?$select=UserType, Id, Mail, UserPrincipalName, DisplayName, GivenName, Surname, DisplayName, Mail, MobilePhone, JobTitle, Department, OfficeLocation&$filter=UserPrincipalName eq '{{entityUpn}}' + body: none + auth: inherit +} + +params:query { + $select: UserType, Id, Mail, UserPrincipalName, DisplayName, GivenName, Surname, DisplayName, Mail, MobilePhone, JobTitle, Department, OfficeLocation + $filter: UserPrincipalName eq '{{entityUpn}}' + ~$filter: UserPrincipalName eq 'AdeleV@{{tenantPrefix}}.OnMicrosoft.com' + ~$filter: UserPrincipalName eq 'yvand_microsoft.com%23EXT%23@{{tenantPrefix}}.onmicrosoft.com' +} + +vars:post-response { + userId: res.body.value[0].id +} + diff --git a/graph-requests-samples/augmentation/2- get group membership.bru b/graph-requests-samples/augmentation/2- get group membership.bru new file mode 100644 index 0000000..86c6405 --- /dev/null +++ b/graph-requests-samples/augmentation/2- get group membership.bru @@ -0,0 +1,15 @@ +meta { + name: 2- Get group membership + type: http + seq: 2 +} + +post { + url: https://graph.microsoft.com/v1.0/users/{{userId}}/microsoft.graph.getMemberGroups + body: json + auth: inherit +} + +body:json { + {"securityEnabledOnly":false} +} diff --git a/graph-requests-samples/authentication/get access token.bru b/graph-requests-samples/authentication/get access token.bru new file mode 100644 index 0000000..8fd6f00 --- /dev/null +++ b/graph-requests-samples/authentication/get access token.bru @@ -0,0 +1,22 @@ +meta { + name: get access token + type: http + seq: 1 +} + +get { + url: + auth: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: https://login.microsoftonline.com/{{tenantPrefix}}.onmicrosoft.com/oauth2/v2.0/token + client_id: {{clientId}} + client_secret: {{clientSecret}} + scope: https://graph.microsoft.com/.default +} + +vars:post-response { + accessToken: res.body.access_token +} diff --git a/graph-requests-samples/bruno.json b/graph-requests-samples/bruno.json new file mode 100644 index 0000000..8b19c25 --- /dev/null +++ b/graph-requests-samples/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "EntraCP", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/graph-requests-samples/collection.bru b/graph-requests-samples/collection.bru new file mode 100644 index 0000000..15566b2 --- /dev/null +++ b/graph-requests-samples/collection.bru @@ -0,0 +1,15 @@ +headers { + Authorization: Bearer {{accessToken}} +} + +auth { + mode: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: https://login.microsoftonline.com/{{tenantPrefix}}.onmicrosoft.com/oauth2/v2.0/token + client_id: {{clientId}} + client_secret: {{clientSecret}} + scope: https://graph.microsoft.com/.default +} diff --git a/graph-requests-samples/environments/entracp-environment.bru b/graph-requests-samples/environments/entracp-environment.bru new file mode 100644 index 0000000..7fd12a4 --- /dev/null +++ b/graph-requests-samples/environments/entracp-environment.bru @@ -0,0 +1,7 @@ +vars { + tenantPrefix: {{process.env.TENANTPREFIX}} + entityUpn: clouduser1@{{process.env.TENANTPREFIX}}.onmicrosoft.com + entityStartsWithValue: cloud + clientId: {{process.env.CLIENTID}} + clientSecret: {{process.env.CLIENTSECRET}} +} diff --git a/graph-requests-samples/search/list members of a group.bru b/graph-requests-samples/search/list members of a group.bru new file mode 100644 index 0000000..c9c48e5 --- /dev/null +++ b/graph-requests-samples/search/list members of a group.bru @@ -0,0 +1,19 @@ +meta { + name: List members of a group + type: http + seq: 5 +} + +get { + url: https://graph.microsoft.com/v1.0/groups/{{groupId}}/members/microsoft.graph.user?$select=id, userPrincipalName, mail + body: none + auth: inherit +} + +params:query { + $select: id, userPrincipalName, mail +} + +headers { + Accept: application/json +} diff --git a/graph-requests-samples/search/replay search users and groups using batch.bru b/graph-requests-samples/search/replay search users and groups using batch.bru new file mode 100644 index 0000000..7a3873e --- /dev/null +++ b/graph-requests-samples/search/replay search users and groups using batch.bru @@ -0,0 +1,36 @@ +meta { + name: replay search users and groups using batch + type: http + seq: 8 +} + +post { + url: https://graph.microsoft.com/v1.0/$batch + body: json + auth: inherit +} + +body:json { + { + "requests": [ + { + "id": "9d99b7fa-428a-4025-acd5-8947f35dbffc", + "url": "/users?%24top=30\u0026%24filter=%28%20%28startswith%28UserPrincipalName%2C%20%27admini%27%29%20and%20UserType%20eq%20%27Member%27%29%20or%20%28startswith%28Mail%2C%20%27admini%27%29%20and%20UserType%20eq%20%27Guest%27%29%20%29%20or%20startswith%28DisplayName%2C%20%27admini%27%29%20or%20startswith%28GivenName%2C%20%27admini%27%29%20or%20startswith%28Surname%2C%20%27admini%27%29%20or%20startswith%28Mail%2C%20%27admini%27%29\u0026%24count=true\u0026%24select=UserType,Mail,UserPrincipalName,DisplayName,GivenName,Surname,Mail,DisplayName,Mail,MobilePhone,JobTitle,Department,OfficeLocation", + "method": "GET", + "headers": { + "Accept": "application/json", + "ConsistencyLevel": "eventual" + } + }, + { + "id": "7937d970-ad05-42af-96bf-559fe776a986", + "url": "/groups?%24top=30\u0026%24filter=startswith%28DisplayName%2C%20%27admini%27%29\u0026%24count=true\u0026%24select=Id,securityEnabled,DisplayName,DisplayName,Mail", + "method": "GET", + "headers": { + "Accept": "application/json", + "ConsistencyLevel": "eventual" + } + } + ] + } +} diff --git a/graph-requests-samples/search/run a batch request.bru b/graph-requests-samples/search/run a batch request.bru new file mode 100644 index 0000000..f5c5c6c --- /dev/null +++ b/graph-requests-samples/search/run a batch request.bru @@ -0,0 +1,44 @@ +meta { + name: Run a batch request + type: http + seq: 7 +} + +post { + url: https://graph.microsoft.com/v1.0/$batch + body: json + auth: inherit +} + +body:json { + { + "requests": [ + { + "id": "9d99b7fa-428a-4025-acd5-8947f35dbffc", + "url": "/users?%24top=30\u0026%24filter=%28%20%28startswith%28UserPrincipalName%2C%20%27{{entityStartsWithValue}}%27%29%20and%20UserType%20eq%20%27Member%27%29%20or%20%28startswith%28Mail%2C%20%27{{entityStartsWithValue}}%27%29%20and%20UserType%20eq%20%27Guest%27%29%20%29%20or%20startswith%28DisplayName%2C%20%27{{entityStartsWithValue}}%27%29%20or%20startswith%28GivenName%2C%20%27{{entityStartsWithValue}}%27%29%20or%20startswith%28Surname%2C%20%27{{entityStartsWithValue}}%27%29%20or%20startswith%28Mail%2C%20%27{{entityStartsWithValue}}%27%29\u0026%24count=true\u0026%24select=UserType,Mail,UserPrincipalName,DisplayName,GivenName,Surname,Mail,DisplayName,Mail,MobilePhone,JobTitle,Department,OfficeLocation", + "method": "GET", + "headers": { + "Accept": "application/json", + "ConsistencyLevel": "eventual" + } + }, + { + "id": "7937d970-ad05-42af-96bf-559fe776a986", + "url": "/groups?%24top=30\u0026%24filter=startswith%28DisplayName%2C%20%27{{entityStartsWithValue}}%27%29\u0026%24count=true\u0026%24select=Id,securityEnabled,DisplayName,DisplayName,Mail", + "method": "GET", + "headers": { + "Accept": "application/json", + "ConsistencyLevel": "eventual" + } + }, + { + "id": "b762c874-eccd-4fd9-abf8-0334561bfd5b", + "url": "/groups/{{groupId}}/members/microsoft.graph.user?$select=id, userPrincipalName, mail", + "method": "GET", + "headers": { + "Accept": "application/json" + } + } + ] + } +} diff --git a/graph-requests-samples/search/search groups.bru b/graph-requests-samples/search/search groups.bru new file mode 100644 index 0000000..85690af --- /dev/null +++ b/graph-requests-samples/search/search groups.bru @@ -0,0 +1,16 @@ +meta { + name: Search groups + type: http + seq: 4 +} + +get { + url: https://graph.microsoft.com/v1.0/groups?$select=Id, DisplayName&$filter=startswith(DisplayName,'{{entityStartsWithValue}}') + body: none + auth: inherit +} + +params:query { + $select: Id, DisplayName + $filter: startswith(DisplayName,'{{entityStartsWithValue}}') +} diff --git a/graph-requests-samples/search/search users copy.bru b/graph-requests-samples/search/search users copy.bru new file mode 100644 index 0000000..4081ade --- /dev/null +++ b/graph-requests-samples/search/search users copy.bru @@ -0,0 +1,27 @@ +meta { + name: Search users Copy + type: http + seq: 3 +} + +get { + url: https://graph.microsoft.com/v1.0/users?$filter=startswith(UserPrincipalName, 'onprem') + body: none + auth: inherit +} + +params:query { + $filter: startswith(UserPrincipalName, 'onprem') + ~$top: 30 + ~$count: true + ~$select: UserType,Mail,UserPrincipalName,BusinessPhones,DisplayName,GivenName,Surname,Mail,DisplayName,Mail,MobilePhone,JobTitle,Department,OfficeLocation + ~$filter: ( (startswith(UserPrincipalName, '+1 ') and UserType eq 'Member') or (startswith(Mail, '+1 ') and UserType eq 'Guest') ) or startswith(BusinessPhones, '+1 ') or startswith(DisplayName, '+1 ') or startswith(GivenName, '+1 ') or startswith(Surname, '+1 ') or startswith(Mail, '+1 ') + ~$filter: businessPhones/any(p:startsWith(p, '%2B1')) + ~$filter: businessPhones/any(s:s eq '1234') + ~$filter: identities/any(c:c/issuerAssignedId:startsWith(c:c/issuerAssignedId, 'admin')) + ~$filter: identities/any(c:c/issuerAssignedId eq 'admin@{{tenantPrefix}}.onmicrosoft.com' and c/issuer eq '{{tenantPrefix}}.onmicrosoft.com') +} + +headers { + ConsistencyLevel: Eventual +} diff --git a/graph-requests-samples/search/search users.bru b/graph-requests-samples/search/search users.bru new file mode 100644 index 0000000..a2c5867 --- /dev/null +++ b/graph-requests-samples/search/search users.bru @@ -0,0 +1,20 @@ +meta { + name: Search users + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/users?$select=UserType, Mail, UserPrincipalName&$filter=accountEnabled eq true and startswith(UserPrincipalName,'{{entityStartsWithValue}}')&$expand=memberOf($select=id, displayName)&$top=30 + body: none + auth: inherit +} + +params:query { + $select: UserType, Mail, UserPrincipalName + $filter: accountEnabled eq true and startswith(UserPrincipalName,'{{entityStartsWithValue}}') + $expand: memberOf($select=id, displayName) + $top: 30 + ~$select: UserType, Mail, UserPrincipalName, DisplayName, GivenName, Surname, DisplayName, Mail, MobilePhone, JobTitle, Department, OfficeLocation + ~$filter: accountEnabled eq true and (startswith(UserPrincipalName,'{{entityStartsWithValue}}') or startswith(DisplayName,'{{entityStartsWithValue}}') or startswith(GivenName,'{{entityStartsWithValue}}') or startswith(Surname,'{{entityStartsWithValue}}')) +} diff --git a/graph-requests-samples/search/show a group.bru b/graph-requests-samples/search/show a group.bru new file mode 100644 index 0000000..7ef8370 --- /dev/null +++ b/graph-requests-samples/search/show a group.bru @@ -0,0 +1,19 @@ +meta { + name: Show a group + type: http + seq: 6 +} + +get { + url: https://graph.microsoft.com/v1.0/groups/{{groupId}}?$select=id, displayName + body: none + auth: inherit +} + +params:query { + $select: id, displayName +} + +headers { + Accept: application/json +} diff --git a/graph-requests-samples/unit tests/extension attribute/extension properties/list extension properties.bru b/graph-requests-samples/unit tests/extension attribute/extension properties/list extension properties.bru new file mode 100644 index 0000000..0fcae2b --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/extension properties/list extension properties.bru @@ -0,0 +1,11 @@ +meta { + name: List extension properties + type: http + seq: 1 +} + +post { + url: https://graph.microsoft.com/v1.0/directoryObjects/getAvailableExtensionProperties + body: json + auth: inherit +} diff --git a/graph-requests-samples/unit tests/extension attribute/service principal policies/assign a claimsmappingpolicy to a service principal.bru b/graph-requests-samples/unit tests/extension attribute/service principal policies/assign a claimsmappingpolicy to a service principal.bru new file mode 100644 index 0000000..537b9de --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/service principal policies/assign a claimsmappingpolicy to a service principal.bru @@ -0,0 +1,18 @@ +meta { + name: Assign a claimsMappingPolicy to a service principal + type: http + seq: 2 +} + +post { + url: https://graph.microsoft.com/v1.0/servicePrincipals(appId='7ade56f8-12b0-472b-a923-102874ee083a')/claimsMappingPolicies/$ref + body: json + auth: inherit +} + +body:json { + + { + "@odata.id":"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/fc3beba5-5869-4020-9366-8bf7d0e8d924" + } +} diff --git a/graph-requests-samples/unit tests/extension attribute/service principal policies/list claimsmappingpolicies assigned to a service principal copy 2.bru b/graph-requests-samples/unit tests/extension attribute/service principal policies/list claimsmappingpolicies assigned to a service principal copy 2.bru new file mode 100644 index 0000000..5ad0ae4 --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/service principal policies/list claimsmappingpolicies assigned to a service principal copy 2.bru @@ -0,0 +1,11 @@ +meta { + name: List claimsMappingPolicies assigned to a service principal Copy 2 + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/servicePrincipals(appId='7ade56f8-12b0-472b-a923-102874ee083a')/claimsMappingPolicies + body: json + auth: inherit +} diff --git a/graph-requests-samples/unit tests/extension attribute/tenant policies/create a claimsmappingpolicy for saml claims copy.bru b/graph-requests-samples/unit tests/extension attribute/tenant policies/create a claimsmappingpolicy for saml claims copy.bru new file mode 100644 index 0000000..e6412dd --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/tenant policies/create a claimsmappingpolicy for saml claims copy.bru @@ -0,0 +1,24 @@ +meta { + name: Create a claimsMappingPolicy for SAML claims Copy + type: http + seq: 2 +} + +post { + url: https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies + body: json + auth: inherit +} + +headers { + Content-type: application/json +} + +body:json { + { + "definition": [ + "{\"ClaimsMappingPolicy\": { \"Version\": 1, \"IncludeBasicClaimSet\": \"false\", \"ClaimsSchema\": [{ \"Source\": \"User\", \"ExtensionID\": \"extension_7ade56f812b0472ba923102874ee083a_extensionAttribute1\", \"SamlClaimType\": \"http://schemas.yvand.org/claims/type1\" }] }}" + ], + "displayName": "Yvand claims policy with extension attribute" + } +} diff --git a/graph-requests-samples/unit tests/extension attribute/tenant policies/delete a claimsmappingpolicy.bru b/graph-requests-samples/unit tests/extension attribute/tenant policies/delete a claimsmappingpolicy.bru new file mode 100644 index 0000000..dafcf7a --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/tenant policies/delete a claimsmappingpolicy.bru @@ -0,0 +1,24 @@ +meta { + name: Delete a claimsMappingPolicy + type: http + seq: 3 +} + +delete { + url: https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/042e63eb-5d1f-44dd-8e63-af3700401f52 + body: json + auth: inherit +} + +headers { + Content-type: application/json +} + +body:json { + { + "definition": [ + "{\"ClaimsMappingPolicy\": { \"Version\": 1, \"IncludeBasicClaimSet\": \"false\", \"ClaimsSchema\": [{ \"Source\": \"User\", \"ExtensionID\": \"extension_7ade56f812b0472ba923102874ee083a_extensionAttribute1\", \"SamlClaimType\": \"http://schemas.yvand.org/claims/type1\" }] }}" + ], + "displayName": "Yvand claims policy with extension attribute" + } +} diff --git a/graph-requests-samples/unit tests/extension attribute/tenant policies/list claimsmappingpolicies in the tenant.bru b/graph-requests-samples/unit tests/extension attribute/tenant policies/list claimsmappingpolicies in the tenant.bru new file mode 100644 index 0000000..721da9e --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/tenant policies/list claimsmappingpolicies in the tenant.bru @@ -0,0 +1,11 @@ +meta { + name: List claimsMappingPolicies in the tenant + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies + body: json + auth: inherit +} diff --git a/graph-requests-samples/unit tests/extension attribute/tenant policies/update a claimsmappingpolicy for saml claims copy.bru b/graph-requests-samples/unit tests/extension attribute/tenant policies/update a claimsmappingpolicy for saml claims copy.bru new file mode 100644 index 0000000..1ecc842 --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/tenant policies/update a claimsmappingpolicy for saml claims copy.bru @@ -0,0 +1,24 @@ +meta { + name: Update a claimsMappingPolicy for SAML claims Copy + type: http + seq: 4 +} + +patch { + url: https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/fc3beba5-5869-4020-9366-8bf7d0e8d924 + body: json + auth: inherit +} + +headers { + Content-type: application/json +} + +body:json { + { + "definition": [ + "{\"ClaimsMappingPolicy\": { \"Version\": 1, \"IncludeBasicClaimSet\": \"false\", \"ClaimsSchema\": [{ \"Source\": \"User\", \"ExtensionID\": \"extension_7ade56f812b0472ba923102874ee083a_extensionAttribute1\", \"SamlClaimType\": \"http://schemas.yvand.org/claims/type1\" }, { \"Source\": \"User\", \"ID\": \"localuserprincipalname\", \"SamlClaimType\": \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier\" }, { \"Source\": \"User\", \"ID\": \"localuserprincipalname\", \"SamlClaimType\": \"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name\" }] }}" + ], + "displayName": "Yvand claims policy with extension attribute" + } +} diff --git a/graph-requests-samples/unit tests/extension attribute/users/list users with their extension attribute value.bru b/graph-requests-samples/unit tests/extension attribute/users/list users with their extension attribute value.bru new file mode 100644 index 0000000..0cb8b3d --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/users/list users with their extension attribute value.bru @@ -0,0 +1,21 @@ +meta { + name: List users with their extension attribute value + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/users?$select=id, displayName, userPrincipalName, UserType, mail, givenName, extension_7ade56f812b0472ba923102874ee083a_extensionAttribute1&$filter=startswith(userPrincipalName, 'testEntraCPUser_001') + body: none + auth: inherit +} + +params:query { + $select: id, displayName, userPrincipalName, UserType, mail, givenName, extension_7ade56f812b0472ba923102874ee083a_extensionAttribute1 + $filter: startswith(userPrincipalName, 'testEntraCPUser_001') +} + +headers { + Content-Type: application/json + Accept: application/json +} diff --git a/graph-requests-samples/unit tests/extension attribute/users/update user to set the extension attribute value.bru b/graph-requests-samples/unit tests/extension attribute/users/update user to set the extension attribute value.bru new file mode 100644 index 0000000..ab8d880 --- /dev/null +++ b/graph-requests-samples/unit tests/extension attribute/users/update user to set the extension attribute value.bru @@ -0,0 +1,22 @@ +meta { + name: Update user to set the extension attribute value + type: http + seq: 2 +} + +patch { + url: https://graph.microsoft.com/v1.0/users/testEntraCPUser_001@Yvand.onmicrosoft.com + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "extension_7ade56f812b0472ba923102874ee083a_extensionAttribute1": "value1" + } +} diff --git a/graph-requests-samples/unit tests/list test groups.bru b/graph-requests-samples/unit tests/list test groups.bru new file mode 100644 index 0000000..f78477a --- /dev/null +++ b/graph-requests-samples/unit tests/list test groups.bru @@ -0,0 +1,20 @@ +meta { + name: List test groups + type: http + seq: 2 +} + +get { + url: https://graph.microsoft.com/v1.0/groups?$select=ID, displayName, groupTypes, securityEnabled&$filter=startswith(DisplayName, 'testentracp') + body: none + auth: inherit +} + +params:query { + $select: ID, displayName, groupTypes, securityEnabled + $filter: startswith(DisplayName, 'testentracp') +} + +headers { + Content-Type: application/json +} diff --git a/graph-requests-samples/unit tests/list test users.bru b/graph-requests-samples/unit tests/list test users.bru new file mode 100644 index 0000000..a3c9a6d --- /dev/null +++ b/graph-requests-samples/unit tests/list test users.bru @@ -0,0 +1,21 @@ +meta { + name: List test users + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/users?$select=id, displayName, userPrincipalName, UserType, mail, givenName&$filter=startswith(userPrincipalName, 'testEntraCP') + body: none + auth: inherit +} + +params:query { + $select: id, displayName, userPrincipalName, UserType, mail, givenName + $filter: startswith(userPrincipalName, 'testEntraCP') +} + +headers { + Content-Type: application/json + Accept: application/json +} diff --git a/graph-requests-samples/unit tests/show group by id.bru b/graph-requests-samples/unit tests/show group by id.bru new file mode 100644 index 0000000..8848042 --- /dev/null +++ b/graph-requests-samples/unit tests/show group by id.bru @@ -0,0 +1,19 @@ +meta { + name: Show group by Id + type: http + seq: 3 +} + +get { + url: https://graph.microsoft.com/v1.0/groups/{{groupId}}?$select=id, displayName, groupTypes + body: none + auth: inherit +} + +params:query { + $select: id, displayName, groupTypes +} + +headers { + Content-Type: application/json +} diff --git a/graph-requests-samples/unit tests/show group members.bru b/graph-requests-samples/unit tests/show group members.bru new file mode 100644 index 0000000..d99bb0a --- /dev/null +++ b/graph-requests-samples/unit tests/show group members.bru @@ -0,0 +1,19 @@ +meta { + name: Show group members + type: http + seq: 4 +} + +get { + url: https://graph.microsoft.com/v1.0/groups/{{groupId}}/members?$select=userPrincipalName + body: none + auth: inherit +} + +params:query { + $select: userPrincipalName +} + +headers { + Content-Type: application/json +} diff --git a/graph-requests-samples/validation/validate group.bru b/graph-requests-samples/validation/validate group.bru new file mode 100644 index 0000000..42d65e1 --- /dev/null +++ b/graph-requests-samples/validation/validate group.bru @@ -0,0 +1,17 @@ +meta { + name: Validate group + type: http + seq: 2 +} + +get { + url: https://graph.microsoft.com/v1.0/groups?$select=Id, Id, DisplayName, Mail&$filter=Id eq '{{groupId}}'&$top=1 + body: none + auth: inherit +} + +params:query { + $select: Id, Id, DisplayName, Mail + $filter: Id eq '{{groupId}}' + $top: 1 +} diff --git a/graph-requests-samples/validation/validate user.bru b/graph-requests-samples/validation/validate user.bru new file mode 100644 index 0000000..25ecc79 --- /dev/null +++ b/graph-requests-samples/validation/validate user.bru @@ -0,0 +1,21 @@ +meta { + name: Validate user + type: http + seq: 1 +} + +get { + url: https://graph.microsoft.com/v1.0/users?$select=id, userType, mail, userPrincipalName, displayName, Mail, mobilePhone, jobTitle, department, officeLocation&$filter=( (UserPrincipalName eq '{{entityUpn}}' and UserType eq 'Member') or (Mail eq '{{EntityUPN}}' and UserType eq 'Guest') )&$top=1 + body: none + auth: inherit +} + +params:query { + $select: id, userType, mail, userPrincipalName, displayName, Mail, mobilePhone, jobTitle, department, officeLocation + $filter: ( (UserPrincipalName eq '{{entityUpn}}' and UserType eq 'Member') or (Mail eq '{{EntityUPN}}' and UserType eq 'Guest') ) + $top: 1 +} + +vars:post-response { + userId: res.body.value[0].id +} From e5b3628e6632d1419115133fa9b1dd8b8abd5637 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Wed, 24 Jul 2024 09:52:40 +0200 Subject: [PATCH 06/23] reorganize folders --- .../ADMIN/SharePointProjectItem.spdata | 0 .../CustomClaimsProvider.csproj | 0 .../CustomClaimsProvider.sln | 0 .../CustomClaimsProvider/EntraCP_Custom.cs | 0 .../EntraCP.Custom.ClaimsProvider.Template.xml | 0 .../EntraCP.Custom.ClaimsProvider.feature | 0 .../EntraCP.Custom.EventReceiver.cs | 0 .../Package/Package.Template.xml | 0 .../Package/Package.package | 0 .../Properties/AssemblyInfo.cs | 0 .../CustomClaimsProvider/README.md | 9 +++++++++ .../README.md | 0 samples/CustomClaimsProvider/README.md | 5 ----- samples/Yvand.EntraCP.dll | Bin 149504 -> 0 bytes 14 files changed, 9 insertions(+), 5 deletions(-) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/CustomClaimsProvider.csproj (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/CustomClaimsProvider.sln (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/EntraCP_Custom.cs (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/Package/Package.Template.xml (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/Package/Package.package (100%) rename {samples => custom-claims-provider-samples}/CustomClaimsProvider/Properties/AssemblyInfo.cs (100%) create mode 100644 custom-claims-provider-samples/CustomClaimsProvider/README.md rename {samples => custom-claims-provider-samples}/README.md (100%) delete mode 100644 samples/CustomClaimsProvider/README.md delete mode 100644 samples/Yvand.EntraCP.dll diff --git a/samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata b/custom-claims-provider-samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata similarity index 100% rename from samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata rename to custom-claims-provider-samples/CustomClaimsProvider/ADMIN/SharePointProjectItem.spdata diff --git a/samples/CustomClaimsProvider/CustomClaimsProvider.csproj b/custom-claims-provider-samples/CustomClaimsProvider/CustomClaimsProvider.csproj similarity index 100% rename from samples/CustomClaimsProvider/CustomClaimsProvider.csproj rename to custom-claims-provider-samples/CustomClaimsProvider/CustomClaimsProvider.csproj diff --git a/samples/CustomClaimsProvider/CustomClaimsProvider.sln b/custom-claims-provider-samples/CustomClaimsProvider/CustomClaimsProvider.sln similarity index 100% rename from samples/CustomClaimsProvider/CustomClaimsProvider.sln rename to custom-claims-provider-samples/CustomClaimsProvider/CustomClaimsProvider.sln diff --git a/samples/CustomClaimsProvider/EntraCP_Custom.cs b/custom-claims-provider-samples/CustomClaimsProvider/EntraCP_Custom.cs similarity index 100% rename from samples/CustomClaimsProvider/EntraCP_Custom.cs rename to custom-claims-provider-samples/CustomClaimsProvider/EntraCP_Custom.cs diff --git a/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml b/custom-claims-provider-samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml similarity index 100% rename from samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml rename to custom-claims-provider-samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.Template.xml diff --git a/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature b/custom-claims-provider-samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature similarity index 100% rename from samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature rename to custom-claims-provider-samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.ClaimsProvider.feature diff --git a/samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs b/custom-claims-provider-samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs similarity index 100% rename from samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs rename to custom-claims-provider-samples/CustomClaimsProvider/Features/EntraCP.Custom.ClaimsProvider/EntraCP.Custom.EventReceiver.cs diff --git a/samples/CustomClaimsProvider/Package/Package.Template.xml b/custom-claims-provider-samples/CustomClaimsProvider/Package/Package.Template.xml similarity index 100% rename from samples/CustomClaimsProvider/Package/Package.Template.xml rename to custom-claims-provider-samples/CustomClaimsProvider/Package/Package.Template.xml diff --git a/samples/CustomClaimsProvider/Package/Package.package b/custom-claims-provider-samples/CustomClaimsProvider/Package/Package.package similarity index 100% rename from samples/CustomClaimsProvider/Package/Package.package rename to custom-claims-provider-samples/CustomClaimsProvider/Package/Package.package diff --git a/samples/CustomClaimsProvider/Properties/AssemblyInfo.cs b/custom-claims-provider-samples/CustomClaimsProvider/Properties/AssemblyInfo.cs similarity index 100% rename from samples/CustomClaimsProvider/Properties/AssemblyInfo.cs rename to custom-claims-provider-samples/CustomClaimsProvider/Properties/AssemblyInfo.cs diff --git a/custom-claims-provider-samples/CustomClaimsProvider/README.md b/custom-claims-provider-samples/CustomClaimsProvider/README.md new file mode 100644 index 0000000..a854b43 --- /dev/null +++ b/custom-claims-provider-samples/CustomClaimsProvider/README.md @@ -0,0 +1,9 @@ +# Sample with a hard-coded configuration and a manual reference to EntraCP + +This project shows how to create a claims provider that inherits EntraCP. It uses a simple, hard-coded configuration to specify the tenant. + +> [!WARNING] +> Do NOT deploy this solution in a SharePoint farm that already has EntraCP deployed, unless both use **exactly** the same versions of NuGet dependencies. If they use different versions, that may cause errors when loading DLLs, due to mismatches with the assembly bindings in the machine.config file. + +> [!IMPORTANT] +> You need to manually add a reference to `Yvand.EntraCP.dll`, and its version will determine the version of the Nuget packages `Azure.Identity` and `Microsoft.Graph` you should use in your project, because that will allow you to reuse the same assembly bindings provided in the file `assembly-bindings.config` and avoid the tedious task of figuring them all out by yourself. diff --git a/samples/README.md b/custom-claims-provider-samples/README.md similarity index 100% rename from samples/README.md rename to custom-claims-provider-samples/README.md diff --git a/samples/CustomClaimsProvider/README.md b/samples/CustomClaimsProvider/README.md deleted file mode 100644 index 94bb58c..0000000 --- a/samples/CustomClaimsProvider/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Sample with a hard-coded configuration - -This project shows how to create a claims provider that inherits EntraCP. It uses a simple, hard-coded configuration to specify the tenant. - -Do NOT deploy this solution in a SharePoint farm that already has EntraCP deployed, unless both use **exactly** the same versions of NuGet dependencies. If they use different versions, that may cause errors when loading DLLs, due to mismatches with the assembly bindings in the machine.config file. diff --git a/samples/Yvand.EntraCP.dll b/samples/Yvand.EntraCP.dll deleted file mode 100644 index 89a103d52eb2519bd946ba8f865d31d0d952e032..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149504 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 Z$NMrc0Iknuw?Ddf8D;;m$p3d3_ Date: Wed, 24 Jul 2024 14:03:56 +0200 Subject: [PATCH 07/23] Update UnitTestsHelper.cs --- Yvand.EntraCP.Tests/UnitTestsHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Yvand.EntraCP.Tests/UnitTestsHelper.cs b/Yvand.EntraCP.Tests/UnitTestsHelper.cs index ac486ad..05b6128 100644 --- a/Yvand.EntraCP.Tests/UnitTestsHelper.cs +++ b/Yvand.EntraCP.Tests/UnitTestsHelper.cs @@ -285,10 +285,11 @@ public IEnumerable GetSomeEntities(int count, Func filter = null) count = Entities.Count; } + int randomNumberMaxValue = Entities.Where(filter ?? (x => true)).Count() - 1; List entitiesIdxs = new List(count); for (int i = 0; i < count; i++) { - entitiesIdxs.Add(RandomNumber.Next(0, Entities.Where(filter ?? (x => true)).Count() - 1)); + entitiesIdxs.Add(RandomNumber.Next(0, randomNumberMaxValue)); } foreach (int userIdx in entitiesIdxs) From 795d17d6a454a7080f618791ec57a08d3a61b746 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Thu, 25 Jul 2024 10:00:58 +0200 Subject: [PATCH 08/23] remove outdated pipelines --- azure-pipelines/ci-apply-dtl-artifacts.yml | 131 ----------------- azure-pipelines/ci-compile.yml | 132 ------------------ azure-pipelines/ci-create-dtl-environment.yml | 21 --- 3 files changed, 284 deletions(-) delete mode 100644 azure-pipelines/ci-apply-dtl-artifacts.yml delete mode 100644 azure-pipelines/ci-compile.yml delete mode 100644 azure-pipelines/ci-create-dtl-environment.yml diff --git a/azure-pipelines/ci-apply-dtl-artifacts.yml b/azure-pipelines/ci-apply-dtl-artifacts.yml deleted file mode 100644 index bbfb89e..0000000 --- a/azure-pipelines/ci-apply-dtl-artifacts.yml +++ /dev/null @@ -1,131 +0,0 @@ -resources: -- repo: self - -jobs: -- job: ApplyArtifactsSP2013 - condition: eq(variables['Deployment.ProvisionSharePoint2013'], true) - displayName: Apply artifacts on SP2013 - timeoutInMinutes: 30 - variables: - jobSharePointVersion: 2013 - pool: - vmImage: 'windows-2019' - steps: - - checkout: none #skip checking out the default repository resource - - task: automagically.DownloadFile.DownloadFile.DownloadFile@1 - displayName: 'Download apply-dtl-artifact.ps1' - inputs: - FileUrl: 'https://raw.githubusercontent.com/Yvand/AzureRM-Templates/master/DevTestLabs-Artifacts/manage-artifacts/apply-dtl-artifact.ps1' - DestinationFolder: '$(System.DefaultWorkingDirectory)\scripts' - - - task: AzurePowerShell@3 - displayName: 'Create and register a VSTS agent in DevOps agent pools by applying artifact "Azure Pipelines Agent"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: '-DevTestLabName "$(DevTestLabs.LabName)" -VirtualMachineName "SP$(jobSharePointVersion)" -RepositoryName "Yvand/AzureRM-Templates" -ArtifactName "windows-vsts-build-agent" -param_vstsAccount "$(DevOps.OrganizationName)" -param_vstsPassword "$(DevOps.AccessToken)" -param_poolName "$(system.teamProject)-Tests-$(jobSharePointVersion)" -param_windowsLogonAccount "$(Deployment.DomainName)\$(Deployment.AdminUserName)" -param_windowsLogonPassword "$(Deployment.AdminPassword)" -param_agentName "SP$(jobSharePointVersion)" -param_agentNameSuffix "-$(Build.BuildNumber)" -param_RunAsAutoLogon false -param_driveLetter C -param_workDirectory ""' - preferredAzurePowerShellVersion: 5.1.1 - - - task: AzurePowerShell@3 - displayName: 'Apply artifact "Download Azure Pipelines Artifact and Run Script"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: -DevTestLabName '$(DevTestLabs.LabName)' -VirtualMachineName 'SP$(jobSharePointVersion)' -RepositoryName 'Yvand/AzureRM-Templates' -ArtifactName 'windows-vsts-download-and-run-script' -param_vstsProjectUri 'https://dev.azure.com/$(DevOps.OrganizationName)/$(system.teamProject)' -param_buildDefinitionName '$(DevOps.BuildArtifactsPipelineName)' -param_personalAccessToken $(DevOps.AccessToken) -param_pathToScript 'drop_$(Tests.BuildConfiguration)\$(Deployment.ConfigureServerFolderName)\ConfigureLab.ps1' -param_scriptArguments "-pathToPackage '..\$(system.teamProject)\bin\$(Tests.BuildConfiguration)\$(system.teamProject).wsp' -claimsProviderName '$(system.teamProject)' -spTrustName '$(Deployment.DomainFQDN)' -adminUserName '$(Deployment.DomainName)\$(Deployment.AdminUserName)' -adminPassword '$(Deployment.AdminPassword)'" - preferredAzurePowerShellVersion: 5.1.1 - -- job: ApplyArtifactsSP2016 - condition: eq(variables['Deployment.ProvisionSharePoint2016'], true) - displayName: Apply artifacts on SP2016 - timeoutInMinutes: 30 - variables: - jobSharePointVersion: 2016 - pool: - vmImage: 'windows-2019' - steps: - - checkout: none #skip checking out the default repository resource - - task: automagically.DownloadFile.DownloadFile.DownloadFile@1 - displayName: 'Download apply-dtl-artifact.ps1' - inputs: - FileUrl: 'https://raw.githubusercontent.com/Yvand/AzureRM-Templates/master/DevTestLabs-Artifacts/manage-artifacts/apply-dtl-artifact.ps1' - DestinationFolder: '$(System.DefaultWorkingDirectory)\scripts' - - - task: AzurePowerShell@3 - displayName: 'Create and register a VSTS agent in DevOps agent pools by applying artifact "Azure Pipelines Agent"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: '-DevTestLabName "$(DevTestLabs.LabName)" -VirtualMachineName "SP$(jobSharePointVersion)" -RepositoryName "Yvand/AzureRM-Templates" -ArtifactName "windows-vsts-build-agent" -param_vstsAccount "$(DevOps.OrganizationName)" -param_vstsPassword "$(DevOps.AccessToken)" -param_poolName "$(system.teamProject)-Tests-$(jobSharePointVersion)" -param_windowsLogonAccount "$(Deployment.DomainName)\$(Deployment.AdminUserName)" -param_windowsLogonPassword "$(Deployment.AdminPassword)" -param_agentName "SP$(jobSharePointVersion)" -param_agentNameSuffix "-$(Build.BuildNumber)" -param_RunAsAutoLogon false -param_driveLetter C -param_workDirectory ""' - preferredAzurePowerShellVersion: 5.1.1 - - - task: AzurePowerShell@3 - displayName: 'Apply artifact "Download Azure Pipelines Artifact and Run Script"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: -DevTestLabName '$(DevTestLabs.LabName)' -VirtualMachineName 'SP$(jobSharePointVersion)' -RepositoryName 'Yvand/AzureRM-Templates' -ArtifactName 'windows-vsts-download-and-run-script' -param_vstsProjectUri 'https://dev.azure.com/$(DevOps.OrganizationName)/$(system.teamProject)' -param_buildDefinitionName '$(DevOps.BuildArtifactsPipelineName)' -param_personalAccessToken $(DevOps.AccessToken) -param_pathToScript 'drop_$(Tests.BuildConfiguration)\$(Deployment.ConfigureServerFolderName)\ConfigureLab.ps1' -param_scriptArguments "-pathToPackage '..\$(system.teamProject)\bin\$(Tests.BuildConfiguration)\$(system.teamProject).wsp' -claimsProviderName '$(system.teamProject)' -spTrustName '$(Deployment.DomainFQDN)' -adminUserName '$(Deployment.DomainName)\$(Deployment.AdminUserName)' -adminPassword '$(Deployment.AdminPassword)'" - preferredAzurePowerShellVersion: 5.1.1 - -- job: ApplyArtifactsSP2019 - condition: eq(variables['Deployment.ProvisionSharePoint2019'], true) - displayName: Apply artifacts on SP2019 - timeoutInMinutes: 30 - variables: - jobSharePointVersion: 2019 - pool: - vmImage: 'windows-2019' - steps: - - checkout: none #skip checking out the default repository resource - - task: automagically.DownloadFile.DownloadFile.DownloadFile@1 - displayName: 'Download apply-dtl-artifact.ps1' - inputs: - FileUrl: 'https://raw.githubusercontent.com/Yvand/AzureRM-Templates/master/DevTestLabs-Artifacts/manage-artifacts/apply-dtl-artifact.ps1' - DestinationFolder: '$(System.DefaultWorkingDirectory)\scripts' - - - task: AzurePowerShell@3 - displayName: 'Create and register a VSTS agent in DevOps agent pools by applying artifact "Azure Pipelines Agent"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: '-DevTestLabName "$(DevTestLabs.LabName)" -VirtualMachineName "SP$(jobSharePointVersion)" -RepositoryName "Yvand/AzureRM-Templates" -ArtifactName "windows-vsts-build-agent" -param_vstsAccount "$(DevOps.OrganizationName)" -param_vstsPassword "$(DevOps.AccessToken)" -param_poolName "$(system.teamProject)-Tests-$(jobSharePointVersion)" -param_windowsLogonAccount "$(Deployment.DomainName)\$(Deployment.AdminUserName)" -param_windowsLogonPassword "$(Deployment.AdminPassword)" -param_agentName "SP$(jobSharePointVersion)" -param_agentNameSuffix "-$(Build.BuildNumber)" -param_RunAsAutoLogon false -param_driveLetter C -param_workDirectory ""' - preferredAzurePowerShellVersion: 5.1.1 - - - task: AzurePowerShell@3 - displayName: 'Apply artifact "Download Azure Pipelines Artifact and Run Script"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: -DevTestLabName '$(DevTestLabs.LabName)' -VirtualMachineName 'SP$(jobSharePointVersion)' -RepositoryName 'Yvand/AzureRM-Templates' -ArtifactName 'windows-vsts-download-and-run-script' -param_vstsProjectUri 'https://dev.azure.com/$(DevOps.OrganizationName)/$(system.teamProject)' -param_buildDefinitionName '$(DevOps.BuildArtifactsPipelineName)' -param_personalAccessToken $(DevOps.AccessToken) -param_pathToScript 'drop_$(Tests.BuildConfiguration)\$(Deployment.ConfigureServerFolderName)\ConfigureLab.ps1' -param_scriptArguments "-pathToPackage '..\$(system.teamProject)\bin\$(Tests.BuildConfiguration)\$(system.teamProject).wsp' -claimsProviderName '$(system.teamProject)' -spTrustName '$(Deployment.DomainFQDN)' -adminUserName '$(Deployment.DomainName)\$(Deployment.AdminUserName)' -adminPassword '$(Deployment.AdminPassword)'" - preferredAzurePowerShellVersion: 5.1.1 - -- job: ApplyArtifactsSPSubscription - condition: eq(variables['Deployment.ProvisionSharePointSubscription'], true) - displayName: Apply artifacts on SPSubscription - timeoutInMinutes: 30 - variables: - jobSharePointVersion: Subscription - pool: - vmImage: 'windows-latest' - steps: - - checkout: none #skip checking out the default repository resource - - task: automagically.DownloadFile.DownloadFile.DownloadFile@1 - displayName: 'Download apply-dtl-artifact.ps1' - inputs: - FileUrl: 'https://raw.githubusercontent.com/Yvand/AzureRM-Templates/master/DevTestLabs-Artifacts/manage-artifacts/apply-dtl-artifact.ps1' - DestinationFolder: '$(System.DefaultWorkingDirectory)\scripts' - - - task: AzurePowerShell@3 - displayName: 'Create and register a VSTS agent in DevOps agent pools by applying artifact "Azure Pipelines Agent"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: '-DevTestLabName "$(DevTestLabs.LabName)" -VirtualMachineName "SP$(jobSharePointVersion)" -RepositoryName "Yvand/AzureRM-Templates" -ArtifactName "windows-vsts-build-agent" -param_vstsAccount "$(DevOps.OrganizationName)" -param_vstsPassword "$(DevOps.AccessToken)" -param_poolName "$(system.teamProject)-Tests-$(jobSharePointVersion)" -param_windowsLogonAccount "$(Deployment.DomainName)\$(Deployment.AdminUserName)" -param_windowsLogonPassword "$(Deployment.AdminPassword)" -param_agentName "SP$(jobSharePointVersion)" -param_agentNameSuffix "-$(Build.BuildNumber)" -param_RunAsAutoLogon false -param_driveLetter C -param_workDirectory ""' - preferredAzurePowerShellVersion: 5.1.1 - - - task: AzurePowerShell@3 - displayName: 'Apply artifact "Download Azure Pipelines Artifact and Run Script"' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - ScriptPath: '$(System.DefaultWorkingDirectory)\scripts\apply-dtl-artifact.ps1' - ScriptArguments: -DevTestLabName '$(DevTestLabs.LabName)' -VirtualMachineName 'SP$(jobSharePointVersion)' -RepositoryName 'Yvand/AzureRM-Templates' -ArtifactName 'windows-vsts-download-and-run-script' -param_vstsProjectUri 'https://dev.azure.com/$(DevOps.OrganizationName)/$(system.teamProject)' -param_buildDefinitionName '$(DevOps.BuildArtifactsPipelineName)' -param_personalAccessToken $(DevOps.AccessToken) -param_pathToScript 'drop_$(Tests.BuildConfiguration)\$(Deployment.ConfigureServerFolderName)\ConfigureLab.ps1' -param_scriptArguments "-pathToPackage '..\$(system.teamProject)\bin\$(Tests.BuildConfiguration)\$(system.teamProject).wsp' -claimsProviderName '$(system.teamProject)' -spTrustName '$(Deployment.DomainFQDN)' -adminUserName '$(Deployment.DomainName)\$(Deployment.AdminUserName)' -adminPassword '$(Deployment.AdminPassword)'" - preferredAzurePowerShellVersion: 5.1.1 \ No newline at end of file diff --git a/azure-pipelines/ci-compile.yml b/azure-pipelines/ci-compile.yml deleted file mode 100644 index 786a253..0000000 --- a/azure-pipelines/ci-compile.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: $(BuildVersion).$(date:yyyyMMdd).$(Build.BuildId) -resources: -- repo: self - -pr: -- master - -variables: - SolutionFileName: '$(system.teamProject).sln' - -jobs: -- job: Compile - strategy: - maxParallel: 2 - matrix: - debugJob: - configuration: debug - releaseJob: - configuration: release - displayName: Compile - pool: - vmImage: 'windows-2019' - demands: - - msbuild - - visualstudio - - azureps - steps: - - task: DownloadSecureFile@1 - displayName: 'Download signing key' - inputs: - secureFile: '$(SigningKeySecureFileID)' - - - powershell: | - # Set variables - $azureStorageBaseDirectory = "Resources\$(system.teamProject)" - $projectLocalPath = "$(System.DefaultWorkingDirectory)\$(system.teamProject)" - $devTestLabsLocalPath = "$(Build.ArtifactStagingDirectory)\$(Tests.DataFolderName)" - - # Create now folder that will contain later scripts to configure local test server - #if ((Test-Path -Path "$(Build.ArtifactStagingDirectory)\$(Deployment.ConfigureServerFolderName)" -PathType Container) -eq $false) { - #New-Item -ItemType Directory -Path "$(Build.ArtifactStagingDirectory)\$(Deployment.ConfigureServerFolderName)" - #} - - Write-Output ("Copy signing key from $(DownloadSecureFile.secureFilePath) to $projectLocalPath") - Copy-Item "$(DownloadSecureFile.secureFilePath)" -Destination "$projectLocalPath" - - Write-Output ("Copy Microsoft.SharePoint.dll from Azure storage account") - $azureContext = New-AzureStorageContext $(AzureStorageAccountName) $(AzureStorageAccountKey) - $azureShare = Get-AzureStorageShare $(AzureStorageShareName) -Context $azureContext - Get-AzureStorageFileContent -Share $azureShare -Path "$azureStorageBaseDirectory\SharePoint 2013\Microsoft.SharePoint.dll" "$projectLocalPath\Microsoft.SharePoint.dll" - Write-Output ("Add Microsoft.SharePoint.dll to the GAC") - [System.Reflection.Assembly]::Load("System.EnterpriseServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") - $publish = New-Object System.EnterpriseServices.Internal.Publish - $publish.GacInstall("$projectLocalPath\Microsoft.SharePoint.dll") - - Write-Output ("Copy integration tests data from the Azure storage account") - # Create the destination directory - if ((Test-Path -Path $devTestLabsLocalPath -PathType Container) -eq $false) { - New-Item -ItemType Directory -Path $devTestLabsLocalPath - } - $azurePath = Join-Path -Path $azureStorageBaseDirectory -ChildPath "$(Tests.DataFolderName)" - Get-AzureStorageFile -ShareName $(AzureStorageShareName) -Context $azureContext -Path $azurePath | Get-AzureStorageFile | ?{$_.GetType().Name -eq "CloudFile"} | Get-AzureStorageFileContent -Destination $devTestLabsLocalPath - - displayName: 'Import resources' - - - task: automagically.DownloadFile.DownloadFile.DownloadFile@1 - displayName: 'Download ConfigureLab.ps1' - inputs: - FileUrl: 'https://raw.githubusercontent.com/Yvand/AzureRM-Templates/master/DevTestLabs-Artifacts/manage-artifacts/ConfigureLab.ps1' - DestinationFolder: '$(Build.ArtifactStagingDirectory)\$(Deployment.ConfigureServerFolderName)' - - - task: automagically.DownloadFile.DownloadFile.DownloadFile@1 - displayName: 'Download ConfigureLab.psm1' - inputs: - FileUrl: 'https://raw.githubusercontent.com/Yvand/AzureRM-Templates/master/DevTestLabs-Artifacts/manage-artifacts/ConfigureLab.psm1' - DestinationFolder: '$(Build.ArtifactStagingDirectory)\$(Deployment.ConfigureServerFolderName)' - - - task: NuGetToolInstaller@0 - displayName: 'Use NuGet 4.4.1' - inputs: - versionSpec: 4.4.1 - - - task: NuGetCommand@2 - displayName: 'NuGet restore' - inputs: - restoreSolution: '$(SolutionFileName)' - - - task: bleddynrichards.Assembly-Info-Task.Assembly-Info-Task.Assembly-Info-NetFramework@2 - displayName: 'Set $(system.teamProject) assemblies manifest' - inputs: - FileNames: '**\AssemblyInfo.cs' - Title: $(system.teamProject) - Product: $(system.teamProject) - Description: '$(ProductDescription)' - Company: GitHub.com/Yvand - Copyright: 'Copyright © $(date:YYYY) Yvan Duhamel, All rights reserved' - Trademark: '$(system.teamProject)' - VersionNumber: 1.0.0.0 - FileVersionNumber: '$(Build.BuildNumber)' - InformationalVersion: '$(Build.BuildNumber)' - - - task: VSBuild@1 - displayName: 'Build $(system.teamProject) solution' - inputs: - solution: '$(SolutionFileName)' - msbuildArgs: '/p:IsPackaging=true' - platform: '$(BuildPlatform)' - configuration: '$(configuration)' - msbuildArchitecture: x64 - - - task: CopyFiles@2 - displayName: 'Copy binaries to artifacts' - inputs: - SourceFolder: '$(Build.SourcesDirectory)' - Contents: | - $(system.teamProject)/bin/$(configuration)/?($(system.teamProject).*) - $(system.teamProject).Tests/bin/$(configuration)/?(*.dll) - TargetFolder: '$(Build.ArtifactStagingDirectory)' - - - task: CopyFiles@2 - displayName: 'Copy dependencies to artifacts' - inputs: - SourceFolder: '$(Build.SourcesDirectory)' - Contents: | - $(system.teamProject)/bin/$(configuration)/?(*.dll) - TargetFolder: '$(Build.ArtifactStagingDirectory)/dependencies' - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: drop' - inputs: - pathtoPublish: '$(Build.ArtifactStagingDirectory)' - artifactName: 'drop_$(configuration)' diff --git a/azure-pipelines/ci-create-dtl-environment.yml b/azure-pipelines/ci-create-dtl-environment.yml deleted file mode 100644 index 658ef1e..0000000 --- a/azure-pipelines/ci-create-dtl-environment.yml +++ /dev/null @@ -1,21 +0,0 @@ -resources: -- repo: self - -jobs: -- job: CreateTestEnvironment - displayName: Create test environment - timeoutInMinutes: 90 - pool: - vmImage: 'windows-latest' - steps: - - checkout: none #skip checking out the default repository resource - - task: ms-azuredevtestlabs.tasks.azure-dtl-task-createEnvironment.AzureDevTestLabsCreateEnvironment@3 - displayName: 'Create Azure DevTest Labs Environment' - inputs: - azureSubscription: '$(DevTestLabs.AzureConnectionName)' - LabId: '/subscriptions/$(DevTestLabs.AzureSubscriptionId)/resourceGroups/$(DevTestLabs.LabName)/providers/Microsoft.DevTestLab/labs/$(DevTestLabs.LabName)' - RepositoryId: '/subscriptions/$(DevTestLabs.AzureSubscriptionId)/resourcegroups/$(DevTestLabs.LabName)/providers/microsoft.devtestlab/labs/$(DevTestLabs.LabName)/artifactsources/$(DevTestLabs.RepoID)' - TemplateId: '/subscriptions/$(DevTestLabs.AzureSubscriptionId)/resourceGroups/$(DevTestLabs.LabName)/providers/Microsoft.DevTestLab/labs/$(DevTestLabs.LabName)/artifactSources/$(DevTestLabs.RepoID)/armTemplates/$(DevTestLabs.ARMTemplateName)' - EnvironmentName: 'Tests-$(system.teamProject)' - ParameterOverrides: "-provisionSharePoint2013 $(Deployment.ProvisionSharePoint2013) -provisionSharePoint2016 $(Deployment.ProvisionSharePoint2016) -provisionSharePoint2019 $(Deployment.ProvisionSharePoint2019) -provisionSharePointSubscription $(Deployment.ProvisionSharePointSubscription) -adminUserName '$(Deployment.AdminUserName)' -adminPassword '$(Deployment.AdminPassword)' -serviceAccountsPassword '$(Deployment.ServiceAccountsPassword)' -addPublicIPAddressToEachVM true -configureADFS true -addAzureBastion true -enableHybridBenefitServerLicenses true -enableAutomaticUpdates false" - timeoutInMinutes: 90 From aa41bffceb6d932c30cf2d88aa2bbf66c6a4db8c Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Fri, 2 Aug 2024 15:40:03 +0200 Subject: [PATCH 09/23] Improve tests (#281) * work * work * work * Update Populate-EntraIDTenant.ps1 * work * work * work; * work * Update Populate-EntraIDTenant.ps1 --- .../BasicConfigurationTests.cs | 31 ++-- Yvand.EntraCP.Tests/BypassDirectoryTests.cs | 13 +- .../ClaimsProviderTestsBase.cs | 147 +++++++++--------- Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs | 12 +- .../ExtensionAttributeTests.cs | 2 +- .../FilterUsersBasedOnGroupsTests.cs | 70 +++++++-- Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs | 4 +- .../SecurityEnabledGroupsTests.cs | 8 +- .../Setup/Populate-EntraIDTenant.ps1 | 90 ++++++----- Yvand.EntraCP.Tests/UnitTestsHelper.cs | 126 +++++---------- Yvand.EntraCP.Tests/local.runsettings | 4 +- 11 files changed, 249 insertions(+), 258 deletions(-) diff --git a/Yvand.EntraCP.Tests/BasicConfigurationTests.cs b/Yvand.EntraCP.Tests/BasicConfigurationTests.cs index 2907abc..d940bc4 100644 --- a/Yvand.EntraCP.Tests/BasicConfigurationTests.cs +++ b/Yvand.EntraCP.Tests/BasicConfigurationTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using System; -using System.Linq; namespace Yvand.EntraClaimsProvider.Tests { @@ -23,27 +22,28 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } - //[Test] - //public void TestRandomTestGroups([Random(0, UnitTestsHelper.TotalNumberTestGroups - 1, 5)] int idx) - //{ - // EntraIdTestGroup group = EntraIdTestGroupsSource.Groups[idx]; - // TestSearchAndValidateForEntraIDGroup(group); - //} - [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); + base.TestAugmentationAgainst1RandomGroup(user); + } + + [Test] + public void TestAGuestUser() + { + TestUser user = TestEntitySourceManager.GetOneUser(UserType.Guest); + base.TestSearchAndValidateForTestUser(user); } //[Test] - //public void TestRandomTestUsers([Random(0, UnitTestsHelper.TotalNumberTestUsers - 1, 5)] int idx) + //public void TestRandomUsers([Random(0, UnitTestsHelper.TotalNumberTestUsers - 1, 5)] int idx) //{ // var user = EntraIdTestUsersSource.Users[idx]; - // base.TestSearchAndValidateForEntraIDUser(user); + // base.TestSearchAndValidateForTestUser(user); //} [Test] @@ -55,11 +55,12 @@ public override void TestAugmentationOfGoldUsersAgainstRandomGroups() #if DEBUG [TestCase("testEntraCPUser_001")] - [TestCase("testEntraCPUser_020")] + [TestCase("testEntraCPUser_326")] public void DebugTestUser(string upnPrefix) { - TestUser user = TestEntitySourceManager.AllTestUsers.First(x => x.UserPrincipalName.StartsWith(upnPrefix)); - base.TestSearchAndValidateForEntraIDUser(user); + TestUser user = TestEntitySourceManager.FindUser(upnPrefix); + base.TestSearchAndValidateForTestUser(user); + base.TestAugmentationAgainst1RandomGroup(user); } [TestCase(@"testentracp", 30, "")] diff --git a/Yvand.EntraCP.Tests/BypassDirectoryTests.cs b/Yvand.EntraCP.Tests/BypassDirectoryTests.cs index 0d3689c..3d2463a 100644 --- a/Yvand.EntraCP.Tests/BypassDirectoryTests.cs +++ b/Yvand.EntraCP.Tests/BypassDirectoryTests.cs @@ -1,7 +1,6 @@ using Microsoft.SharePoint.Administration.Claims; using NUnit.Framework; using System.Security.Claims; -using Yvand.EntraClaimsProvider.Configuration; namespace Yvand.EntraClaimsProvider.Tests { @@ -29,20 +28,20 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); user.UserPrincipalName = user.DisplayName; user.Mail = user.DisplayName; user.DisplayName = $"{PrefixBypassUserSearch}{user.DisplayName}"; - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); group.Id = group.DisplayName; group.DisplayName = $"{PrefixBypassGroupSearch}{group.DisplayName}"; - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [TestCase(PrefixBypassUserSearch + "externalUser@contoso.com", 1, "externalUser@contoso.com")] @@ -80,13 +79,13 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } [Test] diff --git a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs index 4c2e010..dd817d8 100644 --- a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs +++ b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Text; using Yvand.EntraClaimsProvider.Configuration; +using static Azure.Core.HttpHeader; namespace Yvand.EntraClaimsProvider.Tests { @@ -47,35 +48,34 @@ public string GroupIdentifierClaimType protected EntraIDProviderSettings Settings = new EntraIDProviderSettings(); - private object _LockVerifyIfCurrentUserShouldBeFound = new object(); private object _LockInitGroupsWhichUsersMustBeMemberOfAny = new object(); - private List _GroupsWhichUsersMustBeMemberOfAny; + private bool GroupsWhichUsersMustBeMemberOfAnyReady = false; + private List _GroupsWhichUsersMustBeMemberOfAny = new List(); protected List GroupsWhichUsersMustBeMemberOfAny { get { - if (_GroupsWhichUsersMustBeMemberOfAny != null) { return _GroupsWhichUsersMustBeMemberOfAny; } + if (GroupsWhichUsersMustBeMemberOfAnyReady) { return _GroupsWhichUsersMustBeMemberOfAny; } lock (_LockInitGroupsWhichUsersMustBeMemberOfAny) { - if (_GroupsWhichUsersMustBeMemberOfAny != null) { return _GroupsWhichUsersMustBeMemberOfAny; } - _GroupsWhichUsersMustBeMemberOfAny = new List(); + if (GroupsWhichUsersMustBeMemberOfAnyReady) { return _GroupsWhichUsersMustBeMemberOfAny; } string groupsWhichUsersMustBeMemberOfAny = Settings.RestrictSearchableUsersByGroups; if (!String.IsNullOrWhiteSpace(groupsWhichUsersMustBeMemberOfAny)) { string[] groupIds = groupsWhichUsersMustBeMemberOfAny.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string groupId in groupIds) { - TestGroup groupSettings = TestEntitySourceManager.GroupsWithCustomSettings.FirstOrDefault(x => x.Id == groupId); - if (groupSettings == null) { groupSettings = new TestGroup(); } - _GroupsWhichUsersMustBeMemberOfAny.Add(groupSettings); + TestGroup group = TestEntitySourceManager.AllTestGroups.First(x => x.Id == groupId); + _GroupsWhichUsersMustBeMemberOfAny.Add(group); } } + GroupsWhichUsersMustBeMemberOfAnyReady = true; + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Initialized List of {nameof(GroupsWhichUsersMustBeMemberOfAny)} with {GroupsWhichUsersMustBeMemberOfAny.Count} items: {String.Join(", ", GroupsWhichUsersMustBeMemberOfAny.Select(x => x.Id).ToArray())}"); return _GroupsWhichUsersMustBeMemberOfAny; } } } - /// /// Initialize settings /// @@ -116,28 +116,12 @@ public virtual void CheckSettingsTest() } } - public void TestSearchAndValidateForEntraIDGroup(TestGroup entity) - { - string inputValue = entity.DisplayName; - int expectedCount = 1; - bool shouldValidate = true; - - if (Settings.AlwaysResolveUserInput) - { - inputValue = entity.Id; - expectedCount = Settings.ClaimTypes.GetConfigsMappedToClaimType().Count(); - } - if (Settings.FilterSecurityEnabledGroupsOnly && entity.SecurityEnabled == false) - { - expectedCount = 0; - shouldValidate = false; - } - - TestSearchOperation(inputValue, expectedCount, entity.Id); - TestValidationOperation(GroupIdentifierClaimType, entity.Id, shouldValidate); - } - - public void TestSearchAndValidateForEntraIDUser(TestUser entity) + /// + /// Tests the search and validation operations for the user specified and against the current configuration. + /// The property DisplayName is used as the people picker input + /// + /// + public void TestSearchAndValidateForTestUser(TestUser entity) { int expectedCount = 1; string inputValue = entity.DisplayName; @@ -146,46 +130,31 @@ public void TestSearchAndValidateForEntraIDUser(TestUser entity) if (Settings.AlwaysResolveUserInput) { - inputValue = entity.UserPrincipalName; - claimValue = entity.UserPrincipalName; + claimValue = inputValue; expectedCount = Settings.ClaimTypes.GetConfigsMappedToClaimType().Count(); } else { if (!String.IsNullOrWhiteSpace(Settings.RestrictSearchableUsersByGroups)) { - lock (_LockVerifyIfCurrentUserShouldBeFound) // TODO: understand why this lock is necessary - { - // Test 1: Does Settings.RestrictSearchableUsersByGroups contain any group where all test users are members? - bool groupWithAllTestUsersAreMembersFound = false; - foreach (var groupSettings in GroupsWhichUsersMustBeMemberOfAny) - { - if (groupSettings.AllTestUsersAreMembers) - { - groupWithAllTestUsersAreMembersFound = true; - Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] User \"{entity.UserPrincipalName}\" may be found because Settings.RestrictSearchableUsersByGroups contains group: \"{groupSettings.DisplayName}\" with AllTestUsersAreMembers {groupSettings.AllTestUsersAreMembers}."); - break; // No need to change shouldValidate, which is true by default, or process other groups - } - } + // Test 1: Does Settings.RestrictSearchableUsersByGroups contain any group where all test users are members? + bool groupEveryoneIsMemberOfFound = GroupsWhichUsersMustBeMemberOfAny.Any(x => x.EveryoneIsMember == true); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] User \"{entity.UserPrincipalName}\" may be found because at least 1 of the groups in Settings.RestrictSearchableUsersByGroups has property EveryoneIsMember true."); - // Test 2: If test 1 is false, is current entity member of all the test groups? - if (!groupWithAllTestUsersAreMembersFound) + // Test 2: If test 1 is false, is current entity member of all the test groups? + if (!groupEveryoneIsMemberOfFound) + { + if (!entity.IsMemberOfAllGroups) { - - TestUser userSettings = TestEntitySourceManager.UsersWithCustomSettings.FirstOrDefault(x => String.Equals(x.UserPrincipalName, entity.UserPrincipalName, StringComparison.InvariantCultureIgnoreCase)); - if (userSettings == null) { userSettings = new TestUser(); } - if (!userSettings.IsMemberOfAllGroups) - { - shouldValidate = false; - expectedCount = 0; - Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] User \"{entity.UserPrincipalName}\" should not be found because it has IsMemberOfAllGroups {userSettings.IsMemberOfAllGroups} and no group set in Settings.RestrictSearchableUsersByGroups has AllTestUsersAreMembers set to true."); - } + shouldValidate = false; + expectedCount = 0; + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] User \"{entity.UserPrincipalName}\" should not be found because it has IsMemberOfAllGroups {entity.IsMemberOfAllGroups} and no group set in Settings.RestrictSearchableUsersByGroups has AllTestUsersAreMembers set to true."); } } } else { - Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Property Settings.RestrictSearchableUsersByGroups IsNullOrWhiteSpace."); + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] Property Settings.RestrictSearchableUsersByGroups is not set."); } // If shouldValidate is false, user should not be found anyway so no need to do additional checks @@ -214,32 +183,55 @@ public void TestSearchAndValidateForEntraIDUser(TestUser entity) } /// - /// Gold users are the test users who are members of all the test groups + /// Tests the search and validation operations for the group specified and against the current configuration. + /// The property DisplayName is used as the people picker input /// - public virtual void TestAugmentationOfGoldUsersAgainstRandomGroups() + /// + public void TestSearchAndValidateForTestGroup(TestGroup entity) { - Random rnd = new Random(); - int randomIdx = rnd.Next(0, TestEntitySourceManager.AllTestGroups.Count - 1); - Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOfGoldUsersAgainstRandomGroups: Get group in EntraIdTestGroupsSource.Groups at index {randomIdx}."); - TestGroup randomGroup = null; - try + string inputValue = entity.DisplayName; + string claimValue = entity.Id; + int expectedCount = 1; + bool shouldValidate = true; + + if (Settings.AlwaysResolveUserInput) { - randomGroup = TestEntitySourceManager.AllTestGroups[randomIdx]; + claimValue = inputValue; + expectedCount = Settings.ClaimTypes.GetConfigsMappedToClaimType().Count(); } - catch (ArgumentOutOfRangeException) + if (Settings.FilterSecurityEnabledGroupsOnly && entity.SecurityEnabled == false) { - string errorMessage = $"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationOfGoldUsersAgainstRandomGroups: Could not get group in EntraIdTestGroupsSource.Groups at index {randomIdx}. EntraIdTestGroupsSource.Groups has {TestEntitySourceManager.AllTestGroups.Count} items."; - Trace.TraceError(errorMessage); - throw new ArgumentOutOfRangeException(errorMessage); + expectedCount = 0; + shouldValidate = false; } - bool shouldBeMember = Settings.FilterSecurityEnabledGroupsOnly && !randomGroup.SecurityEnabled ? false : true; - foreach (string userPrincipalName in TestEntitySourceManager.UsersWithCustomSettings.Where(x => x.IsMemberOfAllGroups).Select(x => x.UserPrincipalName)) + TestSearchOperation(inputValue, expectedCount, claimValue); + TestValidationOperation(GroupIdentifierClaimType, claimValue, shouldValidate); + } + + /// + /// Gold users are the test users who are members of all the test groups + /// + public virtual void TestAugmentationOfGoldUsersAgainstRandomGroups() + { + foreach (TestUser user in TestEntitySourceManager.GetUsersMembersOfAllGroups()) { - TestAugmentationOperation(userPrincipalName, shouldBeMember, randomGroup.Id); + TestAugmentationAgainst1RandomGroup(user); } } + /// + /// Pick a random group, and check if the claims provider returns the expected membership (should or should not be member) for this group + /// + /// + public void TestAugmentationAgainst1RandomGroup(TestUser user) + { + TestGroup randomGroup = TestEntitySourceManager.GetOneGroup(Settings.FilterSecurityEnabledGroupsOnly); + bool userShouldBeMember = user.IsMemberOfAllGroups || randomGroup.EveryoneIsMember ? true : false; + Trace.TraceInformation($"{DateTime.Now:s} [{this.GetType().Name}] TestAugmentationAgainst1RandomGroup for user \"{user.UserPrincipalName}\", IsMemberOfAllGroupsp: {user.IsMemberOfAllGroups} against group \"{randomGroup.DisplayName}\". userShouldBeMember: {userShouldBeMember}"); + TestAugmentationOperation(user.UserPrincipalName, userShouldBeMember, randomGroup.Id); + } + /// /// Applies the to the configuration object and save it in the configuration database /// @@ -351,10 +343,11 @@ protected void TestValidationOperation(SPClaim inputClaim, bool shouldValidate, /// /// /// - protected void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup, string groupNameToTestInGroupMembership) + /// + protected void TestAugmentationOperation(string claimValue, bool isMemberOfTrustedGroup, string groupClaimValueToTest) { string claimType = UserIdentifierClaimType; - SPClaim groupClaimToTestInGroupMembership = new SPClaim(GroupIdentifierClaimType, groupNameToTestInGroupMembership, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); + SPClaim groupClaimToTestInGroupMembership = new SPClaim(GroupIdentifierClaimType, groupClaimValueToTest, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name)); try { Stopwatch timer = new Stopwatch(); @@ -372,11 +365,11 @@ protected void TestAugmentationOperation(string claimValue, bool isMemberOfTrust if (isMemberOfTrustedGroup) { - Assert.That(groupFound, Is.True, $"Entity \"{claimValue}\" should be member of group \"{groupNameToTestInGroupMembership}\", but this group was not found in the claims returned by the claims provider."); + Assert.That(groupFound, Is.True, $"Entity \"{claimValue}\" should be member of group \"{groupClaimValueToTest}\", but this group was not found in the claims returned by the claims provider."); } else { - Assert.That(groupFound, Is.False, $"Entity \"{claimValue}\" should NOT be member of group \"{groupNameToTestInGroupMembership}\", but this group was found in the claims returned by the claims provider."); + Assert.That(groupFound, Is.False, $"Entity \"{claimValue}\" should NOT be member of group \"{groupClaimValueToTest}\", but this group was found in the claims returned by the claims provider."); } timer.Stop(); Trace.TraceInformation($"{DateTime.Now:s} TestAugmentationOperation finished in {timer.ElapsedMilliseconds} ms. Parameters: claimType: '{claimType}', claimValue: '{claimValue}', isMemberOfTrustedGroup: '{isMemberOfTrustedGroup}'."); diff --git a/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs b/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs index b9ad505..f1f1602 100644 --- a/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs +++ b/Yvand.EntraCP.Tests/ExcludeAUserTypeTests.cs @@ -24,13 +24,13 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } [Test] @@ -63,13 +63,13 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } [Test] @@ -102,13 +102,13 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } [Test] diff --git a/Yvand.EntraCP.Tests/ExtensionAttributeTests.cs b/Yvand.EntraCP.Tests/ExtensionAttributeTests.cs index 79686cf..a3c4737 100644 --- a/Yvand.EntraCP.Tests/ExtensionAttributeTests.cs +++ b/Yvand.EntraCP.Tests/ExtensionAttributeTests.cs @@ -13,7 +13,7 @@ public override void InitializeSettings() ClaimTypeConfig ctConfigExtensionAttribute = new ClaimTypeConfig { ClaimType = TestContext.Parameters["MultiPurposeCustomClaimType"], - ClaimTypeDisplayName = "extattr1", + ClaimTypeDisplayName = "extrattr1", EntityProperty = DirectoryObjectProperty.extensionAttribute1, EntityType = DirectoryObjectType.User, SharePointEntityType = ClaimsProviderConstants.GroupClaimEntityType, diff --git a/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs b/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs index 3e8647b..693df47 100644 --- a/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs +++ b/Yvand.EntraCP.Tests/FilterUsersBasedOnGroupsTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -26,7 +25,21 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); + base.TestAugmentationAgainst1RandomGroup(user); + } + + [Test] + public void TestAGuestUser() + { + TestUser user = TestEntitySourceManager.GetOneUser(UserType.Guest); + base.TestSearchAndValidateForTestUser(user); + } + + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) + { + TestSearchAndValidateForTestGroup(group); } #if DEBUG @@ -34,8 +47,8 @@ public void TestUsers(TestUser user) [TestCase("testEntraCPUser_020")] public void DebugTestUser(string upnPrefix) { - TestUser user = TestEntitySourceManager.AllTestUsers.Find(x => x.UserPrincipalName.StartsWith(upnPrefix)); - base.TestSearchAndValidateForEntraIDUser(user); + TestUser user = TestEntitySourceManager.FindUser(upnPrefix); + base.TestSearchAndValidateForTestUser(user); } #endif } @@ -65,7 +78,21 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); + base.TestAugmentationAgainst1RandomGroup(user); + } + + [Test] + public void TestAGuestUser() + { + TestUser user = TestEntitySourceManager.GetOneUser(UserType.Guest); + base.TestSearchAndValidateForTestUser(user); + } + + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) + { + TestSearchAndValidateForTestGroup(group); } #if DEBUG @@ -74,12 +101,11 @@ public void TestUsers(TestUser user) public void DebugTestUser(string upnPrefix) { TestUser user = TestEntitySourceManager.FindUser(upnPrefix); - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } #endif } -#if DEBUG [TestFixture] [Parallelizable(ParallelScope.Children)] public class DebugFilterUsersBasedOnMultipleGroupsTests : ClaimsProviderTestsBase @@ -92,20 +118,32 @@ public override void InitializeSettings() base.ApplySettings(); } - [TestCase("testEntraCPUser_001")] - [TestCase("testEntraCPUser_020")] - public void DebugTestUser(string upnPrefix) + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] + public void TestUsers(TestUser user) { - TestUser user = TestEntitySourceManager.FindUser(upnPrefix); - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); + base.TestAugmentationAgainst1RandomGroup(user); } [Test] - public void DebugGuestUser() + public void TestAGuestUser() + { + TestUser user = TestEntitySourceManager.GetOneUser(UserType.Guest); + base.TestSearchAndValidateForTestUser(user); + } + + [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] + public void TestGroups(TestGroup group) { - TestUser user = TestEntitySourceManager.AllTestUsers.Find(x => x.Mail.StartsWith("testEntraCPGuestUser_001")); - base.TestSearchAndValidateForEntraIDUser(user); + TestSearchAndValidateForTestGroup(group); + } + + [TestCase("testEntraCPUser_001")] + [TestCase("testEntraCPUser_020")] + public void DebugTestUser(string upnPrefix) + { + TestUser user = TestEntitySourceManager.FindUser(upnPrefix); + base.TestSearchAndValidateForTestUser(user); } } -#endif } diff --git a/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs b/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs index 0b4984b..69bd467 100644 --- a/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs +++ b/Yvand.EntraCP.Tests/GuestAccountsUPNTests.cs @@ -27,13 +27,13 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeUsers), new object[] { TestEntitySourceManager.MaxNumberOfUsersToTest })] public void TestUsers(TestUser user) { - base.TestSearchAndValidateForEntraIDUser(user); + base.TestSearchAndValidateForTestUser(user); } [Test] diff --git a/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs b/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs index 0f395c6..bb8634a 100644 --- a/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs +++ b/Yvand.EntraCP.Tests/SecurityEnabledGroupsTests.cs @@ -1,6 +1,4 @@ -using Microsoft.SharePoint.Administration.Claims; -using NUnit.Framework; -using System.Security.Claims; +using NUnit.Framework; using Yvand.EntraClaimsProvider.Configuration; namespace Yvand.EntraClaimsProvider.Tests @@ -27,7 +25,7 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test] @@ -60,7 +58,7 @@ public override void CheckSettingsTest() [Test, TestCaseSource(typeof(TestEntitySourceManager), nameof(TestEntitySourceManager.GetSomeGroups), new object[] { TestEntitySourceManager.MaxNumberOfGroupsToTest, true })] public void TestGroups(TestGroup group) { - TestSearchAndValidateForEntraIDGroup(group); + TestSearchAndValidateForTestGroup(group); } [Test] diff --git a/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 b/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 index d15c29c..4733dad 100644 --- a/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 +++ b/Yvand.EntraCP.Tests/Setup/Populate-EntraIDTenant.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Users, Microsoft.Graph.Groups +#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Users, Microsoft.Graph.Groups <# .SYNOPSIS @@ -12,9 +12,14 @@ Connect-MgGraph -Scopes "User.ReadWrite.All", "Group.ReadWrite.All" -UseDeviceCode $tenantName = (Get-MgOrganization).VerifiedDomains[0].Name +$exportedUsersFullFilePath = "C:\YvanData\dev\EntraCP_Tests_Users.csv" +$exportedGroupsFullFilePath = "C:\YvanData\dev\EntraCP_Tests_Groups.csv" + $memberUsersNamePrefix = "testEntraCPUser_" $guestUsersNamePrefix = "testEntraCPGuestUser_" $groupNamePrefix = "testEntraCPGroup_" +$usersCount = 999 +$groupsCount = 50 $confirmation = Read-Host "Connected to tenant '$tenantName' and about to process users starting with '$memberUsersNamePrefix' and groups starting with '$groupNamePrefix'. Are you sure you want to proceed? [y/n]" if ($confirmation -ne 'y') { @@ -22,7 +27,6 @@ if ($confirmation -ne 'y') { return } -# Set specific attributes for some users $usersWithSpecificSettings = @( @{ UserPrincipalName = "$($memberUsersNamePrefix)001@$($tenantName)"; IsMemberOfAllGroups = $true } @{ UserPrincipalName = "$($memberUsersNamePrefix)002@$($tenantName)"; UserAttributes = @{ "GivenName" = "firstname 002" } } @@ -38,16 +42,16 @@ $guestUsersList = @( @{ Mail = "$($guestUsersNamePrefix)002@contoso.local"; Id = ""; UserPrincipalName = "" } @{ Mail = "$($guestUsersNamePrefix)003@contoso.local"; Id = ""; UserPrincipalName = "" } ) -#$guestUsers = @("$($guestUsersNamePrefix)001@contoso.local", "$($guestUsersNamePrefix)002@contoso.local", "$($guestUsersNamePrefix)003@contoso.local") +$usersMemberOfAllGroups = [System.Linq.Enumerable]::Where($usersWithSpecificSettings, [Func[object, bool]] { param($x) $x.IsMemberOfAllGroups -eq $true }) $groupsWithSpecificSettings = @( @{ GroupName = "$($groupNamePrefix)001" SecurityEnabled = $false - AllTestUsersAreMembers = $true + EveryoneIsMember = $true }, @{ GroupName = "$($groupNamePrefix)005" - AllTestUsersAreMembers = $true + EveryoneIsMember = $true }, @{ GroupName = "$($groupNamePrefix)008" @@ -56,11 +60,11 @@ $groupsWithSpecificSettings = @( @{ GroupName = "$($groupNamePrefix)018" SecurityEnabled = $false - AllTestUsersAreMembers = $true + EveryoneIsMember = $true }, @{ GroupName = "$($groupNamePrefix)025" - AllTestUsersAreMembers = $true + EveryoneIsMember = $true }, @{ GroupName = "$($groupNamePrefix)028" @@ -69,7 +73,7 @@ $groupsWithSpecificSettings = @( @{ GroupName = "$($groupNamePrefix)038" SecurityEnabled = $false - AllTestUsersAreMembers = $true + EveryoneIsMember = $true }, @{ GroupName = "$($groupNamePrefix)048" @@ -94,11 +98,11 @@ $passwordProfile = @{ } # Bulk add users -$totalUsers = 1000 -for ($i = 1; $i -le $totalUsers; $i++) { +$allUsersInEntra = @() +for ($i = 1; $i -le $usersCount; $i++) { $accountName = "$($memberUsersNamePrefix)$("{0:D3}" -f $i)" $userPrincipalName = "$($accountName)@$($tenantName)" - $user = Get-MgUser -Filter "UserPrincipalName eq '$userPrincipalName'" + $user = Get-MgUser -Filter "UserPrincipalName eq '$userPrincipalName'" -Property Id, UserPrincipalName, Mail, UserType, DisplayName, GivenName if ($null -eq $user) { $additionalUserAttributes = New-Object -TypeName HashTable $userHasSpecificAttributes = [System.Linq.Enumerable]::FirstOrDefault($usersWithSpecificSettings, [Func[object, bool]] { param($x) $x.UserPrincipalName -like $userPrincipalName }) @@ -106,30 +110,28 @@ for ($i = 1; $i -le $totalUsers; $i++) { $additionalUserAttributes = $userHasSpecificAttributes.UserAttributes } - New-MgUser -DisplayName $accountName -PasswordProfile $passwordProfile -AccountEnabled -MailNickName $accountName -UserPrincipalName $userPrincipalName @additionalUserAttributes + New-MgUser -UserPrincipalName $userPrincipalName -DisplayName $accountName -PasswordProfile $passwordProfile -AccountEnabled -MailNickName $accountName @additionalUserAttributes Write-Host "Created user '$userPrincipalName'" -ForegroundColor Green + $user = Get-MgUser -Filter "UserPrincipalName eq '$userPrincipalName'" -Property Id, UserPrincipalName, Mail, UserType, DisplayName, GivenName } + $allUsersInEntra += $user } # Add the guest users foreach ($guestUser in $guestUsersList) { - $guestUserInGraph = Get-MgUser -Filter "Mail eq '$($guestUser.Mail)'" - if ($null -eq $guestUserInGraph) { + $user = Get-MgUser -Filter "Mail eq '$($guestUser.Mail)'" -Property Id, UserPrincipalName, Mail, UserType, DisplayName, GivenName + if ($null -eq $user) { $invitedUser = New-MgInvitation -InvitedUserDisplayName $guestUser.Mail -InvitedUserEmailAddress $guestUser.Mail -SendInvitationMessage:$false -InviteRedirectUrl "https://myapplications.microsoft.com" Write-Host "Invited guest user $($invitedUser.InvitedUserEmailAddress)" -ForegroundColor Green - $guestUserInGraph = $invitedUser.InvitedUser + $user = $invitedUser.InvitedUser + $user = Get-MgUser -Filter "Mail eq '$($guestUser.Mail)'" -Property Id, UserPrincipalName, Mail, UserType, DisplayName, GivenName } - $guestUser.Id = $guestUserInGraph.Id - $guestUser.UserPrincipalName = $guestUserInGraph.UserPrincipalName + $allUsersInEntra += $user } -# groups -$allMemberUsersInEntra = Get-MgUser -ConsistencyLevel eventual -Count userCount -Filter "startsWith(DisplayName, '$($memberUsersNamePrefix)')" -OrderBy UserPrincipalName -Top $totalUsers -$usersMemberOfAllGroups = [System.Linq.Enumerable]::Where($usersWithSpecificSettings, [Func[object, bool]] { param($x) $x.IsMemberOfAllGroups -eq $true }) - -# Bulk add groups -$totalGroups = 50 -for ($i = 1; $i -le $totalGroups; $i++) { +# Bulk add groups and set their membership +$allGroupsInEntra = @() +for ($i = 1; $i -le $groupsCount; $i++) { $groupName = "$($groupNamePrefix)$("{0:D3}" -f $i)" $groupSettings = [System.Linq.Enumerable]::FirstOrDefault($groupsWithSpecificSettings, [Func[object, bool]] { param($x) $x.GroupName -like $groupName }) $entraGroup = Get-MgGroup -Filter "DisplayName eq '$($groupName)'" @@ -147,30 +149,31 @@ for ($i = 1; $i -le $totalGroups; $i++) { Write-Host "Created group $groupName" -ForegroundColor Green $entraGroupJustCreated = $true } + $allGroupsInEntra += $entraGroup if ($false -eq $entraGroupJustCreated) { # Remove all members - $existingGroupMembers = Get-MgGroupMember -GroupId $entraGroup.Id + $existingGroupMembers = Get-MgGroupMember -GroupId $entraGroup.Id -All foreach ($groupMember in $existingGroupMembers) { Remove-MgGroupMemberByRef -GroupId $entraGroup.Id -DirectoryObjectId $groupMember.Id } Write-Host "Removed all members of existing group $($entraGroup.DisplayName)." -ForegroundColor Green } - # Set membership - $newGroupMembers = $usersMemberOfAllGroups | Select-Object -ExpandProperty UserPrincipalName + # Set group membership $newGroupMemberIds = New-Object -TypeName "System.Collections.Generic.List[System.String]" - if ($null -ne $groupSettings -and $groupSettings.ContainsKey("AllTestUsersAreMembers") -and $groupSettings["AllTestUsersAreMembers"] -eq $true) { - $newGroupMembers = $allMemberUsersInEntra.UserPrincipalName - - foreach ($guestUser in $guestUsersList) { - $newGroupMemberIds.Add($guestUser.Id) + if ($null -ne $groupSettings -and $groupSettings.ContainsKey("EveryoneIsMember") -and $groupSettings["EveryoneIsMember"] -eq $true) { + # Everyone is mmember of this group + foreach($userInEntra in $allUsersInEntra) { + $newGroupMemberIds.Add($userInEntra.Id) + } + } else { + # Only users with IsMemberOfAllGroups true are members of this group + foreach($upnOfUserMemberOfAllGroups in $usersMemberOfAllGroups | Select-Object -ExpandProperty UserPrincipalName) { + $upnOfUserMemberOfAllGroups + $userInEntra = [System.Linq.Enumerable]::First($allUsersInEntra, [Func[object, bool]] { param($x) $x.UserPrincipalName -like $upnOfUserMemberOfAllGroups }) + $newGroupMemberIds.Add($userInEntra.Id) } - } - - foreach ($groupMember in $newGroupMembers) { - $entraUser = [System.Linq.Enumerable]::FirstOrDefault($allMemberUsersInEntra, [Func[object, bool]] { param($x) $x.UserPrincipalName -like $groupMember }) - $newGroupMemberIds.Add($entraUser.Id) } # $newGroupMemberIds = $newGroupMemberIds | Select-Object -Unique @@ -179,3 +182,16 @@ for ($i = 1; $i -le $totalGroups; $i++) { } Write-Host "Added $($newGroupMemberIds.Count) member(s) to group $($entraGroup.DisplayName)" -ForegroundColor Green } + +# export users and groups to their CSV file +$allUsersInEntra | +Select-Object -Property Id, UserPrincipalName, Mail, UserType, DisplayName, GivenName, @{ Name = "IsMemberOfAllGroups"; Expression = { if ([System.Linq.Enumerable]::FirstOrDefault($usersWithSpecificSettings, [Func[object, bool]] { param($x) $x.UserPrincipalName -like $_.UserPrincipalName }).IsMemberOfAllGroups) { $true } else { $false } } } | +Export-Csv -Path $exportedUsersFullFilePath -NoTypeInformation +Write-Host "Exported Entra users to CSV file $($exportedUsersFullFilePath)" -ForegroundColor Green + +$allGroupsInEntra | +Select-Object -Property Id, DisplayName, SecurityEnabled, +@{ Name = "EveryoneIsMember"; Expression = { if ([System.Linq.Enumerable]::FirstOrDefault($groupsWithSpecificSettings, [Func[object, bool]] { param($x) $x.GroupName -like $_.DisplayName }).EveryoneIsMember) { $true } else { $false } } }, +@{ Name = "GroupType"; Expression = { $_.GroupTypes[0] } } | +Export-Csv -Path $exportedGroupsFullFilePath -NoTypeInformation +Write-Host "Exported Entra groups to CSV file $($exportedGroupsFullFilePath)" -ForegroundColor Green diff --git a/Yvand.EntraCP.Tests/UnitTestsHelper.cs b/Yvand.EntraCP.Tests/UnitTestsHelper.cs index 05b6128..28375b1 100644 --- a/Yvand.EntraCP.Tests/UnitTestsHelper.cs +++ b/Yvand.EntraCP.Tests/UnitTestsHelper.cs @@ -212,6 +212,7 @@ public override void SetEntityFromDataSourceRow(Row row) UserType = String.Equals(row["userType"], ClaimsProviderConstants.MEMBER_USERTYPE, StringComparison.InvariantCultureIgnoreCase) ? UserType.Member : UserType.Guest; Mail = row["mail"]; GivenName = row["givenName"]; + IsMemberOfAllGroups = Convert.ToBoolean(row["IsMemberOfAllGroups"]); } } @@ -219,7 +220,7 @@ public class TestGroup : TestEntity { public string GroupType; public bool SecurityEnabled = true; - public bool AllTestUsersAreMembers = false; + public bool EveryoneIsMember; public override void SetEntityFromDataSourceRow(Row row) { @@ -227,6 +228,7 @@ public override void SetEntityFromDataSourceRow(Row row) DisplayName = row["displayName"]; GroupType = row["groupType"]; SecurityEnabled = Convert.ToBoolean(row["SecurityEnabled"]); + EveryoneIsMember = Convert.ToBoolean(row["EveryoneIsMember"]); } } @@ -239,20 +241,21 @@ public enum UserType public class TestEntitySource where T : TestEntity, new() { private object _LockInitEntitiesList = new object(); - private List _Entities; + private bool EntitiesReady = false; + private List _Entities = new List(); public List Entities { get { - if (_Entities != null) { return _Entities; } + if (EntitiesReady) { return _Entities; } lock (_LockInitEntitiesList) { - if (_Entities != null) { return _Entities; } - _Entities = new List(); + if (EntitiesReady) { return _Entities; } foreach (T entity in ReadDataSource()) { _Entities.Add(entity); } + EntitiesReady = true; Trace.TraceInformation($"{DateTime.Now:s} [{typeof(T).Name}] Initialized List of {nameof(Entities)} with {Entities.Count} items."); return _Entities; } @@ -280,94 +283,19 @@ private IEnumerable ReadDataSource() public IEnumerable GetSomeEntities(int count, Func filter = null) { - if (count > Entities.Count) - { - count = Entities.Count; - } - - int randomNumberMaxValue = Entities.Where(filter ?? (x => true)).Count() - 1; - List entitiesIdxs = new List(count); + IEnumerable entitiesFiltered = Entities.Where(filter ?? (x => true)); + int entitiesFilteredCount = entitiesFiltered.Count(); + if (count > entitiesFilteredCount) { count = entitiesFilteredCount; } for (int i = 0; i < count; i++) { - entitiesIdxs.Add(RandomNumber.Next(0, randomNumberMaxValue)); - } - - foreach (int userIdx in entitiesIdxs) - { - yield return Entities[userIdx].Clone() as T; + int randomIdx = RandomNumber.Next(0, entitiesFilteredCount); + yield return entitiesFiltered.ElementAt(randomIdx).Clone() as T; } } } public class TestEntitySourceManager { - private static TestUser[] UsersWithCustomSettingsDefinition = new[] - { - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}001@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}010@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}011@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}012@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}013@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}014@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - new TestUser { UserPrincipalName = $"{UnitTestsHelper.TestUsersAccountNamePrefix}015@{UnitTestsHelper.TenantConnection.Name}" , IsMemberOfAllGroups = true }, - }; - private static TestGroup[] GroupsWithCustomSettingsDefinition = new[] - { - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}001" , SecurityEnabled = false, AllTestUsersAreMembers = true}, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}005" , SecurityEnabled = true, AllTestUsersAreMembers = true }, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}008" , SecurityEnabled = false, AllTestUsersAreMembers = false }, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}018" , SecurityEnabled = false, AllTestUsersAreMembers = true }, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}025" , SecurityEnabled = true, AllTestUsersAreMembers = true }, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}028" , SecurityEnabled = false, AllTestUsersAreMembers = false, }, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}038" , SecurityEnabled = false, AllTestUsersAreMembers = true, }, - new TestGroup { DisplayName = $"{UnitTestsHelper.TestGroupsAccountNamePrefix}048" , SecurityEnabled = false, AllTestUsersAreMembers = false, }, - }; - - private static object _LockInitUsersWithCustomSettings = new object(); - private static List _UsersWithCustomSettings; - public static List UsersWithCustomSettings - { - get - { - if (_UsersWithCustomSettings != null) { return _UsersWithCustomSettings; } - lock (_LockInitGroupsWithCustomSettings) - { - if (_UsersWithCustomSettings != null) { return _UsersWithCustomSettings; } - _UsersWithCustomSettings = new List(); - foreach (TestUser userDefinition in UsersWithCustomSettingsDefinition) - { - TestUser user = AllTestUsers.First(x => String.Equals(x.UserPrincipalName, userDefinition.UserPrincipalName, StringComparison.OrdinalIgnoreCase)); - user.IsMemberOfAllGroups = userDefinition.IsMemberOfAllGroups; - _UsersWithCustomSettings.Add(user); - } - } - return _UsersWithCustomSettings; - } - } - - private static object _LockInitGroupsWithCustomSettings = new object(); - private static List _GroupsWithCustomSettings; - public static List GroupsWithCustomSettings - { - get - { - if (_GroupsWithCustomSettings != null) { return _GroupsWithCustomSettings; } - lock (_LockInitGroupsWithCustomSettings) - { - if (_GroupsWithCustomSettings != null) { return _GroupsWithCustomSettings; } - _GroupsWithCustomSettings = new List(); - foreach (TestGroup groupDefinition in GroupsWithCustomSettingsDefinition) - { - TestGroup group = AllTestGroups.First(x => x.DisplayName == groupDefinition.DisplayName); - group.SecurityEnabled = groupDefinition.SecurityEnabled; - group.AllTestUsersAreMembers = groupDefinition.AllTestUsersAreMembers; - _GroupsWithCustomSettings.Add(group); - } - } - return _GroupsWithCustomSettings; - } - } - private static TestEntitySource TestUsersSource = new TestEntitySource(UnitTestsHelper.DataFile_EntraId_TestUsers); public static List AllTestUsers { @@ -386,21 +314,39 @@ public static IEnumerable GetSomeUsers(int count) return TestUsersSource.GetSomeEntities(count, null); } + public static IEnumerable GetUsersMembersOfAllGroups() + { + Func filter = x => x.IsMemberOfAllGroups == true; + return TestUsersSource.GetSomeEntities(Int16.MaxValue, filter); + } + public static TestUser FindUser(string upnPrefix) { - return TestUsersSource.Entities.First(x => x.UserPrincipalName.StartsWith(upnPrefix)).Clone() as TestUser; + Func filter = x => x.UserPrincipalName.StartsWith(upnPrefix); + return TestUsersSource.GetSomeEntities(1, filter).First(); + } + + public static TestUser GetOneUser(UserType userType) + { + Func filter = x => x.UserType == userType; + return TestUsersSource.GetSomeEntities(1, filter).First(); } public static IEnumerable GetSomeGroups(int count, bool securityEnabledOnly) { - Func securityEnabledOnlyFilter = x => x.SecurityEnabled == securityEnabledOnly; - return TestGroupsSource.GetSomeEntities(count, securityEnabledOnlyFilter); + Func filter = x => x.SecurityEnabled == securityEnabledOnly; + return TestGroupsSource.GetSomeEntities(count, filter); + } + + public static TestGroup GetOneGroup() + { + return TestGroupsSource.GetSomeEntities(1, null).First(); } public static TestGroup GetOneGroup(bool securityEnabledOnly) { - Func securityEnabledOnlyFilter = x => x.SecurityEnabled == securityEnabledOnly; - return TestGroupsSource.GetSomeEntities(1, securityEnabledOnlyFilter).First(); + Func filter = x => x.SecurityEnabled == securityEnabledOnly; + return TestGroupsSource.GetSomeEntities(1, filter).First(); } } } \ No newline at end of file diff --git a/Yvand.EntraCP.Tests/local.runsettings b/Yvand.EntraCP.Tests/local.runsettings index 91ea265..57a51ee 100644 --- a/Yvand.EntraCP.Tests/local.runsettings +++ b/Yvand.EntraCP.Tests/local.runsettings @@ -8,8 +8,8 @@ - - + + From 9607a25d0bd4a7b5de27572b6a2a8982baa4a8c4 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Mon, 5 Aug 2024 15:52:24 +0200 Subject: [PATCH 10/23] Update reusable-prepare-dtl-env.yml --- .github/workflows/reusable-prepare-dtl-env.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/reusable-prepare-dtl-env.yml b/.github/workflows/reusable-prepare-dtl-env.yml index 77890b8..d454ec2 100644 --- a/.github/workflows/reusable-prepare-dtl-env.yml +++ b/.github/workflows/reusable-prepare-dtl-env.yml @@ -83,6 +83,9 @@ on: required: true description: 'Azure storage account file share name where unit test files are located' +permissions: + id-token: write + jobs: create-test-environment: if: inputs.skip-create-environment != true From e98b87c4daa681410b4b155518927835c351c996 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Mon, 5 Aug 2024 16:39:50 +0200 Subject: [PATCH 11/23] Update reusable-prepare-dtl-env.yml --- .github/workflows/reusable-prepare-dtl-env.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-prepare-dtl-env.yml b/.github/workflows/reusable-prepare-dtl-env.yml index d454ec2..4ed48b6 100644 --- a/.github/workflows/reusable-prepare-dtl-env.yml +++ b/.github/workflows/reusable-prepare-dtl-env.yml @@ -97,9 +97,11 @@ jobs: dtl_provisionSharePoint2016: ${{ contains(inputs.sharepoint-versions, '2016') }} steps: - name: Azure Login - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Create DevTest Labs environment uses: azure/CLI@v1 From 4006923b2b11d80e3c4041e8ccf0f77cecb0454d Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Mon, 5 Aug 2024 16:53:51 +0200 Subject: [PATCH 12/23] Update run-tests.yml --- .github/workflows/run-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ac71b5e..e448e93 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -34,7 +34,9 @@ jobs: test-claimsProviderPackageUri: ${{ vars.DTL_ClaimsProviderPackageUri }} unittestfiles-storageAccountSourceRelativePath: ${{ vars.unittestfiles_storageAccountSourceRelativePath }} secrets: - AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} azdevops-pat-registerAgent: ${{ secrets.dtl_azdevopsPassword }} dtl-env-accountsPassword: ${{ secrets.dtl_accountsPassword }} unittestfiles-storageAccountEndpoint: ${{ secrets.unittestfiles_storageAccountEndpoint }} From 62ec3356706235b071c76d2ba9ef3f612010ac86 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 6 Aug 2024 15:17:57 +0200 Subject: [PATCH 13/23] simplify the setup of tests --- .../ClaimsProviderTestsBase.cs | 6 +- Yvand.EntraCP.Tests/UnitTestsHelper.cs | 61 ++++++++----------- Yvand.EntraCP.Tests/local.runsettings | 8 +-- 3 files changed, 30 insertions(+), 45 deletions(-) diff --git a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs index dd817d8..dd893ba 100644 --- a/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs +++ b/Yvand.EntraCP.Tests/ClaimsProviderTestsBase.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Security.Claims; using System.Text; using Yvand.EntraClaimsProvider.Configuration; -using static Azure.Core.HttpHeader; namespace Yvand.EntraClaimsProvider.Tests { @@ -90,7 +90,9 @@ public virtual void InitializeSettings() Settings.Timeout = 99999; #endif - Settings.EntraIDTenants = new List { UnitTestsHelper.TenantConnection }; + string json = File.ReadAllText(UnitTestsHelper.AzureTenantsJsonFile); + List azureTenants = JsonConvert.DeserializeObject>(json); + Settings.EntraIDTenants = azureTenants; foreach (EntraIDTenant tenant in Settings.EntraIDTenants) { tenant.ExcludeMemberUsers = ExcludeMemberUsers; diff --git a/Yvand.EntraCP.Tests/UnitTestsHelper.cs b/Yvand.EntraCP.Tests/UnitTestsHelper.cs index 28375b1..e3e41ac 100644 --- a/Yvand.EntraCP.Tests/UnitTestsHelper.cs +++ b/Yvand.EntraCP.Tests/UnitTestsHelper.cs @@ -2,14 +2,13 @@ using Microsoft.SharePoint; using Microsoft.SharePoint.Administration; using Microsoft.SharePoint.Administration.Claims; -using Newtonsoft.Json; using NUnit.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; +using System.Security.Claims; using Yvand.EntraClaimsProvider.Configuration; namespace Yvand.EntraClaimsProvider.Tests @@ -17,10 +16,11 @@ namespace Yvand.EntraClaimsProvider.Tests [SetUpFixture] public class UnitTestsHelper { - public static readonly EntraCP ClaimsProvider = new EntraCP(TestContext.Parameters["ClaimsProviderName"]); - public static SPTrustedLoginProvider SPTrust => Utils.GetSPTrustAssociatedWithClaimsProvider(TestContext.Parameters["ClaimsProviderName"]); + public static readonly string ClaimsProviderName = TestContext.Parameters["ClaimsProviderName"]; + public static readonly EntraCP ClaimsProvider = new EntraCP(ClaimsProviderName); + public static SPTrustedLoginProvider SPTrust => Utils.GetSPTrustAssociatedWithClaimsProvider(ClaimsProviderName); public static Uri TestSiteCollUri; - public static string TestSiteRelativePath => $"/sites/{TestContext.Parameters["TestSiteCollectionName"]}"; + public static string TestSiteRelativePath => $"/sites/{ClaimsProviderName}.UnitTests"; public const int MaxTime = 50000; public const int TestRepeatCount = 1; public static string FarmAdmin => TestContext.Parameters["FarmAdmin"]; @@ -32,44 +32,30 @@ public class UnitTestsHelper public static string AzureTenantsJsonFile => TestContext.Parameters["AzureTenantsJsonFile"]; public static string DataFile_EntraId_TestUsers => TestContext.Parameters["DataFile_EntraId_TestUsers"]; public static string DataFile_EntraId_TestGroups => TestContext.Parameters["DataFile_EntraId_TestGroups"]; - public static string TestUsersAccountNamePrefix => TestContext.Parameters["UserAccountNamePrefix"]; - public static string TestGroupsAccountNamePrefix => TestContext.Parameters["GroupAccountNamePrefix"]; + public static string GroupsClaimType => TestContext.Parameters["GroupsClaimType"]; static TextWriterTraceListener Logger { get; set; } public static EntraIDProviderConfiguration PersistedConfiguration; private static IEntraIDProviderSettings OriginalSettings; - private static EntraIDTenant _TenantConnection; - public static EntraIDTenant TenantConnection - { - get - { - if (_TenantConnection != null) { return _TenantConnection; } - string json = File.ReadAllText(UnitTestsHelper.AzureTenantsJsonFile); - List azureTenants = JsonConvert.DeserializeObject>(json); - _TenantConnection = azureTenants.First(); - return _TenantConnection; - } - } - [OneTimeSetUp] public static void InitializeSiteCollection() { - Logger = new TextWriterTraceListener(TestContext.Parameters["TestLogFileName"]); + Logger = new TextWriterTraceListener($"{ClaimsProviderName}IntegrationTests.log"); Trace.Listeners.Add(Logger); Trace.AutoFlush = true; - Trace.TraceInformation($"{DateTime.Now:s} Start integration tests of {EntraCP.ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(EntraCP)).Location).FileVersion}."); - Trace.TraceInformation($"{DateTime.Now:s} DataFile_EntraId_TestGroups: {DataFile_EntraId_TestGroups}"); - Trace.TraceInformation($"{DateTime.Now:s} DataFile_EntraId_TestUsers: {DataFile_EntraId_TestUsers}"); - Trace.TraceInformation($"{DateTime.Now:s} TestSiteCollectionName: {TestContext.Parameters["TestSiteCollectionName"]}"); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Start integration tests of {EntraCP.ClaimsProviderName} {FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(EntraCP)).Location).FileVersion}."); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] DataFile_EntraId_TestGroups: {DataFile_EntraId_TestGroups}"); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] DataFile_EntraId_TestUsers: {DataFile_EntraId_TestUsers}"); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] TestSiteCollectionName: {TestContext.Parameters["TestSiteCollectionName"]}"); if (SPTrust == null) { - Trace.TraceError($"{DateTime.Now:s} SPTrust: is null"); + Trace.TraceError($"{DateTime.Now:s} [SETUP] SPTrust: is null"); } else { - Trace.TraceInformation($"{DateTime.Now:s} SPTrust: {SPTrust.Name}"); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] SPTrust: {SPTrust.Name}"); } PersistedConfiguration = EntraCP.GetConfiguration(true); @@ -86,7 +72,7 @@ public static void InitializeSiteCollection() #if DEBUG TestSiteCollUri = new Uri($"http://spsites{TestSiteRelativePath}"); - return; // Uncommented when debugging from unit tests + //return; // Uncommented when debugging from unit tests #endif var service = SPFarm.Local.Services.GetValue(String.Empty); @@ -106,16 +92,17 @@ public static void InitializeSiteCollection() }); if (wa == null) { - Trace.TraceError($"{DateTime.Now:s} Web application was NOT found."); + Trace.TraceError($"{DateTime.Now:s} [SETUP] Web application was NOT found."); return; } - Trace.TraceInformation($"{DateTime.Now:s} Web application {wa.Name} found."); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Web application {wa.Name} found."); Uri waRootAuthority = wa.AlternateUrls[0].Uri; TestSiteCollUri = new Uri($"{waRootAuthority.GetLeftPart(UriPartial.Authority)}{TestSiteRelativePath}"); SPClaimProviderManager claimMgr = SPClaimProviderManager.Local; - //string encodedClaim = claimMgr.EncodeClaim(TrustedGroup); - //SPUserInfo userInfo = new SPUserInfo { LoginName = encodedClaim, Name = TrustedGroupToAdd_ClaimValue }; + string trustedGroupName = TestEntitySourceManager.GetOneGroup().Id; + string encodedGroupClaim = claimMgr.EncodeClaim(new SPClaim(GroupsClaimType, trustedGroupName, ClaimValueTypes.String, SPOriginalIssuers.Format(SPOriginalIssuerType.TrustedProvider, UnitTestsHelper.SPTrust.Name))); + SPUserInfo groupInfo = new SPUserInfo { LoginName = encodedGroupClaim, Name = trustedGroupName }; FileVersionInfo spAssemblyVersion = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(SPSite)).Location); string spSiteTemplate = "STS#3"; // modern team site template @@ -128,23 +115,23 @@ public static void InitializeSiteCollection() // The root site may not exist, but it must be present for tests to run if (!SPSite.Exists(waRootAuthority)) { - Trace.TraceInformation($"{DateTime.Now:s} Creating root site collection {waRootAuthority.AbsoluteUri}..."); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Creating root site collection {waRootAuthority.AbsoluteUri}..."); SPSite spSite = wa.Sites.Add(waRootAuthority.AbsoluteUri, "root", "root", 1033, spSiteTemplate, FarmAdmin, String.Empty, String.Empty); spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; - //membersGroup.AddUser(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes); + membersGroup.AddUser(groupInfo.LoginName, groupInfo.Email, groupInfo.Name, groupInfo.Notes); spSite.Dispose(); } if (!SPSite.Exists(TestSiteCollUri)) { - Trace.TraceInformation($"{DateTime.Now:s} Creating site collection {TestSiteCollUri.AbsoluteUri} with template '{spSiteTemplate}'..."); + Trace.TraceInformation($"{DateTime.Now:s} [SETUP] Creating site collection {TestSiteCollUri.AbsoluteUri} with template '{spSiteTemplate}'..."); SPSite spSite = wa.Sites.Add(TestSiteCollUri.AbsoluteUri, EntraCP.ClaimsProviderName, EntraCP.ClaimsProviderName, 1033, spSiteTemplate, FarmAdmin, String.Empty, String.Empty); spSite.RootWeb.CreateDefaultAssociatedGroups(FarmAdmin, FarmAdmin, spSite.RootWeb.Title); SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; - //membersGroup.AddUser(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes); + membersGroup.AddUser(groupInfo.LoginName, groupInfo.Email, groupInfo.Name, groupInfo.Notes); spSite.Dispose(); } else @@ -152,7 +139,7 @@ public static void InitializeSiteCollection() using (SPSite spSite = new SPSite(TestSiteCollUri.AbsoluteUri)) { SPGroup membersGroup = spSite.RootWeb.AssociatedMemberGroup; - //membersGroup.AddUser(userInfo.LoginName, userInfo.Email, userInfo.Name, userInfo.Notes); + membersGroup.AddUser(groupInfo.LoginName, groupInfo.Email, groupInfo.Name, groupInfo.Notes); } } } diff --git a/Yvand.EntraCP.Tests/local.runsettings b/Yvand.EntraCP.Tests/local.runsettings index 57a51ee..abc2f4f 100644 --- a/Yvand.EntraCP.Tests/local.runsettings +++ b/Yvand.EntraCP.Tests/local.runsettings @@ -6,15 +6,11 @@ - + - - - - - + From d625c5306f862bc4e6f45d937218dd7943bf37ae Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 6 Aug 2024 15:31:09 +0200 Subject: [PATCH 14/23] Update run-tests.yml --- .github/workflows/run-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e448e93..970933f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -37,7 +37,7 @@ jobs: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - azdevops-pat-registerAgent: ${{ secrets.dtl_azdevopsPassword }} - dtl-env-accountsPassword: ${{ secrets.dtl_accountsPassword }} - unittestfiles-storageAccountEndpoint: ${{ secrets.unittestfiles_storageAccountEndpoint }} - unittestfiles-storageAccountShareName: ${{ secrets.unittestfiles_storageAccountShareName }} + ADO_PAT_REGISTERAGENT: ${{ secrets.ADO_PAT_REGISTERAGENT }} + DTL_ACCOUNTSPASSWORD: ${{ secrets.DTL_ACCOUNTSPASSWORD }} + UNITTESTFILES_STORAGEACCOUNTENDPOINT: ${{ secrets.UNITTESTFILES_STORAGEACCOUNTENDPOINT }} + UNITTESTFILES_STORAGEACCOUNTSHARENAME: ${{ secrets.UNITTESTFILES_STORAGEACCOUNTSHARENAME }} From efe05f998d0688d7b44aabde4978cd2c168eb3c9 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Wed, 7 Aug 2024 10:03:53 +0200 Subject: [PATCH 15/23] Update run-tests.yml --- .github/workflows/run-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 970933f..4f38709 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -32,12 +32,12 @@ jobs: dtl-env-addAzureBastion: ${{ vars.dtl_addAzureBastion == 'true' }} test-prepareSharePointVmScriptUris: ${{ vars.DTL_PrepareSharePointVmScriptUris }} test-claimsProviderPackageUri: ${{ vars.DTL_ClaimsProviderPackageUri }} - unittestfiles-storageAccountSourceRelativePath: ${{ vars.unittestfiles_storageAccountSourceRelativePath }} + unittestfiles_azure_storage_share_relative_path: ${{ vars.unittestfiles_azure_storage_share_relative_path }} secrets: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} ADO_PAT_REGISTERAGENT: ${{ secrets.ADO_PAT_REGISTERAGENT }} DTL_ACCOUNTSPASSWORD: ${{ secrets.DTL_ACCOUNTSPASSWORD }} - UNITTESTFILES_STORAGEACCOUNTENDPOINT: ${{ secrets.UNITTESTFILES_STORAGEACCOUNTENDPOINT }} - UNITTESTFILES_STORAGEACCOUNTSHARENAME: ${{ secrets.UNITTESTFILES_STORAGEACCOUNTSHARENAME }} + UNITTESTFILES_AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.UNITTESTFILES_AZURE_STORAGE_CONNECTION_STRING }} + UNITTESTFILES_AZURE_STORAGE_SHARE_NAME: ${{ secrets.UNITTESTFILES_AZURE_STORAGE_SHARE_NAME }} From f08c314ed26b65b1fcdd8b83ba4393d7c010c1b6 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Thu, 8 Aug 2024 17:43:10 +0200 Subject: [PATCH 16/23] Update Yvand.EntraCP.sln --- Yvand.EntraCP.sln | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Yvand.EntraCP.sln b/Yvand.EntraCP.sln index 53437db..1723932 100644 --- a/Yvand.EntraCP.sln +++ b/Yvand.EntraCP.sln @@ -12,27 +12,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yvand.EntraCP.Tests", "Yvan EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|x64.ActiveCfg = Debug|Any CPU {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|x64.Build.0 = Debug|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|Any CPU.Build.0 = Release|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|Any CPU.Deploy.0 = Release|Any CPU {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|x64.ActiveCfg = Release|Any CPU {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|x64.Build.0 = Release|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|x64.ActiveCfg = Debug|Any CPU {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|x64.Build.0 = Debug|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|Any CPU.Build.0 = Release|Any CPU {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|x64.ActiveCfg = Release|Any CPU {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection From 3c220da5d7da8bcdc0a7438c0dd8a0520032b0e0 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Mon, 12 Aug 2024 09:22:20 +0200 Subject: [PATCH 17/23] update workflows --- .../workflows/{verify-prs-and-commits.yml => build.yml} | 8 +++++--- .github/workflows/{stale.yaml => stale.yml} | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) rename .github/workflows/{verify-prs-and-commits.yml => build.yml} (76%) rename .github/workflows/{stale.yaml => stale.yml} (93%) diff --git a/.github/workflows/verify-prs-and-commits.yml b/.github/workflows/build.yml similarity index 76% rename from .github/workflows/verify-prs-and-commits.yml rename to .github/workflows/build.yml index 6a223cd..eeebe26 100644 --- a/.github/workflows/verify-prs-and-commits.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,13 @@ -name: Verify PRs and commits +name: Build solution on: workflow_dispatch: push: - branches: [ "master", "dev" ] + branches: + - dev + - 'releases/**' pull_request: - branches: [ "master", "dev" ] + branches: [ "dev", "master", "releases/**" ] jobs: call-build: diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yml similarity index 93% rename from .github/workflows/stale.yaml rename to .github/workflows/stale.yml index 5595f62..548966e 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yml @@ -2,13 +2,13 @@ name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' - +permissions: + issues: write + pull-requests: write + jobs: stale: runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write steps: - uses: actions/stale@v9 with: From c5873e749873e63d6e68f6eb4707cbef3074a3a0 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Mon, 12 Aug 2024 09:25:03 +0200 Subject: [PATCH 18/23] update workflows --- .github/workflows/{run-tests.yml => prepare-dtl-env.yml} | 0 .github/workflows/run-tests-on-runner.yml | 7 +++++++ 2 files changed, 7 insertions(+) rename .github/workflows/{run-tests.yml => prepare-dtl-env.yml} (100%) create mode 100644 .github/workflows/run-tests-on-runner.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/prepare-dtl-env.yml similarity index 100% rename from .github/workflows/run-tests.yml rename to .github/workflows/prepare-dtl-env.yml diff --git a/.github/workflows/run-tests-on-runner.yml b/.github/workflows/run-tests-on-runner.yml new file mode 100644 index 0000000..0e7b5de --- /dev/null +++ b/.github/workflows/run-tests-on-runner.yml @@ -0,0 +1,7 @@ +name: Run Visual Studio tests +on: workflow_dispatch +jobs: + call-workflow-run-tests: + uses: Yvand/EntraCP/.github/workflows/reusaable-run-tests-on-runner.yml@master + with: + test_project_folder_name: 'Yvand.EntraCP-unit-tests' From e10888330f259f28bd7ad8d24566f6cf654da73e Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Wed, 14 Aug 2024 13:38:14 +0200 Subject: [PATCH 19/23] Update Yvand.EntraCP.Tests.csproj --- Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj b/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj index a54c080..caa0e36 100644 --- a/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj +++ b/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj @@ -76,12 +76,12 @@ 4.1.0 - 4.2.0 + 4.3.0 runtime; build; native; contentfiles; analyzers; buildtransitive all - 4.5.0 + 4.6.0 runtime; build; native; contentfiles; analyzers; buildtransitive all From 659a033547666e3681ba9fce3cf6e2983fcddf03 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 20 Aug 2024 09:44:25 +0200 Subject: [PATCH 20/23] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4faa1ff..fdaeb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## EntraCP v27.0 - Unreleased -* Ensure that all group members are retrieved when only users members of specified groups can be found in SharePoint +* Ensure that restrict searchable users feature works for all members, instead of only 100 members maximum - https://github.com/Yvand/EntraCP/issues/264 * Update the script that provisions tenant with test users and groups, to be more reliable and provision 999 users (instead of 50), so tests are more realistics * Improve tests * Publish a sample project that developers can use to create a custom version of EntraCP, for specific needs From d4b71d6d474f2a8cd0d70d74a67401355ddce1b2 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 20 Aug 2024 09:57:07 +0200 Subject: [PATCH 21/23] Update reusable-build.yml --- .github/workflows/reusable-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index a6f6d18..671a843 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -136,7 +136,7 @@ jobs: # Build the application - name: Build the application and create the solution package - run: msbuild "${{ env.project-name }}.sln" /p:Configuration=$env:Configuration /p:IsPackaging=true /p:platform="Any CPU" /p:ContinuousIntegrationBuild=true + run: msbuild "${{ env.project-name }}.sln" /p:Configuration=$env:Configuration /p:IsPackaging=true /p:platform="x64" /p:ContinuousIntegrationBuild=true env: Configuration: ${{ matrix.configuration }} From b44218be64d6cba73f93337d548dc45decd3cd82 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 20 Aug 2024 10:00:26 +0200 Subject: [PATCH 22/23] Revert "Update reusable-build.yml" This reverts commit d4b71d6d474f2a8cd0d70d74a67401355ddce1b2. --- .github/workflows/reusable-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 671a843..a6f6d18 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -136,7 +136,7 @@ jobs: # Build the application - name: Build the application and create the solution package - run: msbuild "${{ env.project-name }}.sln" /p:Configuration=$env:Configuration /p:IsPackaging=true /p:platform="x64" /p:ContinuousIntegrationBuild=true + run: msbuild "${{ env.project-name }}.sln" /p:Configuration=$env:Configuration /p:IsPackaging=true /p:platform="Any CPU" /p:ContinuousIntegrationBuild=true env: Configuration: ${{ matrix.configuration }} From f6e21f89272ff0b41de1dd89c04377aac60469c3 Mon Sep 17 00:00:00 2001 From: Yvan Duhamel Date: Tue, 20 Aug 2024 10:13:00 +0200 Subject: [PATCH 23/23] update build settings --- .../Yvand.EntraCP.Tests.csproj | 19 ++++++------ Yvand.EntraCP.sln | 16 +++++----- Yvand.EntraCP/Yvand.EntraCP.csproj | 29 ++++++++++--------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj b/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj index caa0e36..0700f2f 100644 --- a/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj +++ b/Yvand.EntraCP.Tests/Yvand.EntraCP.Tests.csproj @@ -2,7 +2,7 @@ Debug - AnyCPU + x64 {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88} Library Properties @@ -20,22 +20,23 @@ - + true - full - false bin\Debug\ DEBUG;TRACE + full + x64 + 7.3 prompt - 4 - - pdbonly - true + bin\Release\ TRACE + true + pdbonly + x64 + 7.3 prompt - 4 diff --git a/Yvand.EntraCP.sln b/Yvand.EntraCP.sln index 1723932..5d508e6 100644 --- a/Yvand.EntraCP.sln +++ b/Yvand.EntraCP.sln @@ -16,14 +16,14 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|x64.ActiveCfg = Debug|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|x64.Build.0 = Debug|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|x64.ActiveCfg = Release|Any CPU - {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|x64.Build.0 = Release|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|x64.ActiveCfg = Debug|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|x64.Build.0 = Debug|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|x64.ActiveCfg = Release|Any CPU - {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|x64.Build.0 = Release|Any CPU + {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|x64.ActiveCfg = Debug|x64 + {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Debug|x64.Build.0 = Debug|x64 + {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|x64.ActiveCfg = Release|x64 + {EEC47949-34B5-4805-A04D-A372BE75D3CB}.Release|x64.Build.0 = Release|x64 + {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|x64.ActiveCfg = Debug|x64 + {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Debug|x64.Build.0 = Debug|x64 + {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|x64.ActiveCfg = Release|x64 + {DB8C79E5-F7F7-4841-8A8C-A9832A9CAE88}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Yvand.EntraCP/Yvand.EntraCP.csproj b/Yvand.EntraCP/Yvand.EntraCP.csproj index a498e30..a06e891 100644 --- a/Yvand.EntraCP/Yvand.EntraCP.csproj +++ b/Yvand.EntraCP/Yvand.EntraCP.csproj @@ -31,31 +31,32 @@ - - embedded + + + true + + true - false bin\Debug\ DEBUG;TRACE - prompt - 4 + embedded + x64 false + 8.0 + prompt MinimumRecommendedRules.ruleset - - - portable - true + bin\Release\ TRACE - prompt - 4 + true + portable + x64 false + 8.0 + prompt MinimumRecommendedRules.ruleset - - true -