1
0
mirror of https://github.com/bitwarden/server synced 2026-01-07 19:13:50 +00:00

Merge branch 'master' into flexible-collections/deprecate-custom-collection-perm

# Conflicts:
#	src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
This commit is contained in:
Rui Tome
2023-11-02 15:20:58 +00:00
31 changed files with 2172 additions and 301 deletions

View File

@@ -2,6 +2,7 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
@@ -27,7 +28,7 @@ public class OrganizationUsersControllerTests
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IOrganizationService>().Received(1).AcceptUserAsync(orgId, user, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserByOrgIdAsync(orgId, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
@@ -41,7 +42,7 @@ public class OrganizationUsersControllerTests
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IOrganizationService>().Received(0).AcceptUserAsync(orgId, user, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0).AcceptOrgUserByOrgIdAsync(orgId, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
@@ -63,8 +64,8 @@ public class OrganizationUsersControllerTests
await sutProvider.Sut.Accept(orgId, orgUserId, model);
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.AcceptUserAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()
.UpdateUserResetPasswordEnrollmentAsync(default, default, default, default);
}
@@ -85,8 +86,8 @@ public class OrganizationUsersControllerTests
await sutProvider.Sut.Accept(orgId, orgUserId, model);
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.AcceptUserAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
}

View File

@@ -4,6 +4,7 @@ using Bit.Api.Controllers;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -14,6 +15,7 @@ using Bit.Core.Settings;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
@@ -37,6 +39,7 @@ public class AccountsControllerTests : IDisposable
private readonly IProviderUserRepository _providerUserRepository;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
public AccountsControllerTests()
{
@@ -53,6 +56,8 @@ public class AccountsControllerTests : IDisposable
_sendService = Substitute.For<ISendService>();
_captchaValidationService = Substitute.For<ICaptchaValidationService>();
_policyService = Substitute.For<IPolicyService>();
_setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();
_sut = new AccountsController(
_globalSettings,
_cipherRepository,
@@ -66,7 +71,8 @@ public class AccountsControllerTests : IDisposable
_sendRepository,
_sendService,
_captchaValidationService,
_policyService
_policyService,
_setInitialMasterPasswordCommand
);
}
@@ -381,6 +387,124 @@ public class AccountsControllerTests : IDisposable
);
}
[Theory]
[BitAutoData(true, false)] // User has PublicKey and PrivateKey, and Keys in request are NOT null
[BitAutoData(true, true)] // User has PublicKey and PrivateKey, and Keys in request are null
[BitAutoData(false, false)] // User has neither PublicKey nor PrivateKey, and Keys in request are NOT null
[BitAutoData(false, true)] // User has neither PublicKey nor PrivateKey, and Keys in request are null
public async Task PostSetPasswordAsync_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(
bool hasExistingKeys,
bool shouldSetKeysToNull,
User user,
SetPasswordRequestModel setPasswordRequestModel)
{
// Arrange
const string existingPublicKey = "existingPublicKey";
const string existingEncryptedPrivateKey = "existingEncryptedPrivateKey";
const string newPublicKey = "newPublicKey";
const string newEncryptedPrivateKey = "newEncryptedPrivateKey";
if (hasExistingKeys)
{
user.PublicKey = existingPublicKey;
user.PrivateKey = existingEncryptedPrivateKey;
}
else
{
user.PublicKey = null;
user.PrivateKey = null;
}
if (shouldSetKeysToNull)
{
setPasswordRequestModel.Keys = null;
}
else
{
setPasswordRequestModel.Keys = new KeysRequestModel()
{
PublicKey = newPublicKey,
EncryptedPrivateKey = newEncryptedPrivateKey
};
}
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
user,
setPasswordRequestModel.MasterPasswordHash,
setPasswordRequestModel.Key,
setPasswordRequestModel.OrgIdentifier)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
await _sut.PostSetPasswordAsync(setPasswordRequestModel);
// Assert
await _setInitialMasterPasswordCommand.Received(1)
.SetInitialMasterPasswordAsync(
Arg.Is<User>(u => u == user),
Arg.Is<string>(s => s == setPasswordRequestModel.MasterPasswordHash),
Arg.Is<string>(s => s == setPasswordRequestModel.Key),
Arg.Is<string>(s => s == setPasswordRequestModel.OrgIdentifier));
// Additional Assertions for User object modifications
Assert.Equal(setPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);
Assert.Equal(setPasswordRequestModel.Kdf, user.Kdf);
Assert.Equal(setPasswordRequestModel.KdfIterations, user.KdfIterations);
Assert.Equal(setPasswordRequestModel.KdfMemory, user.KdfMemory);
Assert.Equal(setPasswordRequestModel.KdfParallelism, user.KdfParallelism);
Assert.Equal(setPasswordRequestModel.Key, user.Key);
if (hasExistingKeys)
{
// User Keys should not be modified
Assert.Equal(existingPublicKey, user.PublicKey);
Assert.Equal(existingEncryptedPrivateKey, user.PrivateKey);
}
else if (!shouldSetKeysToNull)
{
// User had no keys so they should be set to the request model's keys
Assert.Equal(setPasswordRequestModel.Keys.PublicKey, user.PublicKey);
Assert.Equal(setPasswordRequestModel.Keys.EncryptedPrivateKey, user.PrivateKey);
}
else
{
// User had no keys and the request model's keys were null, so they should be set to null
Assert.Null(user.PublicKey);
Assert.Null(user.PrivateKey);
}
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(
SetPasswordRequestModel setPasswordRequestModel)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setPasswordRequestModel));
}
[Theory]
[BitAutoData]
public async Task PostSetPasswordAsync_WhenSettingPasswordFails_ShouldThrowBadRequestException(
User user,
SetPasswordRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Some Error" })));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into
// something greater in order to share common test steps with

View File

@@ -0,0 +1,133 @@
using System.Security.Claims;
using System.Text.Json;
using Bit.Api.AdminConsole.Controllers;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.Controllers;
// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.
[ControllerCustomize(typeof(PoliciesController))]
[SutProviderCustomize]
public class PoliciesControllerTests
{
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_WhenCalled_ReturnsMasterPasswordPolicy(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser,
Policy policy, MasterPasswordPolicyData mpPolicyData)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)userId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(orgId, userId)
.Returns(orgUser);
policy.Type = PolicyType.MasterPassword;
policy.Enabled = true;
// data should be a JSON serialized version of the mpPolicyData object
policy.Data = JsonSerializer.Serialize(mpPolicyData);
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword)
.Returns(policy);
// Act
var result = await sutProvider.Sut.GetMasterPasswordPolicy(orgId);
// Assert
Assert.NotNull(result);
Assert.Equal(policy.Id, result.Id);
Assert.Equal(policy.Type, result.Type);
Assert.Equal(policy.Enabled, result.Enabled);
// Assert that the data is deserialized correctly into a Dictionary<string, object>
// for all MasterPasswordPolicyData properties
Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["MinComplexity"]).GetInt32());
Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["MinLength"]).GetInt32());
Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["RequireLower"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["RequireUpper"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["RequireNumbers"]).GetBoolean());
Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["RequireSpecial"]).GetBoolean());
Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["EnforceOnLogin"]).GetBoolean());
}
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_OrgUserIsNull_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)userId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(orgId, userId)
.Returns((OrganizationUser)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_PolicyIsNull_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)userId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(orgId, userId)
.Returns(orgUser);
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword)
.Returns((Policy)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
[Theory]
[BitAutoData]
public async Task GetMasterPasswordPolicy_PolicyNotEnabled_ThrowsNotFoundException(
SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser, Policy policy)
{
// Arrange
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns((Guid?)userId);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(orgId, userId)
.Returns(orgUser);
policy.Enabled = false; // Ensuring the policy is not enabled
sutProvider.GetDependency<IPolicyRepository>()
.GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword)
.Returns(policy);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
}

