1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[AC-2522] Remove collection enhancements opt-in (#4110)

* Delete controller endpoint
* Delete command
* Drop sproc
This commit is contained in:
Thomas Rittson 2024-05-24 09:00:04 +10:00 committed by GitHub
parent ba93c0008b
commit be41865b59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 5 additions and 1101 deletions

View File

@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
@ -53,7 +52,6 @@ public class OrganizationsController : Controller
private readonly IFeatureService _featureService;
private readonly GlobalSettings _globalSettings;
private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
@ -74,7 +72,6 @@ public class OrganizationsController : Controller
IFeatureService featureService,
GlobalSettings globalSettings,
IPushNotificationService pushNotificationService,
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
IProviderRepository providerRepository,
IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory)
@ -94,7 +91,6 @@ public class OrganizationsController : Controller
_featureService = featureService;
_globalSettings = globalSettings;
_pushNotificationService = pushNotificationService;
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
_providerRepository = providerRepository;
_providerBillingService = providerBillingService;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
@ -558,38 +554,4 @@ public class OrganizationsController : Controller
await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization);
}
/// <summary>
/// Migrates user, collection, and group data to the new Flexible Collections permissions scheme,
/// then sets organization.FlexibleCollections to true to enable these new features for the organization.
/// This is irreversible.
/// </summary>
/// <param name="organizationId"></param>
/// <exception cref="NotFoundException"></exception>
[HttpPost("{id}/enable-collection-enhancements")]
[RequireFeature(FeatureFlagKeys.FlexibleCollectionsMigration)]
public async Task EnableCollectionEnhancements(Guid id)
{
if (!await _currentContext.OrganizationOwner(id))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
throw new NotFoundException();
}
await _organizationEnableCollectionEnhancementsCommand.EnableCollectionEnhancements(organization);
// Force a vault sync for all owners and admins of the organization so that changes show immediately
// Custom users are intentionally not handled as they are likely to be less impacted and we want to limit simultaneous syncs
var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, null);
await Task.WhenAll(orgUsers
.Where(ou => ou.UserId.HasValue &&
ou.Status == OrganizationUserStatusType.Confirmed &&
ou.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
.Select(ou => _pushNotificationService.PushSyncOrganizationsAsync(ou.UserId.Value)));
}
}

View File

@ -1,12 +0,0 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
/// <summary>
/// Enable collection enhancements for an organization.
/// This command will be deprecated once all organizations have collection enhancements enabled.
/// </summary>
public interface IOrganizationEnableCollectionEnhancementsCommand
{
Task EnableCollectionEnhancements(Organization organization);
}

View File

