1
0
mirror of https://github.com/bitwarden/server synced 2025-12-12 14:23:38 +00:00
Files
server/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
Rui Tomé 9d05105dc0 [PM-23981] Fix DefaultUserCollection filtering in organization user updates (#6161)
* Refactor UpdateOrganizationUserCommand to validate and filter out DefaultUserCollections during user updates.

* Enhance UpdateOrganizationUserCommandTests to filter out DefaultUserCollections during user updates, ensuring only shared collections are processed. Updated test logic to reflect new filtering behavior.

* Add integration test for updating organization user with existing default collection. The test verifies successful updates to user permissions, group access, and collection access, ensuring correct handling of shared and default collections.

* Refactor UpdateOrganizationUserCommand to separate the collection validation and DefaultUserCollection filtering

* Refactored integration test setup/assertion for clarity
2025-08-07 11:12:45 +01:00

349 lines
14 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" })).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 VerifyDefaultCollectionCreatedAsync(acceptedOrgUser);
}
[Fact]
public async Task BulkConfirm_WithValidUsers_ReturnsSuccess()
{
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);
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 VerifyMultipleUsersHaveDefaultCollectionsAsync(acceptedUsers);
}
[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> emails)
{
var acceptedUsers = new List<OrganizationUser>();
foreach (var email in emails)
{
await _factory.LoginWithNewAccount(email);
var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email,
OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);
acceptedUsers.Add(acceptedOrgUser);
}
return acceptedUsers;
}
private async Task VerifyDefaultCollectionCreatedAsync(OrganizationUser orgUser)
{
var collectionRepository = _factory.GetService<ICollectionRepository>();
var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value);
Assert.Single(collections);
Assert.Equal(_mockEncryptedString, collections.First().Name);
}
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);
}
}
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);
}
}
}