View File

@@ -0,0 +1,55 @@
using Bit.Core.Tokens;
namespace Bit.Test.Common.Fakes;
/// <summary>
/// Used to fake the IDataProtectorTokenFactory for testing purposes.
/// Generalized for use with all Tokenables.
/// </summary>
public class FakeDataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where T : Tokenable, new()
{
// Instead of real encryption, use a simple Dictionary to emulate protection/unprotection
private readonly Dictionary<string, T> _tokenDatabase = new Dictionary<string, T>();
public string Protect(T data)
{
// Generate a simple token representation
var token = Guid.NewGuid().ToString();
// Store the data against the token
_tokenDatabase[token] = data;
return token;
}
public T Unprotect(string token)
{
// If the token exists in the dictionary, return the corresponding data
if (_tokenDatabase.TryGetValue(token, out var data))
{
return data;
}
// If the token doesn't exist, throw an exception similar to a decryption failure.
throw new Exception("Failed to unprotect token.");
}
public bool TryUnprotect(string token, out T data)
{
try
{
data = Unprotect(token);
return true;
}
catch
{
data = default;
return false;
}
}
public bool TokenValid(string token)
{
return _tokenDatabase.ContainsKey(token);
}
}

View File

@@ -0,0 +1,264 @@
using AutoFixture.Xunit2;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Xunit;
namespace Bit.Core.Test.Auth.Models.Business.Tokenables;
// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.
public class OrgUserInviteTokenableTests
{
// Allow a small tolerance for possible execution delays or clock precision.
private readonly TimeSpan _timeTolerance = TimeSpan.FromMilliseconds(10);
/// <summary>
/// Tests that the default constructor sets the expiration date to the expected duration.
/// </summary>
[Fact]
public void Constructor_DefaultInitialization_ExpirationSetToExpectedDuration()
{
var token = new OrgUserInviteTokenable();
var expectedExpiration = DateTime.UtcNow + OrgUserInviteTokenable.GetTokenLifetime();
Assert.True(TimesAreCloseEnough(expectedExpiration, token.ExpirationDate, _timeTolerance));
}
/// <summary>
/// Tests that the constructor sets the properties correctly from a valid OrganizationUser object.
/// </summary>
[Theory, AutoData]
public void Constructor_ValidOrgUser_PropertiesSetFromOrgUser(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser);
Assert.Equal(orgUser.Id, token.OrgUserId);
Assert.Equal(orgUser.Email, token.OrgUserEmail);
}
/// <summary>
/// Tests that the constructor sets the properties to default values when given a null OrganizationUser object.
/// </summary>
[Fact]
public void Constructor_NullOrgUser_PropertiesSetToDefault()
{
var token = new OrgUserInviteTokenable(null);
Assert.Equal(default, token.OrgUserId);
Assert.Equal(default, token.OrgUserEmail);
}
/// <summary>
/// Tests that a custom expiration date is preserved after token initialization.
/// </summary>
[Fact]
public void Constructor_CustomExpirationDate_ExpirationMatchesProvidedValue()
{
var customExpiration = DateTime.UtcNow.AddHours(3);
var token = new OrgUserInviteTokenable
{
ExpirationDate = customExpiration
};
Assert.True(TimesAreCloseEnough(customExpiration, token.ExpirationDate, _timeTolerance));
}
/// <summary>
/// Tests the validity of a token initialized with a null org user.
/// </summary>
[Fact]
public void Valid_NullOrgUser_ReturnsFalse()
{
var token = new OrgUserInviteTokenable(null);
Assert.False(token.Valid);
}
/// <summary>
/// Tests the validity of a token with a non-matching identifier.
/// </summary>
[Fact]
public void Valid_WrongIdentifier_ReturnsFalse()
{
var token = new OrgUserInviteTokenable
{
Identifier = "IncorrectIdentifier"
};
Assert.False(token.Valid);
}
/// <summary>
/// Tests the validity of the token when the OrgUserId is set to default.
/// </summary>
[Fact]
public void Valid_DefaultOrgUserId_ReturnsFalse()
{
var token = new OrgUserInviteTokenable
{
OrgUserId = default // Guid.Empty
};
Assert.False(token.Valid);
}
/// <summary>
/// Tests the validity of the token when the OrgUserEmail is null or empty.
/// </summary>
[Theory]
[InlineData(null)]
[InlineData("")]
public void Valid_NullOrEmptyOrgUserEmail_ReturnsFalse(string email)
{
var token = new OrgUserInviteTokenable
{
OrgUserEmail = email
};
Assert.False(token.Valid);
}
/// <summary>
/// Tests the validity of the token when the token is expired.
/// </summary>
[Fact]
public void Valid_ExpiredToken_ReturnsFalse()
{
var expiredDate = DateTime.UtcNow.AddHours(-3);
var token = new OrgUserInviteTokenable
{
ExpirationDate = expiredDate
};
Assert.False(token.Valid);
}
/// <summary>
/// Tests the TokenIsValid method when given a null OrganizationUser object.
/// </summary>
[Fact]
public void TokenIsValid_NullOrgUser_ReturnsFalse()
{
var token = new OrgUserInviteTokenable(null);
Assert.False(token.TokenIsValid(null));
}
/// <summary>
/// Tests the TokenIsValid method when the OrgUserId does not match.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_WrongUserId_ReturnsFalse(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser)
{
OrgUserId = Guid.NewGuid() // Force a different ID
};
Assert.False(token.TokenIsValid(orgUser));
}
/// <summary>
/// Tests the TokenIsValid method when the OrgUserEmail does not match.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_WrongEmail_ReturnsFalse(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser)
{
OrgUserEmail = "wrongemail@example.com" // Force a different email
};
Assert.False(token.TokenIsValid(orgUser));
}
/// <summary>
/// Tests the TokenIsValid method when both OrgUserId and OrgUserEmail match.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_MatchingUserIdAndEmail_ReturnsTrue(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser);
Assert.True(token.TokenIsValid(orgUser));
}
/// <summary>
/// Tests the TokenIsValid method to ensure email comparison is case-insensitive.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_EmailCaseInsensitiveComparison_ReturnsTrue(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser);
// Modify the orgUser's email case
orgUser.Email = orgUser.Email.ToUpperInvariant();
Assert.True(token.TokenIsValid(orgUser));
}
/// <summary>
/// Tests the TokenIsValid method when the token is expired.
/// Should return true as TokenIsValid only validates token data -- not token expiration.
/// </summary>
[Theory, AutoData]
public void TokenIsValid_ExpiredToken_ReturnsTrue(OrganizationUser orgUser)
{
var expiredDate = DateTime.UtcNow.AddHours(-3);
var token = new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = expiredDate
};
Assert.True(token.TokenIsValid(orgUser));
}
/// <summary>
/// Tests the deserialization of a token to ensure that the ExpirationDate is preserved.
/// </summary>
[Theory, AutoData]
public void FromToken_SerializedToken_PreservesExpirationDate(OrganizationUser orgUser)
{
// Arbitrary time for testing
var expectedDateTime = DateTime.UtcNow.AddHours(-3);
var token = new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = expectedDateTime
};
var result = Tokenable.FromToken<OrgUserInviteTokenable>(token.ToToken());
Assert.True(TimesAreCloseEnough(expectedDateTime, result.ExpirationDate, _timeTolerance));
}
/// <summary>
/// Tests the deserialization of a token to ensure that the OrgUserId property is preserved.
/// </summary>
[Theory, AutoData]
public void FromToken_SerializedToken_PreservesOrgUserId(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser);
var result = Tokenable.FromToken<OrgUserInviteTokenable>(token.ToToken());
Assert.Equal(orgUser.Id, result.OrgUserId);
}
/// <summary>
/// Tests the deserialization of a token to ensure that the OrgUserEmail property is preserved.
/// </summary>
[Theory, AutoData]
public void FromToken_SerializedToken_PreservesOrgUserEmail(OrganizationUser orgUser)
{
var token = new OrgUserInviteTokenable(orgUser);
var result = Tokenable.FromToken<OrgUserInviteTokenable>(token.ToToken());
Assert.Equal(orgUser.Email, result.OrgUserEmail);
}
private bool TimesAreCloseEnough(DateTime time1, DateTime time2, TimeSpan tolerance)
{
return (time1 - time2).Duration() < tolerance;
}
}

