server/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
Rui Tomé e042572cfb
[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.
2025-08-11 16:36:40 +01:00

365 lines
15 KiB
C#

using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
public OrganizationUserControllerTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = new List<Guid> { Guid.NewGuid() }
};
var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/remove", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task DeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var userToRemove = Guid.NewGuid();
var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}");
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task GetAccountRecoveryDetails_WithoutManageResetPasswordPermission_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = []
};
var httpResponse =
await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/account-recovery-details", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
public async Task InitializeAsync()
{
_ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
}
[Fact]
public async Task Confirm_WithValidUser_ReturnsSuccess()
{
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { ("test1@bitwarden.com", OrganizationUserType.User) })).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, 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]
public async Task BulkConfirm_WithValidUsers_ReturnsSuccess()
{
const string testKeyFormat = "test-key-{0}";
await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);
var acceptedUsers = await CreateAcceptedUsersAsync([
("test1@example.com", OrganizationUserType.User),
("test2@example.com", OrganizationUserType.Owner),
("test3@example.com", OrganizationUserType.User)
]);
await _loginHelper.LoginAsync(_ownerEmail);
var bulkConfirmModel = new OrganizationUserBulkConfirmRequestModel
{
Keys = acceptedUsers.Select((organizationUser, index) => new OrganizationUserBulkConfirmRequestModelEntry
{
Id = organizationUser.Id,
Key = string.Format(testKeyFormat, index)
}),
DefaultUserCollectionName = _mockEncryptedString
};
var bulkConfirmResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/confirm", bulkConfirmModel);
Assert.Equal(HttpStatusCode.OK, bulkConfirmResponse.StatusCode);
await VerifyMultipleUsersConfirmedAsync(acceptedUsers.Select((organizationUser, index) =>
(organizationUser, string.Format(testKeyFormat, index))).ToList());
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]
public async Task Put_WithExistingDefaultCollection_Success()
{
// Arrange
await _loginHelper.LoginAsync(_ownerEmail);
var (userEmail, organizationUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.User);
var (group, sharedCollection, defaultCollection) = await CreateTestDataAsync();
await AssignDefaultCollectionToUserAsync(organizationUser, defaultCollection);
// Act
var updateRequest = CreateUpdateRequest(sharedCollection, group);
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/{organizationUser.Id}", updateRequest);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
// Assert
await VerifyUserWasUpdatedCorrectlyAsync(organizationUser, expectedType: OrganizationUserType.Custom, expectedManageGroups: true);
await VerifyGroupAccessWasAddedAsync(organizationUser, [group]);
await VerifyCollectionAccessWasUpdatedCorrectlyAsync(organizationUser, sharedCollection.Id, defaultCollection.Id);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
private async Task<(Group group, Collection sharedCollection, Collection defaultCollection)> CreateTestDataAsync()
{
var groupRepository = _factory.GetService<IGroupRepository>();
var group = await groupRepository.CreateAsync(new Group
{
OrganizationId = _organization.Id,
Name = $"Test Group {Guid.NewGuid()}"
});
var collectionRepository = _factory.GetService<ICollectionRepository>();
var sharedCollection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = _organization.Id,
Name = $"Test Collection {Guid.NewGuid()}",
Type = CollectionType.SharedCollection
});
var defaultCollection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = _organization.Id,
Name = $"My Items {Guid.NewGuid()}",
Type = CollectionType.DefaultUserCollection
});
return (group, sharedCollection, defaultCollection);
}
private async Task AssignDefaultCollectionToUserAsync(OrganizationUser organizationUser, Collection defaultCollection)
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
await organizationUserRepository.ReplaceAsync(organizationUser,
new List<CollectionAccessSelection>
{
new CollectionAccessSelection
{
Id = defaultCollection.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
}
});
}
private static OrganizationUserUpdateRequestModel CreateUpdateRequest(Collection sharedCollection, Group group)
{
return new OrganizationUserUpdateRequestModel
{
Type = OrganizationUserType.Custom,
Permissions = new Permissions
{
ManageGroups = true
},
Collections = new List<SelectionReadOnlyRequestModel>
{
new SelectionReadOnlyRequestModel
{
Id = sharedCollection.Id,
ReadOnly = true,
HidePasswords = false,
Manage = false
}
},
Groups = new List<Guid> { group.Id }
};
}
private async Task VerifyUserWasUpdatedCorrectlyAsync(
OrganizationUser organizationUser,
OrganizationUserType expectedType,
bool expectedManageGroups)
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var updatedOrgUser = await organizationUserRepository.GetByIdAsync(organizationUser.Id);
Assert.NotNull(updatedOrgUser);
Assert.Equal(expectedType, updatedOrgUser.Type);
Assert.Equal(expectedManageGroups, updatedOrgUser.GetPermissions().ManageGroups);
}
private async Task VerifyGroupAccessWasAddedAsync(
OrganizationUser organizationUser, IEnumerable<Group> groups)
{
var groupRepository = _factory.GetService<IGroupRepository>();
var userGroups = await groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
Assert.All(groups, group => Assert.Contains(group.Id, userGroups));
}
private async Task VerifyCollectionAccessWasUpdatedCorrectlyAsync(
OrganizationUser organizationUser, Guid sharedCollectionId, Guid defaultCollectionId)
{
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var (_, collectionAccess) = await organizationUserRepository.GetByIdWithCollectionsAsync(organizationUser.Id);
var collectionIds = collectionAccess.Select(c => c.Id).ToHashSet();
Assert.Contains(defaultCollectionId, collectionIds);
Assert.Contains(sharedCollectionId, collectionIds);
var newCollectionAccess = collectionAccess.First(c => c.Id == sharedCollectionId);
Assert.True(newCollectionAccess.ReadOnly);
Assert.False(newCollectionAccess.HidePasswords);
Assert.False(newCollectionAccess.Manage);
}
private async Task<List<OrganizationUser>> CreateAcceptedUsersAsync(
IEnumerable<(string email, OrganizationUserType userType)> newUsers)
{
var acceptedUsers = new List<OrganizationUser>();
foreach (var (email, userType) in newUsers)
{
await _factory.LoginWithNewAccount(email);
var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(
_factory, _organization.Id, email,
userType, userStatusType: OrganizationUserStatusType.Accepted);
acceptedUsers.Add(acceptedOrgUser);
}
return acceptedUsers;
}
private async Task VerifyDefaultCollectionCountAsync(OrganizationUser orgUser, int expectedCount)
{
var collectionRepository = _factory.GetService<ICollectionRepository>();
var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value);
Assert.Equal(expectedCount, collections.Count);
}
private async Task VerifyUserConfirmedAsync(OrganizationUser orgUser, string expectedKey)
{
await VerifyMultipleUsersConfirmedAsync(new List<(OrganizationUser orgUser, string key)> { (orgUser, expectedKey) });
}
private async Task VerifyMultipleUsersConfirmedAsync(List<(OrganizationUser orgUser, string key)> acceptedOrganizationUsers)
{
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
for (int i = 0; i < acceptedOrganizationUsers.Count; i++)
{
var confirmedUser = await orgUserRepository.GetByIdAsync(acceptedOrganizationUsers[i].orgUser.Id);
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
Assert.Equal(acceptedOrganizationUsers[i].key, confirmedUser.Key);
}
}
}