@ -1,113 +0,0 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements;
public class OrganizationEnableCollectionEnhancementsCommand : IOrganizationEnableCollectionEnhancementsCommand
{
private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly ILogger<OrganizationEnableCollectionEnhancementsCommand> _logger;
public OrganizationEnableCollectionEnhancementsCommand(ICollectionRepository collectionRepository,
IGroupRepository groupRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
ILogger<OrganizationEnableCollectionEnhancementsCommand> logger)
{
_collectionRepository = collectionRepository;
_groupRepository = groupRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_logger = logger;
}
public async Task EnableCollectionEnhancements(Organization organization)
{
if (organization.FlexibleCollections)
{
throw new BadRequestException("Organization has already been migrated to the new collection enhancements");
}
// Log the Organization data that will change when the migration is complete
await LogPreMigrationDataAsync(organization.Id);
// Run the data migration script
await _organizationRepository.EnableCollectionEnhancements(organization.Id);
organization.FlexibleCollections = true;
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
}
/// <summary>
/// This method logs the data that will be migrated to the new collection enhancements so that it can be restored if needed
/// </summary>
/// <param name="organizationId"></param>
private async Task LogPreMigrationDataAsync(Guid organizationId)
{
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
// Grab Group Ids that have AccessAll enabled as it will be removed in the data migration
var groupIdsWithAccessAllEnabled = groups
.Where(g => g.AccessAll)
.Select(g => g.Id)
.ToList();
var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, type: null);
// Grab OrganizationUser Ids that have AccessAll enabled as it will be removed in the data migration
var organizationUserIdsWithAccessAllEnabled = organizationUsers
.Where(ou => ou.AccessAll)
.Select(ou => ou.Id)
.ToList();
// Grab OrganizationUser Ids of Manager users as that will be downgraded to User in the data migration
var migratedManagers = organizationUsers
.Where(ou => ou.Type == OrganizationUserType.Manager)
.Select(ou => ou.Id)
.ToList();
var usersEligibleToManageCollections = organizationUsers
.Where(ou =>
ou.Type == OrganizationUserType.Manager ||
(ou.Type == OrganizationUserType.Custom &&
!string.IsNullOrEmpty(ou.Permissions) &&
ou.GetPermissions().EditAssignedCollections)
)
.Select(ou => ou.Id)
.ToList();
var collectionUsers = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(organizationId);
// Grab CollectionUser permissions that will change in the data migration
var collectionUsersData = collectionUsers.SelectMany(tuple =>
tuple.Item2.Users.Select(user =>
new
{
CollectionId = tuple.Item1.Id,
OrganizationUserId = user.Id,
user.ReadOnly,
user.HidePasswords
}))
.Where(cud => usersEligibleToManageCollections.Any(ou => ou == cud.OrganizationUserId))
.ToList();
var logObject = new
{
OrganizationId = organizationId,
GroupAccessAll = groupIdsWithAccessAllEnabled,
UserAccessAll = organizationUserIdsWithAccessAllEnabled,
MigratedManagers = migratedManagers,
CollectionUsers = collectionUsersData
};
_logger.LogWarning("Flexible Collections data migration started. Backup data: {LogObject}", JsonSerializer.Serialize(logObject));
}
}

View File

@ -15,5 +15,4 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id);
Task<ICollection<Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take);
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
Task EnableCollectionEnhancements(Guid organizationId);
}

View File

@ -114,11 +114,6 @@ public static class FeatureFlagKeys
public const string ItemShare = "item-share";
public const string KeyRotationImprovements = "key-rotation-improvements";
public const string DuoRedirect = "duo-redirect";
/// <summary>
/// Exposes a migration button in the web vault which allows users to migrate an existing organization to
/// flexible collections
/// </summary>
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string PM5766AutomaticTax = "PM-5766-automatic-tax";
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";

View File

@ -4,8 +4,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Groups;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
@ -52,7 +50,6 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries();
services.AddBaseOrganizationSubscriptionCommandsQueries();
services.AddOrganizationCollectionEnhancementsCommands();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
@ -148,11 +145,6 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();
}
private static void AddOrganizationCollectionEnhancementsCommands(this IServiceCollection services)
{
services.AddScoped<IOrganizationEnableCollectionEnhancementsCommand, OrganizationEnableCollectionEnhancementsCommand>();
}
private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>

View File

@ -169,16 +169,4 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
}
public async Task EnableCollectionEnhancements(Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
await connection.ExecuteAsync(
"[dbo].[Organization_EnableCollectionEnhancements]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure,
commandTimeout: 180);
}
}
}

View File