View File

@@ -0,0 +1,193 @@
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;
[SutProviderCustomize]
public class SetInitialMasterPasswordCommandTests
{
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_Success(SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, string masterPassword, string key, string orgIdentifier,
Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_UserIsNull_ThrowsArgumentNullException(SutProvider<SetInitialMasterPasswordCommand> sutProvider, string masterPassword, string key, string orgIdentifier)
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(null, masterPassword, key, orgIdentifier));
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_AlreadyHasPassword_ReturnsFalse(SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key, string orgIdentifier)
{
// Arrange
user.MasterPassword = "ExistingPassword";
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.False(result.Succeeded);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_NullOrgSsoIdentifier_ThrowsBadRequestException(
SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key)
{
// Arrange
user.MasterPassword = null;
string orgSsoIdentifier = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgSsoIdentifier));
Assert.Equal("Organization SSO Identifier required.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvalidOrganization_Throws(SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key, string orgIdentifier)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier));
Assert.Equal("Organization invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_UserNotFoundInOrganization_Throws(SutProvider<SetInitialMasterPasswordCommand> sutProvider, User user, string masterPassword, string key, Organization org)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(Arg.Any<string>())
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.ReturnsNull();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, org.Identifier));
Assert.Equal("User not found within organization.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_ConfirmedOrgUser_DoesNotCallAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
orgUser.Status = OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().DidNotReceive().AcceptOrgUserAsync(Arg.Any<OrganizationUser>(), Arg.Any<User>(), Arg.Any<IUserService>());
}
[Theory]
[BitAutoData]
public async Task SetInitialMasterPassword_InvitedOrgUser_CallsAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommand> sutProvider,
User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)
{
// Arrange
user.MasterPassword = null;
sutProvider.GetDependency<IUserService>()
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)
.Returns(IdentityResult.Success);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgIdentifier)
.Returns(org);
orgUser.Status = OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);
// Assert
Assert.Equal(IdentityResult.Success, result);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserAsync(orgUser, user, sutProvider.GetDependency<IUserService>());
}
}

