mirror of
https://github.com/bitwarden/server
synced 2026-02-28 10:23:24 +00:00
Craft density modeling for Seeded vaults (#7102)
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Seeder.Data.Distributions;
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Options;
|
||||
using Bit.Seeder.Steps;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.SeederApi.IntegrationTest.DensityModel;
|
||||
|
||||
public class CreateCollectionsStepTests
|
||||
{
|
||||
private static readonly List<Guid> _collectionIds =
|
||||
[.. Enumerable.Range(1, 10).Select(i => new Guid($"00000000-0000-0000-0000-{i:D12}"))];
|
||||
|
||||
private static readonly List<Guid> _groupIds =
|
||||
[.. Enumerable.Range(1, 5).Select(i => new Guid($"11111111-0000-0000-0000-{i:D12}"))];
|
||||
|
||||
private static readonly List<Guid> _userIds =
|
||||
[.. Enumerable.Range(1, 20).Select(i => new Guid($"22222222-0000-0000-0000-{i:D12}"))];
|
||||
|
||||
private static readonly Distribution<PermissionWeight> EvenPermissions = new(
|
||||
(PermissionWeight.ReadOnly, 0.25),
|
||||
(PermissionWeight.ReadWrite, 0.25),
|
||||
(PermissionWeight.Manage, 0.25),
|
||||
(PermissionWeight.HidePasswords, 0.25));
|
||||
|
||||
[Fact]
|
||||
public void ApplyGroupPermissions_EvenSplit_DistributesAllFourTypes()
|
||||
{
|
||||
var assignments = Enumerable.Range(0, 100)
|
||||
.Select(_ => new CollectionGroup { CollectionId = Guid.NewGuid(), GroupId = Guid.NewGuid() })
|
||||
.ToList();
|
||||
|
||||
CreateCollectionsStep.ApplyGroupPermissions(assignments, EvenPermissions);
|
||||
|
||||
Assert.Equal(25, assignments.Count(a => a.ReadOnly));
|
||||
Assert.Equal(25, assignments.Count(a => a.Manage));
|
||||
Assert.Equal(25, assignments.Count(a => a.HidePasswords));
|
||||
Assert.Equal(25, assignments.Count(a => !a.ReadOnly && !a.Manage && !a.HidePasswords));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyGroupPermissions_MutuallyExclusiveFlags()
|
||||
{
|
||||
var assignments = Enumerable.Range(0, 100)
|
||||
.Select(_ => new CollectionGroup { CollectionId = Guid.NewGuid(), GroupId = Guid.NewGuid() })
|
||||
.ToList();
|
||||
|
||||
CreateCollectionsStep.ApplyGroupPermissions(assignments, EvenPermissions);
|
||||
|
||||
Assert.All(assignments, a =>
|
||||
{
|
||||
var flagCount = (a.ReadOnly ? 1 : 0) + (a.HidePasswords ? 1 : 0) + (a.Manage ? 1 : 0);
|
||||
Assert.True(flagCount <= 1, "At most one permission flag should be true");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyGroupPermissions_ReadOnlyHeavy_MajorityAreReadOnly()
|
||||
{
|
||||
var assignments = Enumerable.Range(0, 100)
|
||||
.Select(_ => new CollectionGroup { CollectionId = Guid.NewGuid(), GroupId = Guid.NewGuid() })
|
||||
.ToList();
|
||||
|
||||
CreateCollectionsStep.ApplyGroupPermissions(assignments, PermissionDistributions.Enterprise);
|
||||
|
||||
var readOnlyCount = assignments.Count(a => a.ReadOnly);
|
||||
Assert.True(readOnlyCount >= 80, $"Expected >= 80 ReadOnly, got {readOnlyCount}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyUserPermissions_EvenSplit_DistributesAllFourTypes()
|
||||
{
|
||||
var assignments = Enumerable.Range(0, 100)
|
||||
.Select(_ => new CollectionUser { CollectionId = Guid.NewGuid(), OrganizationUserId = Guid.NewGuid() })
|
||||
.ToList();
|
||||
|
||||
CreateCollectionsStep.ApplyUserPermissions(assignments, EvenPermissions);
|
||||
|
||||
Assert.Equal(25, assignments.Count(a => a.ReadOnly));
|
||||
Assert.Equal(25, assignments.Count(a => a.Manage));
|
||||
Assert.Equal(25, assignments.Count(a => a.HidePasswords));
|
||||
Assert.Equal(25, assignments.Count(a => !a.ReadOnly && !a.Manage && !a.HidePasswords));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCollectionGroups_ClampsToAvailableGroups()
|
||||
{
|
||||
var twoGroups = _groupIds.Take(2).ToList();
|
||||
var step = CreateStep(CollectionFanOutShape.Uniform, min: 5, max: 5);
|
||||
|
||||
var result = step.BuildCollectionGroups(_collectionIds, twoGroups);
|
||||
|
||||
Assert.All(result, cg => Assert.Contains(cg.GroupId, twoGroups));
|
||||
Assert.Equal(20, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCollectionGroups_NoDuplicateGroupPerCollection()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.Uniform, min: 3, max: 3);
|
||||
|
||||
var result = step.BuildCollectionGroups(_collectionIds, _groupIds);
|
||||
|
||||
foreach (var collectionId in _collectionIds)
|
||||
{
|
||||
var groupsForCollection = result.Where(cg => cg.CollectionId == collectionId)
|
||||
.Select(cg => cg.GroupId).ToList();
|
||||
Assert.Equal(groupsForCollection.Count, groupsForCollection.Distinct().Count());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCollectionGroups_Uniform_AssignsGroupsToEveryCollection()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.Uniform, min: 2, max: 2);
|
||||
|
||||
var result = step.BuildCollectionGroups(_collectionIds, _groupIds);
|
||||
|
||||
Assert.Equal(20, result.Count);
|
||||
Assert.All(result, cg => Assert.Contains(cg.GroupId, _groupIds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCollectionUsers_AllCollectionIdsAreValid()
|
||||
{
|
||||
var result = CreateCollectionsStep.BuildCollectionUsers(_collectionIds, _userIds, 10);
|
||||
|
||||
Assert.All(result, cu => Assert.Contains(cu.CollectionId, _collectionIds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCollectionUsers_AssignsOneToThreeCollectionsPerUser()
|
||||
{
|
||||
var result = CreateCollectionsStep.BuildCollectionUsers(_collectionIds, _userIds, 10);
|
||||
|
||||
var perUser = result.GroupBy(cu => cu.OrganizationUserId).ToList();
|
||||
Assert.All(perUser, group => Assert.InRange(group.Count(), 1, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCollectionUsers_RespectsDirectUserCount()
|
||||
{
|
||||
var result = CreateCollectionsStep.BuildCollectionUsers(_collectionIds, _userIds, 5);
|
||||
|
||||
var distinctUsers = result.Select(cu => cu.OrganizationUserId).Distinct().ToList();
|
||||
Assert.Equal(5, distinctUsers.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFanOut_FrontLoaded_FirstTenPercentGetMax()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.FrontLoaded, min: 1, max: 5);
|
||||
|
||||
Assert.Equal(5, step.ComputeFanOut(0, 100, 1, 5));
|
||||
Assert.Equal(5, step.ComputeFanOut(9, 100, 1, 5));
|
||||
Assert.Equal(1, step.ComputeFanOut(10, 100, 1, 5));
|
||||
Assert.Equal(1, step.ComputeFanOut(99, 100, 1, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFanOut_MinEqualsMax_AlwaysReturnsMin()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.Uniform, min: 3, max: 3);
|
||||
|
||||
Assert.Equal(3, step.ComputeFanOut(0, 10, 3, 3));
|
||||
Assert.Equal(3, step.ComputeFanOut(5, 10, 3, 3));
|
||||
Assert.Equal(3, step.ComputeFanOut(9, 10, 3, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFanOut_PowerLaw_FirstCollectionGetsMax()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.PowerLaw, min: 1, max: 5);
|
||||
|
||||
Assert.Equal(5, step.ComputeFanOut(0, 100, 1, 5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFanOut_PowerLaw_LaterCollectionsDecay()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.PowerLaw, min: 1, max: 5);
|
||||
|
||||
var first = step.ComputeFanOut(0, 100, 1, 5);
|
||||
var middle = step.ComputeFanOut(50, 100, 1, 5);
|
||||
var last = step.ComputeFanOut(99, 100, 1, 5);
|
||||
|
||||
Assert.True(first > middle, "First collection should have more fan-out than middle");
|
||||
Assert.True(middle >= last, "Middle should have >= fan-out than last");
|
||||
Assert.True(last >= 1, "Last collection should have at least min fan-out");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFanOut_Uniform_CyclesThroughRange()
|
||||
{
|
||||
var step = CreateStep(CollectionFanOutShape.Uniform, min: 1, max: 3);
|
||||
|
||||
Assert.Equal(1, step.ComputeFanOut(0, 10, 1, 3));
|
||||
Assert.Equal(2, step.ComputeFanOut(1, 10, 1, 3));
|
||||
Assert.Equal(3, step.ComputeFanOut(2, 10, 1, 3));
|
||||
Assert.Equal(1, step.ComputeFanOut(3, 10, 1, 3));
|
||||
}
|
||||
|
||||
private static CreateCollectionsStep CreateStep(CollectionFanOutShape shape, int min, int max)
|
||||
{
|
||||
var density = new DensityProfile
|
||||
{
|
||||
FanOutShape = shape,
|
||||
CollectionFanOutMin = min,
|
||||
CollectionFanOutMax = max
|
||||
};
|
||||
return CreateCollectionsStep.FromCount(0, density);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Options;
|
||||
using Bit.Seeder.Steps;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.SeederApi.IntegrationTest.DensityModel;
|
||||
|
||||
public class CreateGroupsStepTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_MegaGroup_GroupZeroDoesNotParticipateInRemainder()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.MegaGroup, skew: 0.5);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(5, 100);
|
||||
|
||||
var megaFraction = 0.5 + 0.5 * 0.45; // 0.725
|
||||
var expectedMega = (int)(100 * megaFraction); // 72
|
||||
Assert.Equal(expectedMega, allocations[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_MegaGroup_RemainderGoesToNonMegaGroups()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.MegaGroup, skew: 0.5);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(5, 100);
|
||||
|
||||
var nonMegaTotal = allocations[1] + allocations[2] + allocations[3] + allocations[4];
|
||||
Assert.Equal(100 - allocations[0], nonMegaTotal);
|
||||
Assert.True(allocations[1] > 0, "Non-mega groups should have members");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_MegaGroup_SingleGroup_AllUsersAssigned()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.MegaGroup, skew: 0.9);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(1, 100);
|
||||
|
||||
Assert.Single(allocations);
|
||||
Assert.Equal(100, allocations[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_MegaGroup_SingleUser_SingleGroup()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.MegaGroup, skew: 1.0);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(1, 1);
|
||||
|
||||
Assert.Equal(1, allocations[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_MegaGroup_SumsToUserCount()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.MegaGroup, skew: 0.8);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(10, 100);
|
||||
|
||||
Assert.Equal(100, allocations.Sum());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_PowerLaw_FirstGroupIsLargest()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.PowerLaw, skew: 0.8);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(10, 100);
|
||||
|
||||
Assert.Equal(allocations.Max(), allocations[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_PowerLaw_HighSkewMoreConcentrated()
|
||||
{
|
||||
var gentle = CreateStep(MembershipDistributionShape.PowerLaw, skew: 0.0);
|
||||
var steep = CreateStep(MembershipDistributionShape.PowerLaw, skew: 1.0);
|
||||
|
||||
var gentleAllocations = gentle.ComputeUsersPerGroup(10, 100);
|
||||
var steepAllocations = steep.ComputeUsersPerGroup(10, 100);
|
||||
|
||||
Assert.True(steepAllocations[0] > gentleAllocations[0],
|
||||
$"Steep skew group 0 ({steepAllocations[0]}) should be larger than gentle ({gentleAllocations[0]})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_PowerLaw_MoreGroupsThanUsers_NoNegativeAllocations()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.PowerLaw, skew: 1.0);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(20, 5);
|
||||
|
||||
Assert.All(allocations, a => Assert.True(a >= 0, $"Allocation should be >= 0, got {a}"));
|
||||
Assert.Equal(5, allocations.Sum());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_PowerLaw_SumsToUserCount()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.PowerLaw, skew: 0.5);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(10, 100);
|
||||
|
||||
Assert.Equal(100, allocations.Sum());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_Uniform_EvenDistribution()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.Uniform);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(5, 100);
|
||||
|
||||
Assert.All(allocations, a => Assert.Equal(20, a));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeUsersPerGroup_Uniform_SumsToUserCount()
|
||||
{
|
||||
var step = CreateStep(MembershipDistributionShape.Uniform);
|
||||
|
||||
var allocations = step.ComputeUsersPerGroup(7, 100);
|
||||
|
||||
Assert.Equal(100, allocations.Sum());
|
||||
}
|
||||
|
||||
private static CreateGroupsStep CreateStep(MembershipDistributionShape shape, double skew = 0.0)
|
||||
{
|
||||
var density = new DensityProfile
|
||||
{
|
||||
MembershipShape = shape,
|
||||
MembershipSkew = skew
|
||||
};
|
||||
return new CreateGroupsStep(0, density);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user