1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-30 23:11:22 +01:00

Merge branch 'main' into ac/ac-1682/ef-migrations

This commit is contained in:
Rui Tome 2024-02-27 10:45:22 +00:00
commit 988d90f1b4
No known key found for this signature in database
GPG Key ID: 526239D96A8EC066
11 changed files with 705 additions and 5 deletions

View File

@ -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 }}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.2.2</Version>
<Version>2024.2.3</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -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);

View File

@ -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
});

View File

@ -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; }
}

View File

@ -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<User>, IUserService, IDisposable
if (result == IdentityResult.Success)
{
await _mailService.SendWelcomeEmailAsync(user);
if (!string.IsNullOrEmpty(user.ReferenceData))
{
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(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));
}

View File

@ -234,4 +234,13 @@ public class ReferenceEvent
/// <see langword="null"/> when the event was not originated by an application.
/// </value>
public Version? ClientVersion { get; set; }
/// <summary>
/// The initiation path of a user who signed up for a paid version of Bitwarden. For example, "Trial from marketing website".
/// </summary>
/// <value>
/// This value should only be populated when the <see cref="ReferenceEventType"/> is <see cref="ReferenceEventType.Signup"/>. Otherwise,
/// the value should be <see langword="null" />.
/// </value>
public string SignupInitiationPath { get; set; }
}

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
internal class MssqlDatabaseDataAttribute : DatabaseDataAttribute
{
protected override IEnumerable<IServiceProvider> GetDatabaseProviders(IConfiguration config)
{
var configureLogging = (ILoggingBuilder builder) =>
{
if (!config.GetValue<bool>("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<IGlobalSettings>(globalSettings);
dapperSqlServerCollection.AddSingleton(database);
dapperSqlServerCollection.AddDataProtection();
yield return dapperSqlServerCollection.BuildServiceProvider();
}
}
}
}

View File

@ -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<User> 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<Group> 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<Organization> 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<OrganizationUser> 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<Collection> CreateCollection(Organization organization, ICollectionRepository collectionRepository,
IEnumerable<CollectionAccessSelection>? groups = null, IEnumerable<CollectionAccessSelection>? 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 };
}
}

View File

@ -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",
});

View File

@ -33,7 +33,7 @@ public class DatabaseDataAttribute : DataAttribute
}
}
private IEnumerable<IServiceProvider> GetDatabaseProviders(IConfiguration config)
protected virtual IEnumerable<IServiceProvider> GetDatabaseProviders(IConfiguration config)
{
var configureLogging = (ILoggingBuilder builder) =>
{