mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-24582] Bugfix: exclude admins and owners from default user collection creation on confirmation (#6177)
* Update the OrganizationUserController integration Confirm tests to handle the Owner type * Refactor ConfirmOrganizationUserCommand to simplify side-effect handling in organization user confirmation. Update IPolicyRequirementQuery to return eligible org user IDs for policy enforcement. Update tests for method signature changes and default collection creation logic.
This commit is contained in:
@@ -22,7 +22,6 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
private static readonly string _mockEncryptedString =
|
||||
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
|
||||
|
||||
public OrganizationUserControllerTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
@@ -115,7 +114,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
{
|
||||
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
|
||||
|
||||
var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { "test1@bitwarden.com" })).First();
|
||||
var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { ("test1@bitwarden.com", OrganizationUserType.User) })).First();
|
||||
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
@@ -129,7 +128,29 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
Assert.Equal(HttpStatusCode.OK, confirmResponse.StatusCode);
|
||||
|
||||
await VerifyUserConfirmedAsync(acceptedOrgUser, "test-key");
|
||||
await VerifyDefaultCollectionCreatedAsync(acceptedOrgUser);
|
||||
await VerifyDefaultCollectionCountAsync(acceptedOrgUser, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Confirm_WithValidOwner_ReturnsSuccess()
|
||||
{
|
||||
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
|
||||
|
||||
var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { ("owner1@bitwarden.com", OrganizationUserType.Owner) })).First();
|
||||
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var confirmModel = new OrganizationUserConfirmRequestModel
|
||||
{
|
||||
Key = "test-key",
|
||||
DefaultUserCollectionName = _mockEncryptedString
|
||||
};
|
||||
var confirmResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/{acceptedOrgUser.Id}/confirm", confirmModel);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, confirmResponse.StatusCode);
|
||||
|
||||
await VerifyUserConfirmedAsync(acceptedOrgUser, "test-key");
|
||||
await VerifyDefaultCollectionCountAsync(acceptedOrgUser, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -138,8 +159,11 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
const string testKeyFormat = "test-key-{0}";
|
||||
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
|
||||
|
||||
var emails = new[] { "test1@example.com", "test2@example.com", "test3@example.com" };
|
||||
var acceptedUsers = await CreateAcceptedUsersAsync(emails);
|
||||
var acceptedUsers = await CreateAcceptedUsersAsync([
|
||||
("test1@example.com", OrganizationUserType.User),
|
||||
("test2@example.com", OrganizationUserType.Owner),
|
||||
("test3@example.com", OrganizationUserType.User)
|
||||
]);
|
||||
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
@@ -159,7 +183,9 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
|
||||
await VerifyMultipleUsersConfirmedAsync(acceptedUsers.Select((organizationUser, index) =>
|
||||
(organizationUser, string.Format(testKeyFormat, index))).ToList());
|
||||
await VerifyMultipleUsersHaveDefaultCollectionsAsync(acceptedUsers);
|
||||
await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(0), 1);
|
||||
await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(1), 0); // Owner does not get a default collection
|
||||
await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(2), 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -294,16 +320,18 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
Assert.False(newCollectionAccess.Manage);
|
||||
}
|
||||
|
||||
private async Task<List<OrganizationUser>> CreateAcceptedUsersAsync(IEnumerable<string> emails)
|
||||
private async Task<List<OrganizationUser>> CreateAcceptedUsersAsync(
|
||||
IEnumerable<(string email, OrganizationUserType userType)> newUsers)
|
||||
{
|
||||
var acceptedUsers = new List<OrganizationUser>();
|
||||
|
||||
foreach (var email in emails)
|
||||
foreach (var (email, userType) in newUsers)
|
||||
{
|
||||
await _factory.LoginWithNewAccount(email);
|
||||
|
||||
var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email,
|
||||
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);
|
||||
var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(
|
||||
_factory, _organization.Id, email,
|
||||
userType, userStatusType: OrganizationUserStatusType.Accepted);
|
||||
|
||||
acceptedUsers.Add(acceptedOrgUser);
|
||||
}
|
||||
@@ -311,12 +339,11 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
return acceptedUsers;
|
||||
}
|
||||
|
||||
private async Task VerifyDefaultCollectionCreatedAsync(OrganizationUser orgUser)
|
||||
private async Task VerifyDefaultCollectionCountAsync(OrganizationUser orgUser, int expectedCount)
|
||||
{
|
||||
var collectionRepository = _factory.GetService<ICollectionRepository>();
|
||||
var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value);
|
||||
Assert.Single(collections);
|
||||
Assert.Equal(_mockEncryptedString, collections.First().Name);
|
||||
Assert.Equal(expectedCount, collections.Count);
|
||||
}
|
||||
|
||||
private async Task VerifyUserConfirmedAsync(OrganizationUser orgUser, string expectedKey)
|
||||
@@ -334,15 +361,4 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
|
||||
Assert.Equal(acceptedOrganizationUsers[i].key, confirmedUser.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task VerifyMultipleUsersHaveDefaultCollectionsAsync(List<OrganizationUser> acceptedOrganizationUsers)
|
||||
{
|
||||
var collectionRepository = _factory.GetService<ICollectionRepository>();
|
||||
foreach (var acceptedOrganizationUser in acceptedOrganizationUsers)
|
||||
{
|
||||
var collections = await collectionRepository.GetManyByUserIdAsync(acceptedOrganizationUser.UserId!.Value);
|
||||
Assert.Single(collections);
|
||||
Assert.Equal(_mockEncryptedString, collections.First().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -473,10 +472,8 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetByOrganizationAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[organization.Id]));
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
|
||||
.Returns(new List<Guid> { orgUser.Id });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||
|
||||
@@ -504,17 +501,11 @@ public class ConfirmOrganizationUserCommandTests
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetByOrganizationAsync<OrganizationDataOwnershipPolicyRequirement>(org.Id)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[org.Id]));
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, "");
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -533,15 +524,13 @@ public class ConfirmOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetByOrganizationAsync<OrganizationDataOwnershipPolicyRequirement>(org.Id)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[Guid.NewGuid()]));
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(org.Id)
|
||||
.Returns(new List<Guid> { orgUser.UserId!.Value });
|
||||
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +81,11 @@ public class PolicyRequirementQueryTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetByOrganizationAsync_IgnoresOtherPolicyTypes(Guid organizationId)
|
||||
public async Task GetManyByOrganizationIdAsync_IgnoresOtherPolicyTypes(Guid organizationId)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() };
|
||||
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = Guid.NewGuid() };
|
||||
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() };
|
||||
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, OrganizationUserId = Guid.NewGuid() };
|
||||
// Force the repository to return both policies even though that is not the expected result
|
||||
policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg)
|
||||
.Returns([thisPolicy, otherPolicy]);
|
||||
@@ -93,20 +93,20 @@ public class PolicyRequirementQueryTests
|
||||
var factory = new TestPolicyRequirementFactory(_ => true);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
var requirement = await sut.GetByOrganizationAsync<TestPolicyRequirement>(organizationId);
|
||||
var organizationUserIds = await sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId);
|
||||
|
||||
await policyRepository.Received(1).GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg);
|
||||
|
||||
Assert.Contains(thisPolicy, requirement.Policies.Cast<OrganizationPolicyDetails>());
|
||||
Assert.DoesNotContain(otherPolicy, requirement.Policies.Cast<OrganizationPolicyDetails>());
|
||||
Assert.Contains(thisPolicy.OrganizationUserId, organizationUserIds);
|
||||
Assert.DoesNotContain(otherPolicy.OrganizationUserId, organizationUserIds);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetByOrganizationAsync_CallsEnforceCallback(Guid organizationId)
|
||||
public async Task GetManyByOrganizationIdAsync_CallsEnforceCallback(Guid organizationId)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() };
|
||||
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() };
|
||||
var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() };
|
||||
var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() };
|
||||
policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([thisPolicy, otherPolicy]);
|
||||
|
||||
var callback = Substitute.For<Func<PolicyDetails, bool>>();
|
||||
@@ -115,28 +115,28 @@ public class PolicyRequirementQueryTests
|
||||
var factory = new TestPolicyRequirementFactory(callback);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
var requirement = await sut.GetByOrganizationAsync<TestPolicyRequirement>(organizationId);
|
||||
var organizationUserIds = await sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId);
|
||||
|
||||
Assert.Contains(thisPolicy, requirement.Policies.Cast<OrganizationPolicyDetails>());
|
||||
Assert.DoesNotContain(otherPolicy, requirement.Policies.Cast<OrganizationPolicyDetails>());
|
||||
Assert.Contains(thisPolicy.OrganizationUserId, organizationUserIds);
|
||||
Assert.DoesNotContain(otherPolicy.OrganizationUserId, organizationUserIds);
|
||||
callback.Received()(Arg.Is<PolicyDetails>(p => p == thisPolicy));
|
||||
callback.Received()(Arg.Is<PolicyDetails>(p => p == otherPolicy));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetByOrganizationAsync_ThrowsIfNoFactoryRegistered(Guid organizationId)
|
||||
public async Task GetManyByOrganizationIdAsync_ThrowsIfNoFactoryRegistered(Guid organizationId)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
var sut = new PolicyRequirementQuery(policyRepository, []);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotImplementedException>(()
|
||||
=> sut.GetByOrganizationAsync<TestPolicyRequirement>(organizationId));
|
||||
=> sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId));
|
||||
|
||||
Assert.Contains("No Requirement Factory found", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetByOrganizationAsync_HandlesNoPolicies(Guid organizationId)
|
||||
public async Task GetManyByOrganizationIdAsync_HandlesNoPolicies(Guid organizationId)
|
||||
{
|
||||
var policyRepository = Substitute.For<IPolicyRepository>();
|
||||
policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([]);
|
||||
@@ -144,8 +144,8 @@ public class PolicyRequirementQueryTests
|
||||
var factory = new TestPolicyRequirementFactory(x => x.IsProvider);
|
||||
var sut = new PolicyRequirementQuery(policyRepository, [factory]);
|
||||
|
||||
var requirement = await sut.GetByOrganizationAsync<TestPolicyRequirement>(organizationId);
|
||||
var organizationUserIds = await sut.GetManyByOrganizationIdAsync<TestPolicyRequirement>(organizationId);
|
||||
|
||||
Assert.Empty(requirement.Policies);
|
||||
Assert.Empty(organizationUserIds);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user