View File

@@ -0,0 +1,664 @@
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using Microsoft.AspNetCore.DataProtection;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.
[SutProviderCustomize]
public class AcceptOrgUserCommandTests
{
private readonly IUserService _userService = Substitute.For<IUserService>();
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
// Base AcceptOrgUserAsync method tests ----------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task AcceptOrgUser_InvitedUserToSingleOrg_AcceptsOrgUser(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// Act
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
// Assert
// Verify returned org user details
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
// Verify org repository called with updated orgUser
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(
Arg.Is<OrganizationUser>(ou => ou.Id == orgUser.Id && ou.Status == OrganizationUserStatusType.Accepted));
// Verify emails sent to admin
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationAcceptedEmailAsync(
Arg.Is<Organization>(o => o.Id == org.Id),
Arg.Is<string>(e => e == user.Email),
Arg.Is<IEnumerable<string>>(a => a.Contains(adminUserDetails.Email))
);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUser_OrgUserStatusIsRevoked_ReturnsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Common setup
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// Revoke user status
orgUser.Status = OrganizationUserStatusType.Revoked;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
Assert.Equal("Your organization access has been revoked.", exception.Message);
}
[Theory]
[BitAutoData(OrganizationUserStatusType.Accepted)]
[BitAutoData(OrganizationUserStatusType.Confirmed)]
public async Task AcceptOrgUser_OrgUserStatusIsNotInvited_ThrowsBadRequest(
OrganizationUserStatusType orgUserStatus,
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// Set status to something other than invited
orgUser.Status = orgUserStatus;
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
Assert.Equal("Already accepted.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUser_UserJoiningOrgWithSingleOrgPolicyWhileInAnotherOrg_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// Make user part of another org
var otherOrgUser = new OrganizationUser { UserId = user.Id, OrganizationId = Guid.NewGuid() }; // random org ID
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(Task.FromResult<ICollection<OrganizationUser>>(new List<OrganizationUser> { otherOrgUser }));
// Make organization they are trying to join have the single org policy
var singleOrgPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId };
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg, OrganizationUserStatusType.Invited)
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>(
new List<OrganizationUserPolicyDetails> { singleOrgPolicy }));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
Assert.Equal("You may not join this organization until you leave or remove all other organizations.",
exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// Mock that user is part of an org that has the single org policy
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
Assert.Equal(
"You cannot join this organization because you are a member of another organization which forbids it",
exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// User doesn't have 2FA enabled
_userService.TwoFactorIsEnabledAsync(user).Returns(false);
// Organization they are trying to join requires 2FA
var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId };
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication,
OrganizationUserStatusType.Invited)
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>(
new List<OrganizationUserPolicyDetails> { twoFactorPolicy }));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
Assert.Equal("You cannot join this organization until you enable two-step login on your user account.",
exception.Message);
}
// AcceptOrgUserByOrgIdAsync tests --------------------------------------------------------------------------------
[Theory]
[EphemeralDataProtectionAutoData]
public async Task AcceptOrgUserByToken_OldToken_AcceptsUserAndVerifiesEmail(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser);
var oldToken = CreateOldToken(sutProvider, orgUser);
// Act
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, oldToken, _userService);
// Assert
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
// Verify user email verified logic
Assert.True(user.EmailVerified);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(
Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true));
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order
// to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser);
// Must come after common mocks as they mutate the org user.
// Mock tokenable factory to return a token that expires in 5 days
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
});
var newToken = CreateNewToken(orgUser);
// Act
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService);
// Assert
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
// Verify user email verified logic
Assert.True(user.EmailVerified);
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(
Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true));
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByToken_NullOrgUser_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Guid orgUserId)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUserId).Returns((OrganizationUser)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUserId, user, "token", _userService));
Assert.Equal("User invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByToken_GenericInvalidToken_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, OrganizationUser orgUser)
{
// Arrange
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUser.Id)
.Returns(Task.FromResult(orgUser));
var invalidToken = "invalidToken";
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, invalidToken, _userService));
Assert.Equal("Invalid token.", exception.Message);
}
[Theory]
[EphemeralDataProtectionAutoData]
public async Task AcceptOrgUserByToken_ExpiredOldToken_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser);
// As the old token simply set a timestamp which was later compared against the
// OrganizationInviteExpirationHours global setting to determine if it was expired or not,
// we can simply set the expiration to 24 hours ago to simulate an expired token.
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(-24);
var oldToken = CreateOldToken(sutProvider, orgUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, oldToken, _userService));
Assert.Equal("Invalid token.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByToken_ExpiredNewToken_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, OrganizationUser orgUser)
{
// Arrange
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order
// to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUser.Id)
.Returns(Task.FromResult(orgUser));
// Must come after common mocks as they mutate the org user.
// Mock tokenable factory to return a token that expired yesterday
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(-1))
});
var newToken = CreateNewToken(orgUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService));
Assert.Equal("Invalid token.", exception.Message);
}
[Theory]
[BitAutoData(OrganizationUserStatusType.Accepted,
"Invitation already accepted. You will receive an email when your organization membership is confirmed.")]
[BitAutoData(OrganizationUserStatusType.Confirmed,
"You are already part of this organization.")]
public async Task AcceptOrgUserByToken_UserAlreadyInOrg_ThrowsBadRequest(
OrganizationUserStatusType statusType,
string expectedErrorMessage,
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, OrganizationUser orgUser)
{
// Arrange
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order
// to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUser.Id)
.Returns(Task.FromResult(orgUser));
// Indicate that a user with the given email already exists in the organization
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOrganizationAsync(orgUser.OrganizationId, user.Email, true)
.Returns(1);
orgUser.Status = statusType;
// Must come after common mocks as they mutate the org user.
// Mock tokenable factory to return valid, new token that expires in 5 days
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
});
var newToken = CreateNewToken(orgUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService));
Assert.Equal(expectedErrorMessage, exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByToken_EmailMismatch_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, OrganizationUser orgUser)
{
// Arrange
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order
// to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser);
// Modify the orgUser's email to be different from the user's email to simulate the mismatch
orgUser.Email = "mismatchedEmail@example.com";
// Must come after common mocks as they mutate the org user.
// Mock tokenable factory to return a token that expires in 5 days
_orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
});
var newToken = CreateNewToken(orgUser);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService));
Assert.Equal("User email does not match invite.", exception.Message);
}
// AcceptOrgUserByOrgSsoIdAsync -----------------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByOrgSsoIdAsync_ValidData_AcceptsOrgUser(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(org.Identifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(org.Identifier, user, _userService);
// Assert
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByOrgSsoIdAsync_InvalidOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,
string orgSsoIdentifier, User user)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(orgSsoIdentifier)
.Returns((Organization)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(orgSsoIdentifier, user, _userService));
Assert.Equal("Organization invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByOrgSsoIdAsync_UserNotInOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,
Organization org, User user)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdentifierAsync(org.Identifier)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns((OrganizationUser)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(org.Identifier, user, _userService));
Assert.Equal("User not found within organization.", exception.Message);
}
// AcceptOrgUserByOrgIdAsync ---------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByOrgId_ValidData_AcceptsOrgUser(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(org.Id)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns(orgUser);
// Act
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByOrgIdAsync(org.Id, user, _userService);
// Assert
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByOrgId_InvalidOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,
Guid orgId, User user)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(orgId)
.Returns((Organization)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByOrgIdAsync(orgId, user, _userService));
Assert.Equal("Organization invalid.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserByOrgId_UserNotInOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,
Organization org, User user)
{
// Arrange
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(org.Id)
.Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(org.Id, user.Id)
.Returns((OrganizationUser)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.AcceptOrgUserByOrgIdAsync(org.Id, user, _userService));
Assert.Equal("User not found within organization.", exception.Message);
}
// Private helpers -------------------------------------------------------------------------------------------------
/// <summary>
/// Asserts that the given org user is in the expected state after a successful AcceptOrgUserAsync call.
/// For use in happy path tests.
/// </summary>
private void AssertValidAcceptedOrgUser(OrganizationUser resultOrgUser, OrganizationUser expectedOrgUser, User user)
{
Assert.NotNull(resultOrgUser);
Assert.Equal(OrganizationUserStatusType.Accepted, resultOrgUser.Status);
Assert.Equal(expectedOrgUser, resultOrgUser);
Assert.Equal(expectedOrgUser.Id, resultOrgUser.Id);
Assert.Null(resultOrgUser.Email);
Assert.Equal(user.Id, resultOrgUser.UserId);
}
private void SetupCommonAcceptOrgUserByTokenMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user, OrganizationUser orgUser)
{
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(24);
user.EmailVerified = false;
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUser.Id)
.Returns(Task.FromResult(orgUser));
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetCountByOrganizationAsync(orgUser.OrganizationId, user.Email, true)
.Returns(0);
}
/// <summary>
/// Sets up common mock behavior for the AcceptOrgUserAsync tests.
/// This method initializes:
/// - The invited user's email, status, type, and organization ID.
/// - Ensures the user is not part of any other organizations.
/// - Confirms the target organization doesn't have a single org policy.
/// - Ensures the user doesn't belong to an organization with a single org policy.
/// - Assumes the user doesn't have 2FA enabled and the organization doesn't require it.
/// - Provides mock data for an admin to validate email functionality.
/// - Returns the corresponding organization for the given org ID.
/// </summary>
private void SetupCommonAcceptOrgUserMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user,
Organization org,
OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
// Arrange
orgUser.Email = user.Email;
orgUser.Status = OrganizationUserStatusType.Invited;
orgUser.Type = OrganizationUserType.User;
orgUser.OrganizationId = org.Id;
// User is not part of any other orgs
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(
Task.FromResult<ICollection<OrganizationUser>>(new List<OrganizationUser>())
);
// Org they are trying to join does not have single org policy
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg, OrganizationUserStatusType.Invited)
.Returns(
Task.FromResult<ICollection<OrganizationUserPolicyDetails>>(
new List<OrganizationUserPolicyDetails>()
)
);
// User is not part of any organization that applies the single org policy
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
// User doesn't have 2FA enabled
_userService.TwoFactorIsEnabledAsync(user).Returns(false);
// Org does not require 2FA
sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited)
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>(
new List<OrganizationUserPolicyDetails>()));
// Provide at least 1 admin to test email functionality
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin)
.Returns(Task.FromResult<IEnumerable<OrganizationUserUserDetails>>(
new List<OrganizationUserUserDetails>() { adminUserDetails }
));
// Return org
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(org.Id)
.Returns(Task.FromResult(org));
}
private string CreateOldToken(SutProvider<AcceptOrgUserCommand> sutProvider,
OrganizationUser organizationUser)
{
var dataProtector = sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("OrganizationServiceDataProtector");
// Token matching the format used in OrganizationService.InviteUserAsync
var oldToken = dataProtector.Protect(
$"OrganizationUserInvite {organizationUser.Id} {organizationUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
return oldToken;
}
private string CreateNewToken(OrganizationUser orgUser)
{
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);
var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);
return protectedToken;
}
}