@ -1,155 +0,0 @@
CREATE PROCEDURE [dbo].[Organization_EnableCollectionEnhancements]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
-- Step 1: AccessAll migration for Groups
-- Create a temporary table to store the groups with AccessAll = 1
SELECT [Id] AS [GroupId], [OrganizationId]
INTO #TempGroupsAccessAll
FROM [dbo].[Group]
WHERE [OrganizationId] = @OrganizationId
AND [AccessAll] = 1;
-- Step 2: AccessAll migration for OrganizationUsers
-- Create a temporary table to store the OrganizationUsers with AccessAll = 1
SELECT [Id] AS [OrganizationUserId], [OrganizationId]
INTO #TempUsersAccessAll
FROM [dbo].[OrganizationUser]
WHERE [OrganizationId] = @OrganizationId
AND [AccessAll] = 1;
-- Step 3: For all OrganizationUsers with Manager role or 'EditAssignedCollections' permission update their existing CollectionUser rows and insert new rows with [Manage] = 1
-- and finally update all OrganizationUsers with Manager role to User role
-- Create a temporary table to store the OrganizationUsers with Manager role or 'EditAssignedCollections' permission
SELECT ou.[Id] AS [OrganizationUserId],
CASE WHEN ou.[Type] = 3 THEN 1 ELSE 0 END AS [IsManager]
INTO #TempUserManagers
FROM [dbo].[OrganizationUser] ou
WHERE ou.[OrganizationId] = @OrganizationId
AND (ou.[Type] = 3 OR (ou.[Permissions] IS NOT NULL
AND ISJSON(ou.[Permissions]) > 0 AND JSON_VALUE(ou.[Permissions], '$.editAssignedCollections') = 'true'));
-- Step 4: Bump AccountRevisionDate for all OrganizationUsers updated in the previous steps
-- Combine and union the distinct OrganizationUserIds from all steps into a single variable
DECLARE @OrgUsersToBump [dbo].[GuidIdArray]
INSERT INTO @OrgUsersToBump
SELECT DISTINCT [OrganizationUserId] AS Id
FROM (
-- Step 1
SELECT GU.[OrganizationUserId]
FROM [dbo].[GroupUser] GU
INNER JOIN #TempGroupsAccessAll TG ON GU.[GroupId] = TG.[GroupId]
UNION
-- Step 2
SELECT [OrganizationUserId]
FROM #TempUsersAccessAll
UNION
-- Step 3
SELECT [OrganizationUserId]
FROM #TempUserManagers
) AS CombinedOrgUsers;
BEGIN TRY
BEGIN TRANSACTION;
-- Step 1
-- Update existing rows in [dbo].[CollectionGroup]
UPDATE CG
SET
CG.[ReadOnly] = 0,
CG.[HidePasswords] = 0,
CG.[Manage] = 0
FROM [dbo].[CollectionGroup] CG
INNER JOIN [dbo].[Collection] C ON CG.[CollectionId] = C.[Id]
INNER JOIN #TempGroupsAccessAll TG ON CG.[GroupId] = TG.[GroupId]
WHERE C.[OrganizationId] = TG.[OrganizationId];
-- Insert new rows into [dbo].[CollectionGroup]
INSERT INTO [dbo].[CollectionGroup] ([CollectionId], [GroupId], [ReadOnly], [HidePasswords], [Manage])
SELECT C.[Id], TG.[GroupId], 0, 0, 0
FROM [dbo].[Collection] C
INNER JOIN #TempGroupsAccessAll TG ON C.[OrganizationId] = TG.[OrganizationId]
LEFT JOIN [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = TG.[GroupId]
WHERE CG.[CollectionId] IS NULL;
-- Update Group to clear AccessAll flag and update RevisionDate
UPDATE G
SET [AccessAll] = 0, [RevisionDate] = GETUTCDATE()
FROM [dbo].[Group] G
INNER JOIN #TempGroupsAccessAll TG ON G.[Id] = TG.[GroupId];
-- Step 2
-- Update existing rows in [dbo].[CollectionUser]
UPDATE target
SET
target.[ReadOnly] = 0,
target.[HidePasswords] = 0,
target.[Manage] = 0
FROM [dbo].[CollectionUser] AS target
INNER JOIN [dbo].[Collection] AS C ON target.[CollectionId] = C.[Id]
INNER JOIN #TempUsersAccessAll AS TU ON C.[OrganizationId] = TU.[OrganizationId] AND target.[OrganizationUserId] = TU.[OrganizationUserId];
-- Insert new rows into [dbo].[CollectionUser]
INSERT INTO [dbo].[CollectionUser] ([CollectionId], [OrganizationUserId], [ReadOnly], [HidePasswords], [Manage])
SELECT C.[Id] AS [CollectionId], TU.[OrganizationUserId], 0, 0, 0
FROM [dbo].[Collection] C
INNER JOIN #TempUsersAccessAll TU ON C.[OrganizationId] = TU.[OrganizationId]
LEFT JOIN [dbo].[CollectionUser] target
ON target.[CollectionId] = C.[Id] AND target.[OrganizationUserId] = TU.[OrganizationUserId]
WHERE target.[CollectionId] IS NULL;
-- Update OrganizationUser to clear AccessAll flag
UPDATE OU
SET [AccessAll] = 0, [RevisionDate] = GETUTCDATE()
FROM [dbo].[OrganizationUser] OU
INNER JOIN #TempUsersAccessAll TU ON OU.[Id] = TU.[OrganizationUserId];
-- Step 3
-- Update [dbo].[CollectionUser] with [Manage] = 1 using the temporary table
UPDATE CU
SET CU.[ReadOnly] = 0,
CU.[HidePasswords] = 0,
CU.[Manage] = 1
FROM [dbo].[CollectionUser] CU
INNER JOIN #TempUserManagers TUM ON CU.[OrganizationUserId] = TUM.[OrganizationUserId];
-- Insert rows to [dbo].[CollectionUser] with [Manage] = 1 using the temporary table
-- This is for orgUsers who are Managers / EditAssignedCollections but have access via a group
-- We cannot give the whole group Manage permissions so we have to give them a direct assignment
INSERT INTO [dbo].[CollectionUser] ([CollectionId], [OrganizationUserId], [ReadOnly], [HidePasswords], [Manage])
SELECT DISTINCT CG.[CollectionId], TUM.[OrganizationUserId], 0, 0, 1
FROM [dbo].[CollectionGroup] CG
INNER JOIN [dbo].[GroupUser] GU ON CG.[GroupId] = GU.[GroupId]
INNER JOIN #TempUserManagers TUM ON GU.[OrganizationUserId] = TUM.[OrganizationUserId]
WHERE NOT EXISTS (
SELECT 1 FROM [dbo].[CollectionUser] CU
WHERE CU.[CollectionId] = CG.[CollectionId] AND CU.[OrganizationUserId] = TUM.[OrganizationUserId]
);
-- Update [dbo].[OrganizationUser] to migrate all OrganizationUsers with Manager role to User role
UPDATE OU
SET OU.[Type] = 2, OU.[RevisionDate] = GETUTCDATE() -- User
FROM [dbo].[OrganizationUser] OU
INNER JOIN #TempUserManagers TUM ON ou.[Id] = TUM.[OrganizationUserId]
WHERE TUM.[IsManager] = 1; -- Filter for Managers
-- Step 4
-- Execute User_BumpAccountRevisionDateByOrganizationUserIds for the distinct OrganizationUserIds
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrgUsersToBump;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH;
-- Drop the temporary table
DROP TABLE #TempGroupsAccessAll;
DROP TABLE #TempUsersAccessAll;
DROP TABLE #TempUserManagers;
END

View File

@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
@ -46,7 +45,6 @@ public class OrganizationsControllerTests : IDisposable
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly IFeatureService _featureService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
@ -70,7 +68,6 @@ public class OrganizationsControllerTests : IDisposable
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
_featureService = Substitute.For<IFeatureService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_organizationEnableCollectionEnhancementsCommand = Substitute.For<IOrganizationEnableCollectionEnhancementsCommand>();
_providerRepository = Substitute.For<IProviderRepository>();
_providerBillingService = Substitute.For<IProviderBillingService>();
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
@ -91,7 +88,6 @@ public class OrganizationsControllerTests : IDisposable
_featureService,
_globalSettings,
_pushNotificationService,
_organizationEnableCollectionEnhancementsCommand,
_providerRepository,
_providerBillingService,
_orgDeleteTokenDataFactory);
@ -162,48 +158,6 @@ public class OrganizationsControllerTests : IDisposable
await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id);
}
[Theory, AutoData]
public async Task EnableCollectionEnhancements_Success(Organization organization)
{
organization.FlexibleCollections = false;
var admin = new OrganizationUser { UserId = Guid.NewGuid(), Type = OrganizationUserType.Admin, Status = OrganizationUserStatusType.Confirmed };
var owner = new OrganizationUser { UserId = Guid.NewGuid(), Type = OrganizationUserType.Owner, Status = OrganizationUserStatusType.Confirmed };
var user = new OrganizationUser { UserId = Guid.NewGuid(), Type = OrganizationUserType.User, Status = OrganizationUserStatusType.Confirmed };
var invited = new OrganizationUser
{
UserId = null,
Type = OrganizationUserType.Admin,
Email = "invited@example.com",
Status = OrganizationUserStatusType.Invited
};
var orgUsers = new List<OrganizationUser> { admin, owner, user, invited };
_currentContext.OrganizationOwner(organization.Id).Returns(true);
_organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
_organizationUserRepository.GetManyByOrganizationAsync(organization.Id, null).Returns(orgUsers);
await _sut.EnableCollectionEnhancements(organization.Id);
await _organizationEnableCollectionEnhancementsCommand.Received(1).EnableCollectionEnhancements(organization);
await _pushNotificationService.Received(1).PushSyncOrganizationsAsync(admin.UserId.Value);
await _pushNotificationService.Received(1).PushSyncOrganizationsAsync(owner.UserId.Value);
await _pushNotificationService.DidNotReceive().PushSyncOrganizationsAsync(user.UserId.Value);
// Invited orgUser does not have a UserId we can use to assert here, but sut will throw if that null isn't handled
}
[Theory, AutoData]
public async Task EnableCollectionEnhancements_WhenNotOwner_Throws(Organization organization)
{
organization.FlexibleCollections = false;
_currentContext.OrganizationOwner(organization.Id).Returns(false);
_organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
await Assert.ThrowsAsync<NotFoundException>(async () => await _sut.EnableCollectionEnhancements(organization.Id));
await _organizationEnableCollectionEnhancementsCommand.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any<Organization>());
await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncOrganizationsAsync(Arg.Any<Guid>());
}
[Theory, AutoData]
public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats(
Provider provider,

View File

@ -1,46 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements;
[SutProviderCustomize]
public class OrganizationEnableCollectionEnhancementsCommandTests
{
[Theory]
[BitAutoData]
public async Task EnableCollectionEnhancements_Success(
SutProvider<OrganizationEnableCollectionEnhancementsCommand> sutProvider,
Organization organization)
{
organization.FlexibleCollections = false;
await sutProvider.Sut.EnableCollectionEnhancements(organization);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).EnableCollectionEnhancements(organization.Id);
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(
Arg.Is<Organization>(o =>
o.Id == organization.Id &&
o.FlexibleCollections));
}
[Theory]
[BitAutoData]
public async Task EnableCollectionEnhancements_WhenAlreadyMigrated_Throws(
SutProvider<OrganizationEnableCollectionEnhancementsCommand> sutProvider,
Organization organization)
{
organization.FlexibleCollections = true;
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.EnableCollectionEnhancements(organization));
Assert.Contains("has already been migrated", exception.Message);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any<Guid>());
}
}

View File

@ -1,54 +0,0 @@
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

@ -1,611 +0,0 @@
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

@ -0,0 +1,5 @@
IF OBJECT_ID('[dbo].[Organization_EnableCollectionEnhancements]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Organization_EnableCollectionEnhancements]
END
GO