From 56543722ad9d2e003af30b8b57bee34474437efb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:37:23 -0500 Subject: [PATCH 1/4] [deps] DevOps: Update codecov/codecov-action action to v4 (#3840) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78890f1d1b..3ffb37d5ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,6 +53,6 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4 + uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 40a2a567e64740d165fd73ae8493230dc2b1d094 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 26 Feb 2024 11:50:24 -0500 Subject: [PATCH 2/4] Sent initiation path for organization and user signups (#3723) --- .../OrganizationCreateRequestModel.cs | 3 +++ .../Implementations/OrganizationService.cs | 1 + .../Models/Business/OrganizationSignup.cs | 1 + .../Services/Implementations/UserService.cs | 23 ++++++++++++++++++- .../Tools/Models/Business/ReferenceEvent.cs | 9 ++++++++ 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 304978eb13..5cf31460fd 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -48,6 +48,8 @@ public class OrganizationCreateRequestModel : IValidatableObject public bool UseSecretsManager { get; set; } public bool IsFromSecretsManagerTrial { get; set; } + public string InitiationPath { get; set; } + public virtual OrganizationSignup ToOrganizationSignup(User user) { var orgSignup = new OrganizationSignup @@ -79,6 +81,7 @@ public class OrganizationCreateRequestModel : IValidatableObject BillingAddressPostalCode = BillingAddressPostalCode, BillingAddressCountry = BillingAddressCountry, }, + InitiationPath = InitiationPath, }; Keys?.ToOrganizationSignup(orgSignup); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 264962dca7..cb68c21557 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -533,6 +533,7 @@ public class OrganizationService : IOrganizationService PlanName = plan.Name, PlanType = plan.Type, Seats = returnValue.Item1.Seats, + SignupInitiationPath = signup.InitiationPath, Storage = returnValue.Item1.MaxStorageGb, // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 }); diff --git a/src/Core/Models/Business/OrganizationSignup.cs b/src/Core/Models/Business/OrganizationSignup.cs index 970ede9afc..89168b2747 100644 --- a/src/Core/Models/Business/OrganizationSignup.cs +++ b/src/Core/Models/Business/OrganizationSignup.cs @@ -13,4 +13,5 @@ public class OrganizationSignup : OrganizationUpgrade public PaymentMethodType? PaymentMethodType { get; set; } public string PaymentToken { get; set; } public int? MaxAutoscaleSeats { get; set; } = null; + public string InitiationPath { get; set; } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 977e8afd39..a40d4bf302 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,5 +1,4 @@ using System.Security.Claims; -using System.Text.Json; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -28,7 +27,9 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using File = System.IO.File; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Bit.Core.Services; @@ -338,6 +339,26 @@ public class UserService : UserManager, IUserService, IDisposable if (result == IdentityResult.Success) { await _mailService.SendWelcomeEmailAsync(user); + + if (!string.IsNullOrEmpty(user.ReferenceData)) + { + var referenceData = JsonConvert.DeserializeObject>(user.ReferenceData); + if (referenceData.TryGetValue("initiationPath", out var value)) + { + var initiationPath = value.ToString(); + if (!string.IsNullOrEmpty(initiationPath)) + { + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext) + { + SignupInitiationPath = initiationPath + }); + + return result; + } + } + } + await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } diff --git a/src/Core/Tools/Models/Business/ReferenceEvent.cs b/src/Core/Tools/Models/Business/ReferenceEvent.cs index ac21c92e44..03a0b3e1da 100644 --- a/src/Core/Tools/Models/Business/ReferenceEvent.cs +++ b/src/Core/Tools/Models/Business/ReferenceEvent.cs @@ -234,4 +234,13 @@ public class ReferenceEvent /// when the event was not originated by an application. /// public Version? ClientVersion { get; set; } + + /// + /// The initiation path of a user who signed up for a paid version of Bitwarden. For example, "Trial from marketing website". + /// + /// + /// This value should only be populated when the is . Otherwise, + /// the value should be . + /// + public string SignupInitiationPath { get; set; } } From b3a6bf5af3e8b4b3891d22390d94d0ab89d8d1bc Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:25:44 -0800 Subject: [PATCH 3/4] Bumped version to 2024.2.3 (#3842) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 7fd4d54306..fa624fa505 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2024.2.2 + 2024.2.3 Bit.$(MSBuildProjectName) enable From e0ae294953d4472a463cc5fb515e14ca1ceae87d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:40:29 +1000 Subject: [PATCH 4/4] [AC-2099] Flexible Collections migration integration tests (#3828) Add integration tests for Organization_EnableCollectionEnhancements sproc --- .../MssqlDatabaseDataAttribute.cs | 54 ++ ...izationEnableCollectionEnhancementTests.cs | 611 ++++++++++++++++++ .../OrganizationUserRepositoryTests.cs | 2 +- .../DatabaseDataAttribute.cs | 2 +- 4 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs b/test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs new file mode 100644 index 0000000000..01180ca312 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/MssqlDatabaseDataAttribute.cs @@ -0,0 +1,54 @@ +using Bit.Core.Enums; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole; + +/// +/// Used to test the mssql database only. +/// This is generally NOT what you want and is only used for Flexible Collections which has an opt-in method specific +/// to cloud (and therefore mssql) only. This should be deleted during cleanup so that others don't use it. +/// +internal class MssqlDatabaseDataAttribute : DatabaseDataAttribute +{ + protected override IEnumerable GetDatabaseProviders(IConfiguration config) + { + var configureLogging = (ILoggingBuilder builder) => + { + if (!config.GetValue("Quiet")) + { + builder.AddConfiguration(config); + builder.AddConsole(); + builder.AddDebug(); + } + }; + + var databases = config.GetDatabases(); + + foreach (var database in databases) + { + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + var dapperSqlServerCollection = new ServiceCollection(); + dapperSqlServerCollection.AddLogging(configureLogging); + dapperSqlServerCollection.AddDapperRepositories(SelfHosted); + var globalSettings = new GlobalSettings + { + DatabaseProvider = "sqlServer", + SqlServer = new GlobalSettings.SqlSettings + { + ConnectionString = database.ConnectionString, + }, + }; + dapperSqlServerCollection.AddSingleton(globalSettings); + dapperSqlServerCollection.AddSingleton(globalSettings); + dapperSqlServerCollection.AddSingleton(database); + dapperSqlServerCollection.AddDataProtection(); + yield return dapperSqlServerCollection.BuildServiceProvider(); + } + } + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs new file mode 100644 index 0000000000..27f7419255 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationEnableCollectionEnhancementTests.cs @@ -0,0 +1,611 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; + +public class OrganizationEnableCollectionEnhancementTests +{ + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_User_WithAccessAll_GivesCanEditAccessToAllCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.User, accessAll: true, organizationUserRepository); + var collection1 = await CreateCollection(organization, collectionRepository); + var collection2 = await CreateCollection(organization, collectionRepository); + var collection3 = await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.False(updatedOrgUser.AccessAll); + + Assert.Equal(3, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_Group_WithAccessAll_GivesCanEditAccessToAllCollections( + IGroupRepository groupRepository, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository) + { + var organization = await CreateOrganization(organizationRepository); + var group = await CreateGroup(organization, accessAll: true, groupRepository); + var collection1 = await CreateCollection(organization, collectionRepository); + var collection2 = await CreateCollection(organization, collectionRepository); + var collection3 = await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedGroup, collectionAccessSelections) = await groupRepository.GetByIdWithCollectionsAsync(group.Id); + + Assert.False(updatedGroup.AccessAll); + + Assert.Equal(3, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_Manager_WithAccessAll_GivesCanManageAccessToAllCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: true, organizationUserRepository); + var collection1 = await CreateCollection(organization, collectionRepository); + var collection2 = await CreateCollection(organization, collectionRepository); + var collection3 = await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.False(updatedOrgUser.AccessAll); + Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); + + Assert.Equal(3, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection3.Id && + CanManage(cas)); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_Manager_WithoutAccessAll_GivesCanManageAccessToAssignedCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: false, organizationUserRepository); + var collection1 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false }]); + var collection2 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }]); + var collection3 = await CreateCollection(organization, collectionRepository); // no access + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); + + Assert.Equal(2, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.DoesNotContain(collectionAccessSelections, cas => + cas.Id == collection3.Id); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_Manager_WithoutAccessAll_GivesCanManageAccess_ToGroupAssignedCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository, + IGroupRepository groupRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: false, organizationUserRepository); + var group = await CreateGroup(organization, accessAll: false, groupRepository, orgUser); + + var collection1 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); + var collection2 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); + var collection3 = await CreateCollection(organization, collectionRepository); // no access + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, updatedUserAccess) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + // Assert: orgUser should be downgraded from Manager to User + // and given Can Manage permissions over all group assigned collections + Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); + Assert.Equal(2, updatedUserAccess.Count); + Assert.Contains(updatedUserAccess, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(updatedUserAccess, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.DoesNotContain(updatedUserAccess, cas => + cas.Id == collection3.Id); + + // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) + var (updatedGroup, updatedGroupAccess) = await groupRepository.GetByIdWithCollectionsAsync(group.Id); + Assert.Equal(2, updatedGroupAccess.Count); + Assert.Contains(updatedGroupAccess, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.DoesNotContain(updatedGroupAccess, cas => + cas.Id == collection3.Id); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_Manager_WithoutAccessAll_InGroupWithAccessAll_GivesCanManageAccessToAllCollections( + IUserRepository userRepository, + IGroupRepository groupRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: false, organizationUserRepository); + + // Use 2 groups to test for overlapping access + var group1 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); + var group2 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); + + var collection1 = await CreateCollection(organization, collectionRepository); + var collection2 = await CreateCollection(organization, collectionRepository); + var collection3 = await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); + + // OrgUser has direct Can Manage access to all collections + Assert.Equal(3, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection3.Id && + CanManage(cas)); + + // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) + var (updatedGroup1, updatedGroupAccess1) = await groupRepository.GetByIdWithCollectionsAsync(group1.Id); + Assert.Equal(3, updatedGroupAccess1.Count); + Assert.Contains(updatedGroupAccess1, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess1, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess1, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + + var (updatedGroup2, updatedGroupAccess2) = await groupRepository.GetByIdWithCollectionsAsync(group2.Id); + Assert.Equal(3, updatedGroupAccess2.Count); + Assert.Contains(updatedGroupAccess2, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess2, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess2, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_CustomUser_WithEditAssignedCollections_WithAccessAll_GivesCanManageAccessToAllCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: true, + organizationUserRepository, new Permissions { EditAssignedCollections = true }); + var collection1 = await CreateCollection(organization, collectionRepository); + var collection2 = await CreateCollection(organization, collectionRepository); + var collection3 = await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.False(updatedOrgUser.AccessAll); + // Note: custom users do not have their types changed yet, this was done in code with a migration to follow + Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); + + Assert.Equal(3, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection3.Id && + CanManage(cas)); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_CustomUser_WithEditAssignedCollections_WithoutAccessAll_GivesCanManageAccessToAssignedCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: false, + organizationUserRepository, new Permissions { EditAssignedCollections = true }); + var collection1 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false }]); + var collection2 = await CreateCollection(organization, collectionRepository, null, [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }]); + var collection3 = await CreateCollection(organization, collectionRepository); // no access + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); + + Assert.Equal(2, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.DoesNotContain(collectionAccessSelections, cas => + cas.Id == collection3.Id); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_CustomUser_WithEditAssignedCollections_WithoutAccessAll_GivesCanManageAccess_ToGroupAssignedCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository, + IGroupRepository groupRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: false, + organizationUserRepository, new Permissions { EditAssignedCollections = true }); + var group = await CreateGroup(organization, accessAll: false, groupRepository, orgUser); + + var collection1 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); + var collection2 = await CreateCollection(organization, collectionRepository, new[] { new CollectionAccessSelection { Id = group.Id, HidePasswords = false, Manage = false, ReadOnly = false } }); + var collection3 = await CreateCollection(organization, collectionRepository); // no access + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, updatedUserAccess) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + // Assert: user should be given Can Manage permissions over all group assigned collections + Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); + Assert.Equal(2, updatedUserAccess.Count); + Assert.Contains(updatedUserAccess, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(updatedUserAccess, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.DoesNotContain(updatedUserAccess, cas => + cas.Id == collection3.Id); + + // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) + var (updatedGroup, updatedGroupAccess) = await groupRepository.GetByIdWithCollectionsAsync(group.Id); + Assert.Equal(2, updatedGroupAccess.Count); + Assert.Contains(updatedGroupAccess, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.DoesNotContain(updatedGroupAccess, cas => + cas.Id == collection3.Id); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_CustomUser_WithEditAssignedCollections_WithoutAccessAll_InGroupWithAccessAll_GivesCanManageAccessToAllCollections( + IUserRepository userRepository, + IGroupRepository groupRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Custom, accessAll: false, + organizationUserRepository, new Permissions { EditAssignedCollections = true }); + + // Use 2 groups to test for overlapping access + var group1 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); + var group2 = await CreateGroup(organization, accessAll: true, groupRepository, orgUser); + + var collection1 = await CreateCollection(organization, collectionRepository); + var collection2 = await CreateCollection(organization, collectionRepository); + var collection3 = await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type); + + // OrgUser has direct Can Manage access to all collections + Assert.Equal(3, collectionAccessSelections.Count); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection1.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection2.Id && + CanManage(cas)); + Assert.Contains(collectionAccessSelections, cas => + cas.Id == collection3.Id && + CanManage(cas)); + + // Assert: group should only have Can Edit permissions (making sure no side-effects from the Manager migration) + var (updatedGroup1, updatedGroupAccess1) = await groupRepository.GetByIdWithCollectionsAsync(group1.Id); + Assert.Equal(3, updatedGroupAccess1.Count); + Assert.Contains(updatedGroupAccess1, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess1, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess1, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + + var (updatedGroup2, updatedGroupAccess2) = await groupRepository.GetByIdWithCollectionsAsync(group2.Id); + Assert.Equal(3, updatedGroupAccess2.Count); + Assert.Contains(updatedGroupAccess2, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess2, cas => + cas.Id == collection2.Id && + CanEdit(cas)); + Assert.Contains(updatedGroupAccess2, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_NonManagers_WithoutAccessAll_NoChangeToRoleOrCollectionAccess( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var userUser = await CreateUser(userRepository); + var adminUser = await CreateUser(userRepository); + var ownerUser = await CreateUser(userRepository); + var customUser = await CreateUser(userRepository); + + var organization = await CreateOrganization(organizationRepository); + + // All roles that are unaffected by this change without AccessAll + var orgUser = await CreateOrganizationUser(userUser, organization, OrganizationUserType.User, accessAll: false, organizationUserRepository); + var admin = await CreateOrganizationUser(adminUser, organization, OrganizationUserType.Admin, accessAll: false, organizationUserRepository); + var owner = await CreateOrganizationUser(ownerUser, organization, OrganizationUserType.Owner, accessAll: false, organizationUserRepository); + var custom = await CreateOrganizationUser(customUser, organization, OrganizationUserType.Custom, accessAll: false, organizationUserRepository, new Permissions { DeleteAssignedCollections = true, AccessReports = true }); + + var collection1 = await CreateCollection(organization, collectionRepository, null, new[] + { + new CollectionAccessSelection {Id = orgUser.Id}, + new CollectionAccessSelection {Id = custom.Id, HidePasswords = true} + }); + var collection2 = await CreateCollection(organization, collectionRepository, null, new[] + { + new CollectionAccessSelection { Id = owner.Id, HidePasswords = true} , + new CollectionAccessSelection { Id = admin.Id, ReadOnly = true} + }); + var collection3 = await CreateCollection(organization, collectionRepository, null, new[] + { + new CollectionAccessSelection { Id = owner.Id } + }); + + await organizationRepository.EnableCollectionEnhancements(organization.Id); + + var (updatedOrgUser, orgUserAccess) = await organizationUserRepository + .GetDetailsByIdWithCollectionsAsync(orgUser.Id); + Assert.Equal(OrganizationUserType.User, updatedOrgUser.Type); + Assert.Equal(1, orgUserAccess.Count); + Assert.Contains(orgUserAccess, cas => + cas.Id == collection1.Id && + CanEdit(cas)); + + var (updatedAdmin, adminAccess) = await organizationUserRepository + .GetDetailsByIdWithCollectionsAsync(admin.Id); + Assert.Equal(OrganizationUserType.Admin, updatedAdmin.Type); + Assert.Equal(1, adminAccess.Count); + Assert.Contains(adminAccess, cas => + cas.Id == collection2.Id && + cas is { HidePasswords: false, ReadOnly: true, Manage: false }); + + var (updatedOwner, ownerAccess) = await organizationUserRepository + .GetDetailsByIdWithCollectionsAsync(owner.Id); + Assert.Equal(OrganizationUserType.Owner, updatedOwner.Type); + Assert.Equal(2, ownerAccess.Count); + Assert.Contains(ownerAccess, cas => + cas.Id == collection2.Id && + cas is { HidePasswords: true, ReadOnly: false, Manage: false }); + Assert.Contains(ownerAccess, cas => + cas.Id == collection3.Id && + CanEdit(cas)); + + var (updatedCustom, customAccess) = await organizationUserRepository + .GetDetailsByIdWithCollectionsAsync(custom.Id); + Assert.Equal(OrganizationUserType.Custom, updatedCustom.Type); + Assert.Equal(1, customAccess.Count); + Assert.Contains(customAccess, cas => + cas.Id == collection1.Id && + cas is { HidePasswords: true, ReadOnly: false, Manage: false }); + } + + [DatabaseTheory, MssqlDatabaseData] + public async Task Migrate_DoesNotAffect_OtherOrganizations( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Target organization to be migrated + var targetUser = await CreateUser(userRepository); + var targetOrganization = await CreateOrganization(organizationRepository); + await CreateOrganizationUser(targetUser, targetOrganization, OrganizationUserType.Manager, accessAll: true, organizationUserRepository); + await CreateCollection(targetOrganization, collectionRepository); + await CreateCollection(targetOrganization, collectionRepository); + await CreateCollection(targetOrganization, collectionRepository); + + // Unrelated organization + var user = await CreateUser(userRepository); + var organization = await CreateOrganization(organizationRepository); + var orgUser = await CreateOrganizationUser(user, organization, OrganizationUserType.Manager, accessAll: true, organizationUserRepository); + await CreateCollection(organization, collectionRepository); + await CreateCollection(organization, collectionRepository); + await CreateCollection(organization, collectionRepository); + + await organizationRepository.EnableCollectionEnhancements(targetOrganization.Id); + + var (updatedOrgUser, collectionAccessSelections) = await organizationUserRepository + .GetDetailsByIdWithCollectionsAsync(orgUser.Id); + + // OrgUser should not have changed + Assert.Equal(OrganizationUserType.Manager, updatedOrgUser.Type); + Assert.True(updatedOrgUser.AccessAll); + Assert.Equal(0, collectionAccessSelections.Count); + + var updatedOrganization = await organizationRepository.GetByIdAsync(organization.Id); + Assert.False(updatedOrganization.FlexibleCollections); + } + + private async Task CreateUser(IUserRepository userRepository) + { + return await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + } + + private async Task CreateGroup(Organization organization, bool accessAll, IGroupRepository groupRepository, + OrganizationUser? orgUser = null) + { + var group = await groupRepository.CreateAsync(new Group + { + Name = $"Test Group {Guid.NewGuid()}", + OrganizationId = organization.Id, + AccessAll = accessAll + }); + + if (orgUser != null) + { + await groupRepository.UpdateUsersAsync(group.Id, [orgUser.Id]); + } + + return group; + } + + private async Task CreateOrganization(IOrganizationRepository organizationRepository) + { + return await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {Guid.NewGuid()}", + BillingEmail = "Billing Email", // TODO: EF does not enforce this being NOT NULL + Plan = "Test Plan", // TODO: EF does not enforce this being NOT NULl + }); + } + + private async Task CreateOrganizationUser(User user, Organization organization, + OrganizationUserType type, bool accessAll, IOrganizationUserRepository organizationUserRepository, + Permissions? permissions = null) + { + return await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = type, + AccessAll = accessAll, + Permissions = permissions == null ? null : CoreHelpers.ClassToJsonData(permissions) + }); + } + + private async Task CreateCollection(Organization organization, ICollectionRepository collectionRepository, + IEnumerable? groups = null, IEnumerable? users = null) + { + var collection = new Collection { Name = $"Test collection {Guid.NewGuid()}", OrganizationId = organization.Id }; + await collectionRepository.CreateAsync(collection, groups: groups, users: users); + return collection; + } + + private bool CanEdit(CollectionAccessSelection collectionAccess) + { + return collectionAccess is { HidePasswords: false, ReadOnly: false, Manage: false }; + } + + private bool CanManage(CollectionAccessSelection collectionAccess) + { + return collectionAccess is { HidePasswords: false, ReadOnly: false, Manage: true }; + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 631d5a1b81..539ac0856f 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -16,7 +16,7 @@ public class OrganizationUserRepositoryTests var user = await userRepository.CreateAsync(new User { Name = "Test User", - Email = $"test+{Guid.NewGuid()}@email.com", + Email = $"test+{Guid.NewGuid()}@example.com", ApiKey = "TEST", SecurityStamp = "stamp", }); diff --git a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs index 6433fcb207..2c12890ca1 100644 --- a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs +++ b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs @@ -33,7 +33,7 @@ public class DatabaseDataAttribute : DataAttribute } } - private IEnumerable GetDatabaseProviders(IConfiguration config) + protected virtual IEnumerable GetDatabaseProviders(IConfiguration config) { var configureLogging = (ILoggingBuilder builder) => {