mirror of
https://github.com/bitwarden/server
synced 2025-12-12 22:33:45 +00:00
* 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.
365 lines
15 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|