using System.Security.Claims; using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Models.Request; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Authorization; using NSubstitute; using Xunit; namespace Bit.Api.Test.AdminConsole.Controllers; [ControllerCustomize(typeof(OrganizationUsersController))] [SutProviderCustomize] public class OrganizationUserControllerPutTests { [Theory] [BitAutoData] public async Task Put_Success(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); // Authorize all changes for basic happy path test sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) .Returns(AuthorizationResult.Success()); // Save these for later - organizationUser object will be mutated var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => ou.Type == model.Type && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.AccessSecretsManager == model.AccessSecretsManager && ou.Id == orgUserId && ou.Email == orgUserEmail), savingUserId, Arg.Is>(cas => cas.All(c => model.Collections.Any(m => m.Id == c.Id))), model.Groups); } [Theory] [BitAutoData] public async Task Put_NoAdminAccess_CannotAddSelfToCollections(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { // Updating self organizationUser.UserId = savingUserId; organizationAbility.AllowAdminAccessToAllCollectionItems = false; Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model)); Assert.Contains("You cannot add yourself to a collection.", exception.Message); } [Theory] [BitAutoData] public async Task Put_NoAdminAccess_CannotAddSelfToGroups(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { // Updating self organizationUser.UserId = savingUserId; organizationAbility.AllowAdminAccessToAllCollectionItems = false; Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); // Not changing any collection access model.Collections = new List(); var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => ou.Type == model.Type && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.AccessSecretsManager == model.AccessSecretsManager && ou.Id == orgUserId && ou.Email == orgUserEmail), savingUserId, Arg.Is>(cas => cas.All(c => model.Collections.Any(m => m.Id == c.Id))), // Main assertion: groups are not updated (are null) null); } [Theory] [BitAutoData] public async Task Put_WithAdminAccess_CanAddSelfToGroups(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { // Updating self organizationUser.UserId = savingUserId; organizationAbility.AllowAdminAccessToAllCollectionItems = true; Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); // Not changing any collection access model.Collections = new List(); var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => ou.Type == model.Type && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.AccessSecretsManager == model.AccessSecretsManager && ou.Id == orgUserId && ou.Email == orgUserEmail), savingUserId, Arg.Is>(cas => cas.All(c => model.Collections.Any(m => m.Id == c.Id))), model.Groups); } [Theory] [BitAutoData] public async Task Put_UpdateCollections_DoesNotOverwriteUnauthorizedCollections(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { var editedCollectionId = CoreHelpers.GenerateComb(); var readonlyCollectionId1 = CoreHelpers.GenerateComb(); var readonlyCollectionId2 = CoreHelpers.GenerateComb(); var currentCollectionAccess = new List { new() { Id = editedCollectionId, HidePasswords = true, Manage = false, ReadOnly = true }, new() { Id = readonlyCollectionId1, HidePasswords = false, Manage = true, ReadOnly = false }, new() { Id = readonlyCollectionId2, HidePasswords = false, Manage = false, ReadOnly = false }, }; Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess); // User is upgrading editedCollectionId to manage model.Collections = new List { new() { Id = editedCollectionId, HidePasswords = false, Manage = true, ReadOnly = false } }; // Save these for later - organizationUser object will be mutated var orgUserId = organizationUser.Id; var orgUserEmail = organizationUser.Email; // Authorize the editedCollection sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Is(c => c.Id == editedCollectionId), Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) .Returns(AuthorizationResult.Success()); // Do not authorize the readonly collections sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Is(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2), Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) .Returns(AuthorizationResult.Failed()); await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model); // Expect all collection access (modified and unmodified) to be saved await sutProvider.GetDependency().Received(1).UpdateUserAsync(Arg.Is(ou => ou.Type == model.Type && ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) && ou.AccessSecretsManager == model.AccessSecretsManager && ou.Id == orgUserId && ou.Email == orgUserEmail), savingUserId, Arg.Is>(cas => cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) && cas.First(c => c.Id == editedCollectionId).Manage == true && cas.First(c => c.Id == editedCollectionId).ReadOnly == false && cas.First(c => c.Id == editedCollectionId).HidePasswords == false), model.Groups); } [Theory] [BitAutoData] public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotUpdateCollections(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { // Target user is currently assigned to the POSTed collections Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: model.Collections.Select(cas => cas.ToSelectionReadOnly()).ToList()); var postedCollectionIds = model.Collections.Select(c => c.Id).ToHashSet(); // But the saving user does not have permission to update them sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Is(c => postedCollectionIds.Contains(c.Id)), Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) .Returns(AuthorizationResult.Failed()); await Assert.ThrowsAsync(() => sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model)); } [Theory] [BitAutoData] public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotAddCollections(OrganizationUserUpdateRequestModel model, OrganizationUser organizationUser, OrganizationAbility organizationAbility, SutProvider sutProvider, Guid savingUserId) { // The target user is not currently assigned to any collections, so we're granting access for the first time Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []); var postedCollectionIds = model.Collections.Select(c => c.Id).ToHashSet(); // But the saving user does not have permission to assign access to the collections sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Is(c => postedCollectionIds.Contains(c.Id)), Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess))) .Returns(AuthorizationResult.Failed()); await Assert.ThrowsAsync(() => sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model)); } private void Put_Setup(SutProvider sutProvider, OrganizationAbility organizationAbility, OrganizationUser organizationUser, Guid savingUserId, List currentCollectionAccess) { var orgId = organizationAbility.Id = organizationUser.OrganizationId; sutProvider.GetDependency().ManageUsers(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(organizationUser.Id) .Returns(organizationUser); sutProvider.GetDependency().GetOrganizationAbilityAsync(orgId) .Returns(organizationAbility); sutProvider.GetDependency().GetProperUserId(Arg.Any()).Returns(savingUserId); // OrganizationUserRepository: return the user with current collection access sutProvider.GetDependency() .GetByIdWithCollectionsAsync(organizationUser.Id) .Returns(new Tuple>(organizationUser, currentCollectionAccess ?? [])); // Collection repository: return mock Collection objects for any ids passed in sutProvider.GetDependency() .GetManyByManyIdsAsync(Arg.Any>()) .Returns(callInfo => callInfo.Arg>().Select(guid => new Collection { Id = guid }).ToList()); } }