View File

@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Business;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
@@ -24,13 +25,14 @@ using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Core.Test.AutoFixture.PolicyFixtures;
using Bit.Core.Tokens;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Business;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection;
using Bit.Test.Common.Fakes;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
@@ -43,10 +45,16 @@ namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class OrganizationServiceTests
{
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task OrgImportCreateNewUsers(SutProvider<OrganizationService> sutProvider, Guid userId,
Organization org, List<OrganizationUserUserDetails> existingUsers, List<ImportedOrganizationUser> newUsers)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
org.UseDirectory = true;
org.Seats = 10;
newUsers.Add(new ImportedOrganizationUser
@@ -67,6 +75,16 @@ public class OrganizationServiceTests
.Returns(existingUsers.Select(u => new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.Owner, Id = u.Id }).ToList());
sutProvider.GetDependency<ICurrentContext>().ManageUsers(org.Id).Returns(true);
// Mock tokenable factory to return a token that expires in 5 days
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
.CreateToken(Arg.Any<OrganizationUser>())
.Returns(
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
}
);
await sutProvider.Sut.ImportAsync(org.Id, userId, null, newUsers, null, false);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
@@ -98,6 +116,10 @@ public class OrganizationServiceTests
Guid userId, Organization org, List<OrganizationUserUserDetails> existingUsers,
List<ImportedOrganizationUser> newUsers)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
org.UseDirectory = true;
org.Seats = newUsers.Count + existingUsers.Count + 1;
var reInvitedUser = existingUsers.First();
@@ -121,6 +143,16 @@ public class OrganizationServiceTests
var currentContext = sutProvider.GetDependency<ICurrentContext>();
currentContext.ManageUsers(org.Id).Returns(true);
// Mock tokenable factory to return a token that expires in 5 days
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
.CreateToken(Arg.Any<OrganizationUser>())
.Returns(
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
}
);
await sutProvider.Sut.ImportAsync(org.Id, userId, null, newUsers, null, false);
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()
@@ -350,6 +382,10 @@ public class OrganizationServiceTests
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
invite.Emails = invite.Emails.Append(invite.Emails.First());
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
@@ -359,6 +395,16 @@ public class OrganizationServiceTests
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
.Returns(new[] { owner });
// Mock tokenable factory to return a token that expires in 5 days
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
.CreateToken(Arg.Any<OrganizationUser>())
.Returns(
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
}
);
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
await sutProvider.GetDependency<IMailService>().Received(1)
@@ -587,6 +633,10 @@ public class OrganizationServiceTests
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<OrganizationService> sutProvider)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
new JsonSerializerOptions
{
@@ -622,6 +672,16 @@ public class OrganizationServiceTests
}
});
// Mock tokenable factory to return a token that expires in 5 days
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
.CreateToken(Arg.Any<OrganizationUser>())
.Returns(
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
}
);
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, invites);
await sutProvider.GetDependency<IMailService>().Received(1)
@@ -641,6 +701,10 @@ public class OrganizationServiceTests
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<OrganizationService> sutProvider)
{
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
sutProvider.Create();
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
new JsonSerializerOptions
{
@@ -656,6 +720,16 @@ public class OrganizationServiceTests
.Returns(new[] { owner });
currentContext.ManageUsers(organization.Id).Returns(true);
// Mock tokenable factory to return a token that expires in 5 days
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
.CreateToken(Arg.Any<OrganizationUser>())
.Returns(
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
{
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
}
);
await sutProvider.Sut.InviteUsersAsync(organization.Id, eventSystemUser, invites);
await sutProvider.GetDependency<IMailService>().Received(1)
@@ -1949,42 +2023,6 @@ public class OrganizationServiceTests
sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup);
}
[Theory]
[EphemeralDataProtectionAutoData]
public async Task AcceptUserAsync_Success([OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser,
User user, SutProvider<OrganizationService> sutProvider)
{
var token = SetupAcceptUserAsyncTest(sutProvider, user, organizationUser);
var userService = Substitute.For<IUserService>();
await sutProvider.Sut.AcceptUserAsync(organizationUser.Id, user, token, userService);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(
Arg.Is<OrganizationUser>(ou => ou.Id == organizationUser.Id && ou.Status == OrganizationUserStatusType.Accepted));
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(
Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true));
}
private string SetupAcceptUserAsyncTest(SutProvider<OrganizationService> sutProvider, User user,
OrganizationUser organizationUser)
{
user.Email = organizationUser.Email;
user.EmailVerified = false;
var dataProtector = sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("OrganizationServiceDataProtector");
// Token matching the format used in OrganizationService.InviteUserAsync
var token = dataProtector.Protect(
$"OrganizationUserInvite {organizationUser.Id} {organizationUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IGlobalSettings>().OrganizationInviteExpirationHours.Returns(24);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
return token;
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Owner,

View File

@@ -10,6 +10,7 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -18,6 +19,7 @@ using Bit.Core.Tools.Services;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Fakes;
using Bit.Test.Common.Helpers;
using Fido2NetLib;
using Microsoft.AspNetCore.DataProtection;
@@ -272,9 +274,10 @@ public class UserServiceTests
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IOrganizationService>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IWebAuthnCredentialRepository>(),
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginTokenable>>()
);