mirror of
https://github.com/bitwarden/server
synced 2026-01-02 00:23:40 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
@@ -9,6 +9,9 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@@ -60,6 +63,7 @@ public class ProviderService : IProviderService
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||
@@ -69,7 +73,8 @@ public class ProviderService : IProviderService
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
|
||||
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
|
||||
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
@@ -90,6 +95,7 @@ public class ProviderService : IProviderService
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)
|
||||
@@ -117,6 +123,18 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Invalid owner.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerUserId);
|
||||
|
||||
if (organizationAutoConfirmPolicyRequirement
|
||||
.CannotCreateProvider())
|
||||
{
|
||||
throw new BadRequestException(new UserCannotJoinProvider().Message);
|
||||
}
|
||||
}
|
||||
|
||||
var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);
|
||||
provider.GatewayCustomerId = customer.Id;
|
||||
var subscription = await _providerBillingService.SetupSubscription(provider);
|
||||
@@ -249,6 +267,18 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("User email does not match invite.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
|
||||
|
||||
if (organizationAutoConfirmPolicyRequirement
|
||||
.CannotJoinProvider())
|
||||
{
|
||||
throw new BadRequestException(new UserCannotJoinProvider().Message);
|
||||
}
|
||||
}
|
||||
|
||||
providerUser.Status = ProviderUserStatusType.Accepted;
|
||||
providerUser.UserId = user.Id;
|
||||
providerUser.Email = null;
|
||||
@@ -294,6 +324,19 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Invalid user.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
|
||||
|
||||
if (organizationAutoConfirmPolicyRequirement
|
||||
.CannotJoinProvider())
|
||||
{
|
||||
result.Add(Tuple.Create(providerUser, new UserCannotJoinProvider().Message));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Key = keys[providerUser.Id];
|
||||
providerUser.Email = null;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
@@ -101,6 +106,57 @@ public class ProviderServiceTests
|
||||
.ReplaceAsync(Arg.Is<ProviderUser>(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteSetupAsync_WithAutoConfirmEnabled_ThrowsUserCannotJoinProviderError(User user, Provider provider,
|
||||
string key,
|
||||
TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
|
||||
[ProviderUser] ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.ProviderId = provider.Id;
|
||||
providerUser.UserId = user.Id;
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByIdAsync(user.Id).Returns(user);
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
|
||||
|
||||
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
|
||||
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
|
||||
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
|
||||
|
||||
var customer = new Customer { Id = "customer_id" };
|
||||
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
|
||||
|
||||
var subscription = new Subscription { Id = "subscription_id" };
|
||||
providerBillingService.SetupSubscription(provider).Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyDetails = new List<PolicyDetails> { new() { OrganizationId = Guid.NewGuid(), IsProvider = false } };
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
sutProvider.Create();
|
||||
|
||||
var token = protector.Protect(
|
||||
$"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod,
|
||||
billingAddress));
|
||||
|
||||
Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_ProviderIdIsInvalid_Throws(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
@@ -580,6 +636,132 @@ public class ProviderServiceTests
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_WithAutoConfirmEnabledAndPolicyExists_Throws(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetByIdAsync(providerUser.Id)
|
||||
.Returns(providerUser);
|
||||
|
||||
var protector = DataProtectionProvider
|
||||
.Create("ApplicationName")
|
||||
.CreateProtector("ProviderServiceDataProtector");
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new() { OrganizationId = Guid.NewGuid(), IsProvider = false }
|
||||
};
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token));
|
||||
|
||||
Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetByIdAsync(providerUser.Id)
|
||||
.Returns(providerUser);
|
||||
|
||||
var protector = DataProtectionProvider
|
||||
.Create("ApplicationName")
|
||||
.CreateProtector("ProviderServiceDataProtector");
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement([]);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
// Act
|
||||
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
|
||||
|
||||
// Assert
|
||||
Assert.Null(pu.Email);
|
||||
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_WithAutoConfirmDisabled_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,
|
||||
User user,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetByIdAsync(providerUser.Id)
|
||||
.Returns(providerUser);
|
||||
|
||||
var protector = DataProtectionProvider
|
||||
.Create("ApplicationName")
|
||||
.CreateProtector("ProviderServiceDataProtector");
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(protector);
|
||||
sutProvider.Create();
|
||||
|
||||
providerUser.Email = user.Email;
|
||||
var token = protector.Protect($"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);
|
||||
|
||||
// Assert
|
||||
Assert.Null(pu.Email);
|
||||
Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);
|
||||
Assert.Equal(user.Id, pu.UserId);
|
||||
|
||||
// Verify that policy check was never called when feature flag is disabled
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.DidNotReceive()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_NoValid(
|
||||
[ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu1,
|
||||
@@ -626,13 +808,131 @@ public class ProviderServiceTests
|
||||
Assert.Equal("Invalid user.", result[2].Item2);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmEnabledAndPolicyExists_ReturnsError(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
|
||||
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
pu1.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
var providerUsers = new[] { pu1 };
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyDetails = new List<PolicyDetails>
|
||||
{
|
||||
new() { OrganizationId = Guid.NewGuid(), IsProvider = false }
|
||||
};
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(new UserCannotJoinProvider().Message, result[0].Item2);
|
||||
|
||||
// Verify user was not confirmed
|
||||
await providerUserRepository.DidNotReceive().ReplaceAsync(Arg.Any<ProviderUser>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
|
||||
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
pu1.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
var providerUsers = new[] { pu1 };
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)
|
||||
.Returns(policyRequirement);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("", result[0].Item2);
|
||||
|
||||
// Verify user was confirmed
|
||||
await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>
|
||||
pu.Status == ProviderUserStatusType.Confirmed));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmDisabled_Success(
|
||||
[ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,
|
||||
Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
pu1.ProviderId = provider.Id;
|
||||
pu1.UserId = u1.Id;
|
||||
var providerUsers = new[] { pu1 };
|
||||
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(false);
|
||||
|
||||
var dict = providerUsers.ToDictionary(pu => pu.Id, _ => "key");
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("", result[0].Item2);
|
||||
|
||||
// Verify user was confirmed
|
||||
await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>
|
||||
pu.Status == ProviderUserStatusType.Confirmed));
|
||||
|
||||
// Verify that policy check was never called when feature flag is disabled
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.DidNotReceive()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveUserAsync_UserIdIsInvalid_Throws(ProviderUser providerUser,
|
||||
SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
providerUser.Id = default;
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveUserAsync(providerUser, default));
|
||||
providerUser.Id = Guid.Empty;
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.SaveUserAsync(providerUser, Guid.Empty));
|
||||
Assert.Equal("Invite the user first.", exception.Message);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -44,6 +45,7 @@ public class AccountsController : Controller
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public AccountsController(
|
||||
IOrganizationService organizationService,
|
||||
@@ -57,7 +59,8 @@ public class AccountsController : Controller
|
||||
IFeatureService featureService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
ITwoFactorEmailService twoFactorEmailService,
|
||||
IChangeKdfCommand changeKdfCommand
|
||||
IChangeKdfCommand changeKdfCommand,
|
||||
IUserRepository userRepository
|
||||
)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
@@ -72,6 +75,7 @@ public class AccountsController : Controller
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
_twoFactorEmailService = twoFactorEmailService;
|
||||
_changeKdfCommand = changeKdfCommand;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
|
||||
@@ -432,16 +436,36 @@ public class AccountsController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.ReturnErrorOnExistingKeypair))
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))
|
||||
{
|
||||
throw new BadRequestException("User has existing keypair");
|
||||
}
|
||||
throw new BadRequestException("User has existing keypair");
|
||||
}
|
||||
|
||||
if (model.AccountKeys != null)
|
||||
{
|
||||
var accountKeysData = model.AccountKeys.ToAccountKeysData();
|
||||
if (!accountKeysData.IsV2Encryption())
|
||||
{
|
||||
throw new BadRequestException("AccountKeys are only supported for V2 encryption.");
|
||||
}
|
||||
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, accountKeysData);
|
||||
return new KeysResponseModel(accountKeysData, user.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Todo: Drop this after a transition period. This will drop no-account-keys requests.
|
||||
// The V1 check in the other branch should persist
|
||||
// https://bitwarden.atlassian.net/browse/PM-27329
|
||||
await _userService.SaveUserAsync(model.ToUser(user));
|
||||
return new KeysResponseModel(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
user.PrivateKey,
|
||||
user.PublicKey
|
||||
)
|
||||
}, user.Key);
|
||||
}
|
||||
|
||||
await _userService.SaveUserAsync(model.ToUser(user));
|
||||
return new KeysResponseModel(user);
|
||||
}
|
||||
|
||||
[HttpGet("keys")]
|
||||
@@ -453,7 +477,8 @@ public class AccountsController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
return new KeysResponseModel(user);
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||
return new KeysResponseModel(accountKeys, user.Key);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[Route("phishing-domains")]
|
||||
public class PhishingDomainsController(IPhishingDomainRepository phishingDomainRepository, IFeatureService featureService) : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ICollection<string>>> GetPhishingDomainsAsync()
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync();
|
||||
return Ok(domains);
|
||||
}
|
||||
|
||||
[HttpGet("checksum")]
|
||||
public async Task<ActionResult<string>> GetChecksumAsync()
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var checksum = await phishingDomainRepository.GetCurrentChecksumAsync();
|
||||
return Ok(checksum);
|
||||
}
|
||||
}
|
||||
@@ -59,13 +59,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 * * * ?")
|
||||
.Build();
|
||||
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("UpdatePhishingDomainsTrigger")
|
||||
.StartNow()
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInHours(24)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
var updateOrgSubscriptionsTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("UpdateOrgSubscriptionsTrigger")
|
||||
.StartNow()
|
||||
@@ -81,7 +74,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger),
|
||||
new (typeof(OrganizationSubscriptionUpdateJob), updateOrgSubscriptionsTrigger),
|
||||
};
|
||||
|
||||
@@ -111,7 +103,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
services.AddTransient<ValidateUsersJob>();
|
||||
services.AddTransient<ValidateOrganizationsJob>();
|
||||
services.AddTransient<ValidateOrganizationDomainJob>();
|
||||
services.AddTransient<UpdatePhishingDomainsJob>();
|
||||
services.AddTransient<OrganizationSubscriptionUpdateJob>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Jobs;
|
||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Quartz;
|
||||
|
||||
namespace Bit.Api.Jobs;
|
||||
|
||||
public class UpdatePhishingDomainsJob : BaseJob
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IPhishingDomainRepository _phishingDomainRepository;
|
||||
private readonly ICloudPhishingDomainQuery _cloudPhishingDomainQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
public UpdatePhishingDomainsJob(
|
||||
GlobalSettings globalSettings,
|
||||
IPhishingDomainRepository phishingDomainRepository,
|
||||
ICloudPhishingDomainQuery cloudPhishingDomainQuery,
|
||||
IFeatureService featureService,
|
||||
ILogger<UpdatePhishingDomainsJob> logger)
|
||||
: base(logger)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_phishingDomainRepository = phishingDomainRepository;
|
||||
_cloudPhishingDomainQuery = cloudPhishingDomainQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Feature flag is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. No URL configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Cloud communication is disabled in global settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync();
|
||||
if (string.IsNullOrWhiteSpace(remoteChecksum))
|
||||
{
|
||||
_logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync();
|
||||
|
||||
if (string.Equals(currentChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Phishing domains list is up to date (checksum: {Checksum}). Skipping update.",
|
||||
currentChecksum);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Checksums differ (current: {CurrentChecksum}, remote: {RemoteChecksum}). Fetching updated domains from {Source}.",
|
||||
currentChecksum, remoteChecksum, _globalSettings.SelfHosted ? "Bitwarden cloud API" : "external source");
|
||||
|
||||
try
|
||||
{
|
||||
var domains = await _cloudPhishingDomainQuery.GetPhishingDomainsAsync();
|
||||
if (!domains.Contains("phishing.testcategory.com", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
domains.Add("phishing.testcategory.com");
|
||||
}
|
||||
|
||||
if (domains.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating {Count} phishing domains with checksum {Checksum}.",
|
||||
domains.Count, remoteChecksum);
|
||||
await _phishingDomainRepository.UpdatePhishingDomainsAsync(domains, remoteChecksum);
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated phishing domains.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(Constants.BypassFiltersEventId, "No valid domains found in the response. Skipping update.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(Constants.BypassFiltersEventId, ex, "Error updating phishing domains.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
public class KeysResponseModel : ResponseModel
|
||||
{
|
||||
public KeysResponseModel(User user)
|
||||
public KeysResponseModel(UserAccountKeysData accountKeys, string? masterKeyWrappedUserKey)
|
||||
: base("keys")
|
||||
{
|
||||
if (user == null)
|
||||
if (masterKeyWrappedUserKey != null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
Key = masterKeyWrappedUserKey;
|
||||
}
|
||||
|
||||
Key = user.Key;
|
||||
PublicKey = user.PublicKey;
|
||||
PrivateKey = user.PrivateKey;
|
||||
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||
PrivateKey = accountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;
|
||||
AccountKeys = new PrivateKeysResponseModel(accountKeys);
|
||||
}
|
||||
|
||||
public string Key { get; set; }
|
||||
/// <summary>
|
||||
/// The master key wrapped user key. The master key can either be a master-password master key or a
|
||||
/// key-connector master key.
|
||||
/// </summary>
|
||||
public string? Key { get; set; }
|
||||
[Obsolete("Use AccountKeys.PublicKeyEncryptionKeyPair.PublicKey instead")]
|
||||
public string PublicKey { get; set; }
|
||||
[Obsolete("Use AccountKeys.PublicKeyEncryptionKeyPair.WrappedPrivateKey instead")]
|
||||
public string PrivateKey { get; set; }
|
||||
public PrivateKeysResponseModel AccountKeys { get; set; }
|
||||
}
|
||||
|
||||
@@ -187,7 +187,6 @@ public class Startup
|
||||
services.AddBillingOperations();
|
||||
services.AddReportingServices();
|
||||
services.AddImportServices();
|
||||
services.AddPhishingDomainServices(globalSettings);
|
||||
|
||||
services.AddSendServices();
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Core.PhishingDomainFeatures;
|
||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Repositories.Implementations;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
@@ -103,25 +99,4 @@ public static class ServiceCollectionExtensions
|
||||
// Admin Console authorization handlers
|
||||
services.AddAdminConsoleAuthorizationHandlers();
|
||||
}
|
||||
|
||||
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
{
|
||||
services.AddHttpClient("PhishingDomains", client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("User-Agent", globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden");
|
||||
client.Timeout = TimeSpan.FromSeconds(1000); // the source list is very slow
|
||||
});
|
||||
|
||||
services.AddSingleton<AzurePhishingDomainStorageService>();
|
||||
services.AddSingleton<IPhishingDomainRepository, AzurePhishingDomainRepository>();
|
||||
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainRelayQuery>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainDirectQuery>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,6 @@
|
||||
"storage": {
|
||||
"connectionString": "UseDevelopmentStorage=true"
|
||||
},
|
||||
"phishingDomain": {
|
||||
"updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
|
||||
"checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256"
|
||||
},
|
||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,6 @@
|
||||
"accessKeySecret": "SECRET",
|
||||
"region": "SECRET"
|
||||
},
|
||||
"phishingDomain": {
|
||||
"updateUrl": "SECRET"
|
||||
},
|
||||
"distributedIpRateLimiting": {
|
||||
"enabled": true,
|
||||
"maxRedisTimeoutsThreshold": 10,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -34,6 +35,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;
|
||||
|
||||
public AcceptOrgUserCommand(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
@@ -46,7 +48,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator)
|
||||
{
|
||||
// TODO: remove data protector when old token validation removed
|
||||
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
|
||||
@@ -60,6 +63,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
|
||||
@@ -186,13 +190,19 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce Single Organization Policy of organization user is trying to join
|
||||
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
await ValidateAutomaticUserConfirmationPolicyAsync(orgUser, allOrgUsers, user);
|
||||
}
|
||||
|
||||
// Enforce Single Organization Policy of organization user is trying to join
|
||||
var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.SingleOrg, OrganizationUserStatusType.Invited);
|
||||
|
||||
if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||
if (allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId)
|
||||
&& invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("You may not join this organization until you leave or remove all other organizations.");
|
||||
}
|
||||
@@ -255,4 +265,20 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateAutomaticUserConfirmationPolicyAsync(OrganizationUser orgUser,
|
||||
ICollection<OrganizationUser> allOrgUsers, User user)
|
||||
{
|
||||
var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId, allOrgUsers, user)))
|
||||
.Match(
|
||||
error => error.Message,
|
||||
_ => string.Empty
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
throw new BadRequestException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
@@ -8,6 +9,7 @@ using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
@@ -16,6 +18,8 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,
|
||||
IUserService userService,
|
||||
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
|
||||
{
|
||||
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||
@@ -61,7 +65,7 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
return Invalid(request, new UserDoesNotHaveTwoFactorEnabled());
|
||||
}
|
||||
|
||||
if (await OrganizationUserConformsToSingleOrgPolicyAsync(request) is { } error)
|
||||
if (await OrganizationUserConformsToAutomaticUserConfirmationPolicyAsync(request) is { } error)
|
||||
{
|
||||
return Invalid(request, error);
|
||||
}
|
||||
@@ -69,10 +73,8 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
return Valid(request);
|
||||
}
|
||||
|
||||
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId,
|
||||
PolicyType.AutomaticUserConfirmation) is { Enabled: true }
|
||||
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation) is { Enabled: true }
|
||||
&& request.Organization is { UseAutomaticUserConfirmation: true };
|
||||
|
||||
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
@@ -87,30 +89,37 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
.IsTwoFactorRequiredForOrganization(request.Organization!.Id);
|
||||
}
|
||||
|
||||
private async Task<Error?> OrganizationUserConformsToSingleOrgPolicyAsync(
|
||||
/// <summary>
|
||||
/// Validates whether the specified organization user complies with the automatic user confirmation policy.
|
||||
/// This includes checks across all organizations the user is associated with to ensure they meet the compliance criteria.
|
||||
///
|
||||
/// We are not checking single organization policy compliance here because automatically confirm users policy enforces
|
||||
/// a stricter version and applies to all users. If you are compliant with Auto Confirm, you'll be in compliance with
|
||||
/// Single Org.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// The request model encapsulates the current organization, the user being validated, and all organization users associated
|
||||
/// with that user.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// An <see cref="Error"/> if the user fails to meet the automatic user confirmation policy, or null if the validation succeeds.
|
||||
/// </returns>
|
||||
private async Task<Error?> OrganizationUserConformsToAutomaticUserConfirmationPolicyAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
var allOrganizationUsersForUser = await organizationUserRepository
|
||||
.GetManyByUserAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
if (allOrganizationUsersForUser.Count == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var user = await userService.GetUserByIdAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
var policyRequirement = await policyRequirementQuery
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
if (policyRequirement.IsSingleOrgEnabledForThisOrganization(request.Organization!.Id))
|
||||
{
|
||||
return new OrganizationEnforcesSingleOrgPolicy();
|
||||
}
|
||||
|
||||
if (policyRequirement.IsSingleOrgEnabledForOrganizationsOtherThan(request.Organization.Id))
|
||||
{
|
||||
return new OtherOrganizationEnforcesSingleOrgPolicy();
|
||||
}
|
||||
|
||||
return null;
|
||||
return (await automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
request.OrganizationId,
|
||||
allOrganizationUsersForUser,
|
||||
user)))
|
||||
.Match<Error?>(
|
||||
error => error,
|
||||
_ => null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ public record UserIsNotUserType() : BadRequestError("Only organization users wit
|
||||
public record UserIsNotAccepted() : BadRequestError("Cannot confirm user that has not accepted the invitation.");
|
||||
public record OrganizationUserIdIsInvalid() : BadRequestError("Invalid organization user id.");
|
||||
public record UserDoesNotHaveTwoFactorEnabled() : BadRequestError("User does not have two-step login enabled.");
|
||||
public record OrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
|
||||
public record OtherOrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||
public record UserCannotBelongToAnotherOrganization() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
|
||||
public record OtherOrganizationDoesNotAllowOtherMembership() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||
public record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError("Cannot confirm this member because the Automatically Confirm Users policy is not enabled.");
|
||||
public record ProviderUsersCannotJoin() : BadRequestError("An organization the user is a part of has enabled Automatic User Confirmation policy, and it does not support provider users joining.");
|
||||
public record UserCannotJoinProvider() : BadRequestError("An organization the user is a part of has enabled Automatic User Confirmation policy, and it does not support the user joining a provider.");
|
||||
public record CurrentOrganizationUserIsNotPresentInRequest() : BadRequestError("The current organization user does not exist in the request.");
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@@ -33,6 +34,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;
|
||||
|
||||
public ConfirmOrganizationUserCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -47,7 +49,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
IDeviceRepository deviceRepository,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService,
|
||||
ICollectionRepository collectionRepository)
|
||||
ICollectionRepository collectionRepository,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -62,6 +65,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
_collectionRepository = collectionRepository;
|
||||
_automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||
@@ -127,6 +131,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
||||
|
||||
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
|
||||
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
||||
|
||||
@@ -188,6 +193,25 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
|
||||
|
||||
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationId,
|
||||
userOrgs,
|
||||
user)))
|
||||
.Match(
|
||||
error => new BadRequestException(error.Message),
|
||||
_ => null
|
||||
);
|
||||
|
||||
if (error is not null)
|
||||
{
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
||||
var otherSingleOrgPolicies =
|
||||
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
||||
@@ -267,8 +291,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
return;
|
||||
}
|
||||
|
||||
var organizationDataOwnershipPolicy =
|
||||
await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(organizationUser.UserId!.Value);
|
||||
var organizationDataOwnershipPolicy = await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(organizationUser.UserId!.Value);
|
||||
if (!organizationDataOwnershipPolicy.RequiresDefaultCollectionOnConfirm(organizationUser.OrganizationId))
|
||||
{
|
||||
return;
|
||||
@@ -311,8 +334,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
||||
return;
|
||||
}
|
||||
|
||||
var policyEligibleOrganizationUserIds =
|
||||
await _policyRequirementQuery.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organizationId);
|
||||
var policyEligibleOrganizationUserIds = await _policyRequirementQuery
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organizationId);
|
||||
|
||||
var eligibleOrganizationUserIds = confirmedOrganizationUsers
|
||||
.Where(ou => policyEligibleOrganizationUserIds.Contains(ou.Id))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@@ -29,7 +30,8 @@ public class RestoreOrganizationUserCommand(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationService organizationService,
|
||||
IFeatureService featureService,
|
||||
IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator) : IRestoreOrganizationUserCommand
|
||||
{
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||
{
|
||||
@@ -300,6 +302,25 @@ public class RestoreOrganizationUserCommand(
|
||||
{
|
||||
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
||||
}
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var validationResult = await automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId,
|
||||
allOrgUsers,
|
||||
user!));
|
||||
|
||||
var badRequestException = validationResult.Match(
|
||||
error => new BadRequestException(user.Email +
|
||||
" is not compliant with the automatic user confirmation policy: " +
|
||||
error.Message),
|
||||
_ => null);
|
||||
|
||||
if (badRequestException is not null)
|
||||
{
|
||||
throw badRequestException;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
@@ -43,7 +45,9 @@ public class CloudOrganizationSignUpCommand(
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICollectionRepository collectionRepository,
|
||||
IDeviceRepository deviceRepository,
|
||||
IPricingClient pricingClient) : ICloudOrganizationSignUpCommand
|
||||
IPricingClient pricingClient,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService) : ICloudOrganizationSignUpCommand
|
||||
{
|
||||
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
|
||||
{
|
||||
@@ -237,6 +241,17 @@ public class CloudOrganizationSignUpCommand(
|
||||
|
||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var requirement = await policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);
|
||||
|
||||
if (requirement.CannotCreateNewOrganization())
|
||||
{
|
||||
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
@@ -28,6 +30,8 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
public InitPendingOrganizationCommand(
|
||||
IOrganizationService organizationService,
|
||||
@@ -37,7 +41,9 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IOrganizationUserRepository organizationUserRepository
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IFeatureService featureService,
|
||||
IPolicyRequirementQuery policyRequirementQuery
|
||||
)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
@@ -48,6 +54,8 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_featureService = featureService;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)
|
||||
@@ -113,6 +121,17 @@ public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand
|
||||
|
||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var requirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);
|
||||
|
||||
if (requirement.CannotCreateNewOrganization())
|
||||
{
|
||||
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
@@ -31,6 +33,8 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
public SelfHostedOrganizationSignUpCommand(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -44,7 +48,9 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
|
||||
ILicensingService licensingService,
|
||||
IPolicyService policyService,
|
||||
IGlobalSettings globalSettings,
|
||||
IStripePaymentService paymentService)
|
||||
IStripePaymentService paymentService,
|
||||
IFeatureService featureService,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -58,6 +64,8 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
|
||||
_policyService = policyService;
|
||||
_globalSettings = globalSettings;
|
||||
_paymentService = paymentService;
|
||||
_featureService = featureService;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
public async Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync(
|
||||
@@ -103,6 +111,17 @@ public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUp
|
||||
|
||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||
{
|
||||
var requirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);
|
||||
|
||||
if (requirement.CannotCreateNewOrganization())
|
||||
{
|
||||
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
|
||||
/// <summary>
|
||||
/// Request object for <see cref="AutomaticUserConfirmationPolicyEnforcementValidator"/>
|
||||
/// </summary>
|
||||
public record AutomaticUserConfirmationPolicyEnforcementRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Organization to be validated
|
||||
/// </summary>
|
||||
public Guid OrganizationId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All organization users that match the provided user.
|
||||
/// </summary>
|
||||
public ICollection<OrganizationUser> AllOrganizationUsers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// User associated with the organization user to be confirmed
|
||||
/// </summary>
|
||||
public User User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Request object for <see cref="AutomaticUserConfirmationPolicyEnforcementValidator"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This record is used to encapsulate the data required for handling the automatic confirmation policy enforcement.
|
||||
/// </remarks>
|
||||
/// <param name="organizationId">The organization to be validated.</param>
|
||||
/// <param name="organizationUsers">All organization users that match the provided user.</param>
|
||||
/// <param name="user">The user entity connecting all org users provided.</param>
|
||||
public AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
Guid organizationId,
|
||||
IEnumerable<OrganizationUser> organizationUsers,
|
||||
User user)
|
||||
{
|
||||
OrganizationId = organizationId;
|
||||
AllOrganizationUsers = organizationUsers.ToArray();
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
|
||||
public class AutomaticUserConfirmationPolicyEnforcementValidator(
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IProviderUserRepository providerUserRepository)
|
||||
: IAutomaticUserConfirmationPolicyEnforcementValidator
|
||||
{
|
||||
public async Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(
|
||||
AutomaticUserConfirmationPolicyEnforcementRequest request)
|
||||
{
|
||||
var automaticUserConfirmationPolicyRequirement = await policyRequirementQuery
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(request.User.Id);
|
||||
|
||||
var currentOrganizationUser = request.AllOrganizationUsers
|
||||
.FirstOrDefault(x => x.OrganizationId == request.OrganizationId
|
||||
&& x.UserId == request.User.Id);
|
||||
|
||||
if (currentOrganizationUser is null)
|
||||
{
|
||||
return Invalid(request, new CurrentOrganizationUserIsNotPresentInRequest());
|
||||
}
|
||||
|
||||
if (automaticUserConfirmationPolicyRequirement.IsEnabled(request.OrganizationId))
|
||||
{
|
||||
if ((await providerUserRepository.GetManyByUserAsync(request.User.Id)).Count != 0)
|
||||
{
|
||||
return Invalid(request, new ProviderUsersCannotJoin());
|
||||
}
|
||||
|
||||
if (request.AllOrganizationUsers.Count > 1)
|
||||
{
|
||||
return Invalid(request, new UserCannotBelongToAnotherOrganization());
|
||||
}
|
||||
}
|
||||
|
||||
if (automaticUserConfirmationPolicyRequirement.IsEnabledForOrganizationsOtherThan(currentOrganizationUser.OrganizationId))
|
||||
{
|
||||
return Invalid(request, new OtherOrganizationDoesNotAllowOtherMembership());
|
||||
}
|
||||
|
||||
return Valid(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
|
||||
/// <summary>
|
||||
/// Used to enforce the Automatic User Confirmation policy. It uses the <see cref="IPolicyRequirementQuery"/> to retrieve
|
||||
/// the <see cref="AutomaticUserConfirmationPolicyRequirement"/>. It is used to check to make sure the given user is
|
||||
/// valid for the Automatic User Confirmation policy. It also validates that the given user is not a provider
|
||||
/// or a member of another organization regardless of status or type.
|
||||
/// </summary>
|
||||
public interface IAutomaticUserConfirmationPolicyEnforcementValidator
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given user is compliant with the Automatic User Confirmation policy.
|
||||
///
|
||||
/// To be compliant, a user must
|
||||
/// - not be a member of a provider
|
||||
/// - not be a member of another organization
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <remarks>
|
||||
/// This uses the validation result pattern to avoid throwing exceptions.
|
||||
/// </remarks>
|
||||
/// <returns>A validation result with the error message if applicable.</returns>
|
||||
Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(AutomaticUserConfirmationPolicyEnforcementRequest request);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the enforcement status of the Automatic User Confirmation policy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Automatic User Confirmation policy is enforced against all types of users regardless of status or type.
|
||||
///
|
||||
/// Users cannot:
|
||||
/// <ul>
|
||||
/// <li>Be a member of another organization (similar to Single Organization Policy)</li>
|
||||
/// <li>Cannot be a provider</li>
|
||||
/// </ul>
|
||||
/// </remarks>
|
||||
/// <param name="policyDetails">Collection of policy details that apply to this user id</param>
|
||||
public class AutomaticUserConfirmationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement
|
||||
{
|
||||
public bool CannotBeGrantedEmergencyAccess() => policyDetails.Any();
|
||||
|
||||
public bool CannotJoinProvider() => policyDetails.Any();
|
||||
|
||||
public bool CannotCreateProvider() => policyDetails.Any();
|
||||
|
||||
public bool CannotCreateNewOrganization() => policyDetails.Any();
|
||||
|
||||
public bool IsEnabled(Guid organizationId) => policyDetails.Any(p => p.OrganizationId == organizationId);
|
||||
|
||||
public bool IsEnabledForOrganizationsOtherThan(Guid organizationId) =>
|
||||
policyDetails.Any(p => p.OrganizationId != organizationId);
|
||||
}
|
||||
|
||||
public class AutomaticUserConfirmationPolicyRequirementFactory : BasePolicyRequirementFactory<AutomaticUserConfirmationPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.AutomaticUserConfirmation;
|
||||
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles => [];
|
||||
|
||||
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];
|
||||
|
||||
protected override bool ExemptProviders => false;
|
||||
|
||||
public override AutomaticUserConfirmationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>
|
||||
new(policyDetails);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
@@ -23,6 +24,8 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddPolicyRequirements();
|
||||
services.AddPolicySideEffects();
|
||||
services.AddPolicyUpdateEvents();
|
||||
|
||||
services.AddScoped<IAutomaticUserConfirmationPolicyEnforcementValidator, AutomaticUserConfirmationPolicyEnforcementValidator>();
|
||||
}
|
||||
|
||||
[Obsolete("Use AddPolicyUpdateEvents instead.")]
|
||||
@@ -69,5 +72,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, AutomaticUserConfirmationPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
public class KeysRequestModel
|
||||
{
|
||||
[Obsolete("Use AccountKeys.AccountPublicKey instead")]
|
||||
[Required]
|
||||
public string PublicKey { get; set; }
|
||||
[Obsolete("Use AccountKeys.UserKeyEncryptedAccountPrivateKey instead")]
|
||||
[Required]
|
||||
public string EncryptedPrivateKey { get; set; }
|
||||
public AccountKeysRequestModel AccountKeys { get; set; }
|
||||
|
||||
[Obsolete("Use SetAccountKeysForUserCommand instead")]
|
||||
public User ToUser(User existingUser)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey))
|
||||
|
||||
@@ -99,6 +99,9 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
|
||||
public async Task<IdentityResult> RegisterSSOAutoProvisionedUserAsync(User user, Organization organization)
|
||||
{
|
||||
// Validate that the email domain is not blocked by another organization's policy
|
||||
await ValidateEmailDomainNotBlockedAsync(user.Email, organization.Id);
|
||||
|
||||
var result = await _userService.CreateUserAsync(user);
|
||||
if (result == IdentityResult.Success)
|
||||
{
|
||||
|
||||
@@ -198,10 +198,8 @@ public static class FeatureFlagKeys
|
||||
public const string PM28265_ReconcileAdditionalStorageJobEnableLiveMode = "pm-28265-reconcile-additional-storage-job-enable-live-mode";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string Argon2Default = "argon2-default";
|
||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
public const string EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation";
|
||||
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
|
||||
@@ -211,6 +209,7 @@ public static class FeatureFlagKeys
|
||||
public const string NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change";
|
||||
public const string DisableType0Decryption = "pm-25174-disable-type-0-decryption";
|
||||
public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component";
|
||||
public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit";
|
||||
public const string DataRecoveryTool = "pm-28813-data-recovery-tool";
|
||||
|
||||
/* Mobile Team */
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.PhishingDomainFeatures;
|
||||
|
||||
public class AzurePhishingDomainStorageService
|
||||
{
|
||||
private const string _containerName = "phishingdomains";
|
||||
private const string _domainsFileName = "domains.txt";
|
||||
private const string _checksumFileName = "checksum.txt";
|
||||
|
||||
private readonly BlobServiceClient _blobServiceClient;
|
||||
private readonly ILogger<AzurePhishingDomainStorageService> _logger;
|
||||
private BlobContainerClient _containerClient;
|
||||
|
||||
public AzurePhishingDomainStorageService(
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<AzurePhishingDomainStorageService> logger)
|
||||
{
|
||||
_blobServiceClient = new BlobServiceClient(globalSettings.Storage.ConnectionString);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ICollection<string>> GetDomainsAsync()
|
||||
{
|
||||
await InitAsync();
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(_domainsFileName);
|
||||
if (!await blobClient.ExistsAsync())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var response = await blobClient.DownloadAsync();
|
||||
using var streamReader = new StreamReader(response.Value.Content);
|
||||
var content = await streamReader.ReadToEndAsync();
|
||||
|
||||
return [.. content
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.Trim())
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))];
|
||||
}
|
||||
|
||||
public async Task<string> GetChecksumAsync()
|
||||
{
|
||||
await InitAsync();
|
||||
|
||||
var blobClient = _containerClient.GetBlobClient(_checksumFileName);
|
||||
if (!await blobClient.ExistsAsync())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var response = await blobClient.DownloadAsync();
|
||||
using var streamReader = new StreamReader(response.Value.Content);
|
||||
return (await streamReader.ReadToEndAsync()).Trim();
|
||||
}
|
||||
|
||||
public async Task UpdateDomainsAsync(IEnumerable<string> domains, string checksum)
|
||||
{
|
||||
await InitAsync();
|
||||
|
||||
var domainsContent = string.Join(Environment.NewLine, domains);
|
||||
var domainsStream = new MemoryStream(Encoding.UTF8.GetBytes(domainsContent));
|
||||
var domainsBlobClient = _containerClient.GetBlobClient(_domainsFileName);
|
||||
|
||||
await domainsBlobClient.UploadAsync(domainsStream, new BlobUploadOptions
|
||||
{
|
||||
HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" }
|
||||
}, CancellationToken.None);
|
||||
|
||||
var checksumStream = new MemoryStream(Encoding.UTF8.GetBytes(checksum));
|
||||
var checksumBlobClient = _containerClient.GetBlobClient(_checksumFileName);
|
||||
|
||||
await checksumBlobClient.UploadAsync(checksumStream, new BlobUploadOptions
|
||||
{
|
||||
HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" }
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task InitAsync()
|
||||
{
|
||||
if (_containerClient is null)
|
||||
{
|
||||
_containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
|
||||
await _containerClient.CreateIfNotExistsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.PhishingDomainFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of ICloudPhishingDomainQuery for cloud environments
|
||||
/// that directly calls the external phishing domain source
|
||||
/// </summary>
|
||||
public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery
|
||||
{
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<CloudPhishingDomainDirectQuery> _logger;
|
||||
|
||||
public CloudPhishingDomainDirectQuery(
|
||||
IGlobalSettings globalSettings,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<CloudPhishingDomainDirectQuery> logger)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetPhishingDomainsAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Phishing domain update URL is not configured.");
|
||||
}
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient("PhishingDomains");
|
||||
var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.UpdateUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
return ParseDomains(content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SHA256 checksum of the remote phishing domains list
|
||||
/// </summary>
|
||||
/// <returns>The SHA256 checksum as a lowercase hex string</returns>
|
||||
public async Task<string> GetRemoteChecksumAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.ChecksumUrl))
|
||||
{
|
||||
_logger.LogWarning("Phishing domain checksum URL is not configured.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient("PhishingDomains");
|
||||
var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.ChecksumUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
return ParseChecksumResponse(content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving phishing domain checksum from {Url}",
|
||||
_globalSettings.PhishingDomain.ChecksumUrl);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a checksum response in the format "hash *filename"
|
||||
/// </summary>
|
||||
private static string ParseChecksumResponse(string checksumContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksumContent))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Format is typically "hash *filename"
|
||||
var parts = checksumContent.Split(' ', 2);
|
||||
|
||||
return parts.Length > 0 ? parts[0].Trim() : string.Empty;
|
||||
}
|
||||
|
||||
private static List<string> ParseDomains(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return content
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.Trim())
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.PhishingDomainFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of ICloudPhishingDomainQuery for self-hosted environments
|
||||
/// that relays the request to the Bitwarden cloud API
|
||||
/// </summary>
|
||||
public class CloudPhishingDomainRelayQuery : BaseIdentityClientService, ICloudPhishingDomainQuery
|
||||
{
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public CloudPhishingDomainRelayQuery(
|
||||
IHttpClientFactory httpFactory,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<CloudPhishingDomainRelayQuery> logger)
|
||||
: base(
|
||||
httpFactory,
|
||||
globalSettings.Installation.ApiUri,
|
||||
globalSettings.Installation.IdentityUri,
|
||||
"api.licensing",
|
||||
$"installation.{globalSettings.Installation.Id}",
|
||||
globalSettings.Installation.Key,
|
||||
logger)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetPhishingDomainsAsync()
|
||||
{
|
||||
if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication)
|
||||
{
|
||||
throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled.");
|
||||
}
|
||||
|
||||
var result = await SendAsync<object, string[]>(HttpMethod.Get, "phishing-domains", null, true);
|
||||
return result?.ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SHA256 checksum of the remote phishing domains list
|
||||
/// </summary>
|
||||
/// <returns>The SHA256 checksum as a lowercase hex string</returns>
|
||||
public async Task<string> GetRemoteChecksumAsync()
|
||||
{
|
||||
if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication)
|
||||
{
|
||||
throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// For self-hosted environments, we get the checksum from the Bitwarden cloud API
|
||||
var result = await SendAsync<object, string>(HttpMethod.Get, "phishing-domains/checksum", null, true);
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving phishing domain checksum from Bitwarden cloud API");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
|
||||
public interface ICloudPhishingDomainQuery
|
||||
{
|
||||
Task<List<string>> GetPhishingDomainsAsync();
|
||||
Task<string> GetRemoteChecksumAsync();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IPhishingDomainRepository
|
||||
{
|
||||
Task<ICollection<string>> GetActivePhishingDomainsAsync();
|
||||
Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum);
|
||||
Task<string> GetCurrentChecksumAsync();
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.PhishingDomainFeatures;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Repositories.Implementations;
|
||||
|
||||
public class AzurePhishingDomainRepository : IPhishingDomainRepository
|
||||
{
|
||||
private readonly AzurePhishingDomainStorageService _storageService;
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly ILogger<AzurePhishingDomainRepository> _logger;
|
||||
private const string _domainsCacheKey = "PhishingDomains_v1";
|
||||
private const string _checksumCacheKey = "PhishingDomains_Checksum_v1";
|
||||
private static readonly DistributedCacheEntryOptions _cacheOptions = new()
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
|
||||
SlidingExpiration = TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
public AzurePhishingDomainRepository(
|
||||
AzurePhishingDomainStorageService storageService,
|
||||
IDistributedCache cache,
|
||||
ILogger<AzurePhishingDomainRepository> logger)
|
||||
{
|
||||
_storageService = storageService;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ICollection<string>> GetActivePhishingDomainsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cachedDomains = await _cache.GetStringAsync(_domainsCacheKey);
|
||||
if (!string.IsNullOrEmpty(cachedDomains))
|
||||
{
|
||||
_logger.LogDebug("Retrieved phishing domains from cache");
|
||||
return JsonSerializer.Deserialize<ICollection<string>>(cachedDomains) ?? [];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to retrieve phishing domains from cache");
|
||||
}
|
||||
|
||||
var domains = await _storageService.GetDomainsAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _cache.SetStringAsync(
|
||||
_domainsCacheKey,
|
||||
JsonSerializer.Serialize(domains),
|
||||
_cacheOptions);
|
||||
_logger.LogDebug("Stored {Count} phishing domains in cache", domains.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to store phishing domains in cache");
|
||||
}
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
public async Task<string> GetCurrentChecksumAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var cachedChecksum = await _cache.GetStringAsync(_checksumCacheKey);
|
||||
if (!string.IsNullOrEmpty(cachedChecksum))
|
||||
{
|
||||
_logger.LogDebug("Retrieved phishing domain checksum from cache");
|
||||
return cachedChecksum;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to retrieve phishing domain checksum from cache");
|
||||
}
|
||||
|
||||
var checksum = await _storageService.GetChecksumAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(checksum))
|
||||
{
|
||||
await _cache.SetStringAsync(
|
||||
_checksumCacheKey,
|
||||
checksum,
|
||||
_cacheOptions);
|
||||
_logger.LogDebug("Stored phishing domain checksum in cache");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to store phishing domain checksum in cache");
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum)
|
||||
{
|
||||
var domainsList = domains.ToList();
|
||||
await _storageService.UpdateDomainsAsync(domainsList, checksum);
|
||||
|
||||
try
|
||||
{
|
||||
await _cache.SetStringAsync(
|
||||
_domainsCacheKey,
|
||||
JsonSerializer.Serialize(domainsList),
|
||||
_cacheOptions);
|
||||
|
||||
await _cache.SetStringAsync(
|
||||
_checksumCacheKey,
|
||||
checksum,
|
||||
_cacheOptions);
|
||||
|
||||
_logger.LogDebug("Updated phishing domains cache after update operation");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to update phishing domains in cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
||||
public virtual string DevelopmentDirectory { get; set; }
|
||||
public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();
|
||||
public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings();
|
||||
|
||||
public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5;
|
||||
public virtual bool EnableEmailVerification { get; set; }
|
||||
@@ -690,12 +689,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
public int MaxNetworkRetries { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class PhishingDomainSettings : IPhishingDomainSettings
|
||||
{
|
||||
public string UpdateUrl { get; set; }
|
||||
public string ChecksumUrl { get; set; }
|
||||
}
|
||||
|
||||
public class DistributedIpRateLimitingSettings
|
||||
{
|
||||
public string RedisConnectionString { get; set; }
|
||||
|
||||
@@ -28,5 +28,4 @@ public interface IGlobalSettings
|
||||
string DevelopmentDirectory { get; set; }
|
||||
IWebPushSettings WebPush { get; set; }
|
||||
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
||||
IPhishingDomainSettings PhishingDomain { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface IPhishingDomainSettings
|
||||
{
|
||||
string UpdateUrl { get; set; }
|
||||
string ChecksumUrl { get; set; }
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
Send Access Request Validation
|
||||
===
|
||||
# Send Access Request Validation
|
||||
|
||||
This feature supports the ability of Tools to require specific claims for access to sends.
|
||||
|
||||
In order to access Send data a user must meet the requirements laid out in these request validators.
|
||||
|
||||
# ***Important: String Constants***
|
||||
|
||||
The string constants contained herein are used in conjunction with the Auth module in the SDK. Any change to these string values _must_ be intentional and _must_ have a corresponding change in the SDK.
|
||||
> [!IMPORTANT]
|
||||
> The string constants contained herein are used in conjunction with the Auth module in the SDK. Any change to these string values _must_ be intentional and _must_ have a corresponding change in the SDK.
|
||||
|
||||
There is snapshot testing that will fail if the strings change to help detect unintended changes to the string constants.
|
||||
|
||||
# Custom Claims
|
||||
## Custom Claims
|
||||
|
||||
Send access tokens contain custom claims specific to the Send the Send grant type.
|
||||
|
||||
@@ -19,41 +17,41 @@ Send access tokens contain custom claims specific to the Send the Send grant typ
|
||||
1. `send_email` - only set when the Send requires `EmailOtp` authentication type.
|
||||
1. `type` - this will always be `Send`
|
||||
|
||||
# Authentication methods
|
||||
## Authentication methods
|
||||
|
||||
## `NeverAuthenticate`
|
||||
### `NeverAuthenticate`
|
||||
|
||||
For a Send to be in this state two things can be true:
|
||||
1. The Send has been modified and no longer allows access.
|
||||
2. The Send does not exist.
|
||||
|
||||
## `NotAuthenticated`
|
||||
### `NotAuthenticated`
|
||||
|
||||
In this scenario the Send is not protected by any added authentication or authorization and the access token is issued to the requesting user.
|
||||
|
||||
## `ResourcePassword`
|
||||
### `ResourcePassword`
|
||||
|
||||
In this scenario the Send is password protected and a user must supply the correct password hash to be issued an access token.
|
||||
|
||||
## `EmailOtp`
|
||||
### `EmailOtp`
|
||||
|
||||
In this scenario the Send is only accessible to owners of specific email addresses. The user must submit a correct email. Once the email has been entered then ownership of the email must be established via OTP. The Otp is sent to the aforementioned email and must be supplied, along with the email, to be issued an access token.
|
||||
|
||||
# Send Access Request Validation
|
||||
## Send Access Request Validation
|
||||
|
||||
## Required Parameters
|
||||
### Required Parameters
|
||||
|
||||
### All Requests
|
||||
#### All Requests
|
||||
- `send_id` - Base64 URL-encoded GUID of the send being accessed
|
||||
|
||||
### Password Protected Sends
|
||||
#### Password Protected Sends
|
||||
- `password_hash_b64` - client hashed Base64-encoded password.
|
||||
|
||||
### Email OTP Protected Sends
|
||||
#### Email OTP Protected Sends
|
||||
- `email` - Email address associated with the send
|
||||
- `otp` - One-time password (optional - if missing, OTP is generated and sent)
|
||||
|
||||
## Error Responses
|
||||
### Error Responses
|
||||
|
||||
All errors include a custom response field:
|
||||
```json
|
||||
@@ -62,5 +60,4 @@ All errors include a custom response field:
|
||||
"error_description": "Human readable description",
|
||||
"send_access_error_type": "specific_error_code"
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -38,6 +39,7 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
@@ -53,6 +55,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
|
||||
_sut = new AccountsController(
|
||||
_organizationService,
|
||||
@@ -66,7 +69,8 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService,
|
||||
_userAccountKeysQuery,
|
||||
_twoFactorEmailService,
|
||||
_changeKdfCommand
|
||||
_changeKdfCommand,
|
||||
_userRepository
|
||||
);
|
||||
}
|
||||
|
||||
@@ -688,6 +692,37 @@ public class AccountsControllerTests : IDisposable
|
||||
await _sut.PostKdf(model);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostKeys_NoUser_Errors(KeysRequestModel model)
|
||||
{
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKeys(model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("existing", "existing")]
|
||||
[BitAutoData((string)null, "existing")]
|
||||
[BitAutoData("", "existing")]
|
||||
[BitAutoData(" ", "existing")]
|
||||
[BitAutoData("existing", null)]
|
||||
[BitAutoData("existing", "")]
|
||||
[BitAutoData("existing", " ")]
|
||||
public async Task PostKeys_UserAlreadyHasKeys_Errors(string? existingPrivateKey, string? existingPublicKey,
|
||||
KeysRequestModel model)
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
user.PrivateKey = existingPrivateKey;
|
||||
user.PublicKey = existingPublicKey;
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKeys(model));
|
||||
|
||||
Assert.NotNull(exception.Message);
|
||||
Assert.Contains("User has existing keypair", exception.Message);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -738,5 +773,77 @@ public class AccountsControllerTests : IDisposable
|
||||
_userService.GetUserByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(Task.FromResult((User)null));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostKeys_WithAccountKeys_CallsSetV2AccountCryptographicState(
|
||||
User user,
|
||||
KeysRequestModel model)
|
||||
{
|
||||
// Arrange
|
||||
user.PublicKey = null;
|
||||
user.PrivateKey = null;
|
||||
model.AccountKeys = new AccountKeysRequestModel
|
||||
{
|
||||
UserKeyEncryptedAccountPrivateKey = "wrapped-private-key",
|
||||
AccountPublicKey = "public-key",
|
||||
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
PublicKey = "public-key",
|
||||
WrappedPrivateKey = "wrapped-private-key",
|
||||
SignedPublicKey = "signed-public-key"
|
||||
},
|
||||
SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
VerifyingKey = "verifying-key",
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = "wrapped-signing-key"
|
||||
},
|
||||
SecurityState = new SecurityStateModel
|
||||
{
|
||||
SecurityState = "security-state",
|
||||
SecurityVersion = 2
|
||||
}
|
||||
};
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
|
||||
// Act
|
||||
var result = await _sut.PostKeys(model);
|
||||
|
||||
// Assert
|
||||
await _userRepository.Received(1).SetV2AccountCryptographicStateAsync(
|
||||
user.Id,
|
||||
Arg.Any<UserAccountKeysData>());
|
||||
await _userService.DidNotReceiveWithAnyArgs().SaveUserAsync(Arg.Any<User>());
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("keys", result.Object);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostKeys_WithoutAccountKeys_CallsSaveUser(
|
||||
User user,
|
||||
KeysRequestModel model)
|
||||
{
|
||||
// Arrange
|
||||
user.PublicKey = null;
|
||||
user.PrivateKey = null;
|
||||
model.AccountKeys = null;
|
||||
model.PublicKey = "public-key";
|
||||
model.EncryptedPrivateKey = "encrypted-private-key";
|
||||
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
|
||||
// Act
|
||||
var result = await _sut.PostKeys(model);
|
||||
|
||||
// Assert
|
||||
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
|
||||
u.PublicKey == model.PublicKey &&
|
||||
u.PrivateKey == model.EncryptedPrivateKey));
|
||||
await _userRepository.DidNotReceiveWithAnyArgs()
|
||||
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>());
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("keys", result.Object);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
@@ -9,10 +10,16 @@ namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
internal class OrganizationUserPolicyDetailsCustomization : ICustomization
|
||||
{
|
||||
public PolicyType Type { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType UserType { get; set; }
|
||||
public bool IsProvider { get; set; }
|
||||
|
||||
public OrganizationUserPolicyDetailsCustomization(PolicyType type)
|
||||
public OrganizationUserPolicyDetailsCustomization(PolicyType type, OrganizationUserStatusType status, OrganizationUserType userType, bool isProvider)
|
||||
{
|
||||
Type = type;
|
||||
Status = status;
|
||||
UserType = userType;
|
||||
IsProvider = isProvider;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
@@ -20,6 +27,9 @@ internal class OrganizationUserPolicyDetailsCustomization : ICustomization
|
||||
fixture.Customize<OrganizationUserPolicyDetails>(composer => composer
|
||||
.With(o => o.OrganizationId, Guid.NewGuid())
|
||||
.With(o => o.PolicyType, Type)
|
||||
.With(o => o.OrganizationUserStatus, Status)
|
||||
.With(o => o.OrganizationUserType, UserType)
|
||||
.With(o => o.IsProvider, IsProvider)
|
||||
.With(o => o.PolicyEnabled, true));
|
||||
}
|
||||
}
|
||||
@@ -27,14 +37,25 @@ internal class OrganizationUserPolicyDetailsCustomization : ICustomization
|
||||
public class OrganizationUserPolicyDetailsAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly PolicyType _type;
|
||||
private readonly OrganizationUserStatusType _status;
|
||||
private readonly OrganizationUserType _userType;
|
||||
private readonly bool _isProvider;
|
||||
|
||||
public OrganizationUserPolicyDetailsAttribute(PolicyType type)
|
||||
public OrganizationUserPolicyDetailsAttribute(PolicyType type) : this(type, OrganizationUserStatusType.Accepted, OrganizationUserType.User, false)
|
||||
{
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public OrganizationUserPolicyDetailsAttribute(PolicyType type, OrganizationUserStatusType status, OrganizationUserType userType, bool isProvider)
|
||||
{
|
||||
_type = type;
|
||||
_status = status;
|
||||
_userType = userType;
|
||||
_isProvider = isProvider;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new OrganizationUserPolicyDetailsCustomization(_type);
|
||||
return new OrganizationUserPolicyDetailsCustomization(_type, _status, _userType, _isProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -24,6 +26,7 @@ using Bit.Test.Common.Fakes;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
@@ -673,6 +676,79 @@ public class AcceptOrgUserCommandTests
|
||||
Assert.Equal("User not found within organization.", exception.Message);
|
||||
}
|
||||
|
||||
// Auto-confirm policy validation tests --------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AcceptOrgUserAsync_WithAutoConfirmIsNotEnabled_DoesNotCheckCompliance(
|
||||
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
|
||||
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
|
||||
|
||||
await sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>().DidNotReceiveWithAnyArgs()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AcceptOrgUserAsync_WithUserThatIsCompliantWithAutoConfirm_AcceptsUser(
|
||||
SutProvider<AcceptOrgUserCommand> sutProvider,
|
||||
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
|
||||
{
|
||||
// Arrange
|
||||
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
|
||||
|
||||
// Mock auto-confirm enforcement query to return valid (no auto-confirm restrictions)
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));
|
||||
|
||||
// Act
|
||||
var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
|
||||
|
||||
// Assert
|
||||
AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(
|
||||
Arg.Is<OrganizationUser>(ou => ou.Id == orgUser.Id && ou.Status == OrganizationUserStatusType.Accepted));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AcceptOrgUserAsync_WithAutoConfirmIsEnabledAndFailsCompliance_ThrowsBadRequestException(
|
||||
SutProvider<AcceptOrgUserCommand> sutProvider,
|
||||
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails,
|
||||
OrganizationUser otherOrgUser)
|
||||
{
|
||||
// Arrange
|
||||
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Invalid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser, otherOrgUser], user),
|
||||
new UserCannotBelongToAnotherOrganization()));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
|
||||
|
||||
// Should get auto-confirm error
|
||||
Assert.Equal(new UserCannotBelongToAnotherOrganization().Message, exception.Message);
|
||||
}
|
||||
|
||||
// Private helpers -------------------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
@@ -716,7 +792,7 @@ public class AcceptOrgUserCommandTests
|
||||
/// - 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,
|
||||
private static void SetupCommonAcceptOrgUserMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user,
|
||||
Organization org,
|
||||
OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
|
||||
{
|
||||
@@ -729,18 +805,12 @@ public class AcceptOrgUserCommandTests
|
||||
// User is not part of any other orgs
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns(
|
||||
Task.FromResult<ICollection<OrganizationUser>>(new List<OrganizationUser>())
|
||||
);
|
||||
.Returns([]);
|
||||
|
||||
// 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>()
|
||||
)
|
||||
);
|
||||
.Returns([]);
|
||||
|
||||
// User is not part of any organization that applies the single org policy
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
@@ -750,20 +820,24 @@ public class AcceptOrgUserCommandTests
|
||||
// Org does not require 2FA
|
||||
sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited)
|
||||
.Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>(
|
||||
new List<OrganizationUserPolicyDetails>()));
|
||||
.Returns([]);
|
||||
|
||||
// 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 }
|
||||
));
|
||||
.Returns([adminUserDetails]);
|
||||
|
||||
// Return org
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(org.Id)
|
||||
.Returns(Task.FromResult(org));
|
||||
.Returns(org);
|
||||
|
||||
// Auto-confirm enforcement query returns valid by default (no restrictions)
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(request)
|
||||
.Returns(Valid(request));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@@ -12,6 +13,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
@@ -19,6 +21,7 @@ using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;
|
||||
|
||||
@@ -116,11 +119,11 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
@@ -140,12 +143,23 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
.Returns([(user.Id, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,
|
||||
[organizationUser],
|
||||
user)));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
@@ -319,11 +333,11 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
@@ -343,12 +357,24 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
.Returns([(user.Id, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,
|
||||
[organizationUser],
|
||||
user)));
|
||||
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
@@ -362,11 +388,11 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
@@ -386,16 +412,28 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, false)]);
|
||||
.Returns([(user.Id, false)]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(userId)
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement([])); // No 2FA policy
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,
|
||||
[organizationUser],
|
||||
user)));
|
||||
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
@@ -403,128 +441,17 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnThisOrg_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var singleOrgPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.SingleOrg
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationEnforcesSingleOrgPolicy>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnOtherOrg_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var otherOrgId = Guid.NewGuid(); // Different org
|
||||
var singleOrgPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = otherOrgId,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OtherOrganizationEnforcesSingleOrgPolicy>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInSingleOrg_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
@@ -544,61 +471,22 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
.Returns([(user.Id, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([organizationUser]); // Single org
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithNoSingleOrgPolicy_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
autoConfirmPolicy.Type = PolicyType.AutomaticUserConfirmation;
|
||||
autoConfirmPolicy.Enabled = true;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([]));
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,
|
||||
[organizationUser],
|
||||
user)));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
@@ -693,4 +581,59 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNonProviderUser_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(user.Id, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,
|
||||
[organizationUser],
|
||||
user)));
|
||||
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@@ -21,6 +23,7 @@ using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
@@ -559,4 +562,256 @@ public class ConfirmOrganizationUserCommandTests
|
||||
.DidNotReceive()
|
||||
.UpsertDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithAutoConfirmEnabledAndUserBelongsToAnotherOrg_ThrowsBadRequest(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser otherOrgUser, string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
otherOrgUser.UserId = user.Id;
|
||||
otherOrgUser.OrganizationId = Guid.NewGuid(); // Different org
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync([])
|
||||
.ReturnsForAnyArgs([orgUser, otherOrgUser]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Invalid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.Id, [orgUser, otherOrgUser], user),
|
||||
new UserCannotBelongToAnotherOrganization()));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
|
||||
Assert.Equal(new UserCannotBelongToAnotherOrganization().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithAutoConfirmEnabledForOtherOrg_ThrowsBadRequest(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser otherOrgUser, string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
otherOrgUser.UserId = user.Id;
|
||||
otherOrgUser.OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync([])
|
||||
.ReturnsForAnyArgs([orgUser, otherOrgUser]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Invalid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser, otherOrgUser], user),
|
||||
new OtherOrganizationDoesNotAllowOtherMembership()));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
|
||||
Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithAutoConfirmEnabledAndUserIsProvider_ThrowsBadRequest(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync([])
|
||||
.ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Invalid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user),
|
||||
new ProviderUsersCannotJoin()));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
|
||||
Assert.Equal(new ProviderUsersCannotJoin().Message, exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithAutoConfirmNotApplicable_Succeeds(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync([])
|
||||
.ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_WithAutoConfirmValidationBeforeSingleOrgPolicy_ChecksAutoConfirmFirst(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||
OrganizationUser otherOrgUser,
|
||||
[OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange - Setup conditions that would fail BOTH auto-confirm AND single org policy
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser.UserId = user.Id;
|
||||
otherOrgUser.UserId = user.Id;
|
||||
otherOrgUser.OrganizationId = Guid.NewGuid();
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs([orgUser]);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync([])
|
||||
.ReturnsForAnyArgs([orgUser, otherOrgUser]);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
singleOrgPolicy.OrganizationId = org.Id;
|
||||
sutProvider.GetDependency<IPolicyService>()
|
||||
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
|
||||
.Returns([singleOrgPolicy]);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())
|
||||
.Returns(Invalid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser, otherOrgUser], user),
|
||||
new UserCannotBelongToAnotherOrganization()));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));
|
||||
|
||||
Assert.Equal(new UserCannotBelongToAnotherOrganization().Message, exception.Message);
|
||||
Assert.NotEqual("Cannot confirm this member to the organization until they leave or remove all other organizations.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUsersAsync_WithAutoConfirmEnabled_MixedResults(
|
||||
Organization org, OrganizationUser confirmingUser,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,
|
||||
OrganizationUser otherOrgUser, User user1, User user2, User user3,
|
||||
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
org.PlanType = PlanType.EnterpriseAnnually;
|
||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||
orgUser1.UserId = user1.Id;
|
||||
orgUser2.UserId = user2.Id;
|
||||
orgUser3.UserId = user3.Id;
|
||||
otherOrgUser.UserId = user3.Id;
|
||||
otherOrgUser.OrganizationId = Guid.NewGuid();
|
||||
|
||||
var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs(orgUsers);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetManyAsync([]).ReturnsForAnyArgs([user1, user2, user3]);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByManyUsersAsync([])
|
||||
.ReturnsForAnyArgs([orgUser1, orgUser2, orgUser3, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Is<AutomaticUserConfirmationPolicyEnforcementRequest>(r => r.User.Id == user1.Id))
|
||||
.Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser1], user1)));
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Is<AutomaticUserConfirmationPolicyEnforcementRequest>(r => r.User.Id == user2.Id))
|
||||
.Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser2], user2)));
|
||||
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()
|
||||
.IsCompliantAsync(Arg.Is<AutomaticUserConfirmationPolicyEnforcementRequest>(r => r.User.Id == user3.Id))
|
||||
.Returns(Invalid(
|
||||
new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser3, otherOrgUser], user3),
|
||||
new OtherOrganizationDoesNotAllowOtherMembership()));
|
||||
|
||||
var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
Assert.Empty(result[0].Item2);
|
||||
Assert.Empty(result[1].Item2);
|
||||
Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, result[2].Item2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticUserConfirmationPolicyEnforcementValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyEnabledAndUserIsProviderMember_ReturnsProviderUsersCannotJoinError(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
ProviderUser providerUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = providerUser.UserId = user.Id;
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation
|
||||
};
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([providerUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<ProviderUsersCannotJoin>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyEnabledOnOtherOrganization_ReturnsOtherOrganizationDoesNotAllowOtherMembershipError(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrganizationUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
otherOrganizationUser.UserId = user.Id;
|
||||
|
||||
var otherOrgId = Guid.NewGuid();
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = otherOrgId, // Different from organizationUser.OrganizationId
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation
|
||||
};
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser, otherOrganizationUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OtherOrganizationDoesNotAllowOtherMembership>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyDisabledUserIsAMemberOfAnotherOrgReturnsValid(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
otherOrgUser.UserId = user.Id;
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser, otherOrgUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyEnabledUserIsAMemberOfAnotherOrg_ReturnsCannotBeMemberOfAnotherOrgError(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
otherOrgUser.UserId = user.Id;
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser, otherOrgUser],
|
||||
user);
|
||||
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserCannotBelongToAnotherOrganization>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyEnabledAndChecksConditionsInCorrectOrder_ReturnsFirstFailure(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
ProviderUser providerUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
var policyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserId = organizationUser.Id
|
||||
};
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser, otherOrgUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([providerUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<CurrentOrganizationUserIsNotPresentInRequest>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyIsEnabledNoOtherOrganizationsAndNotAProvider_ReturnsValid(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
}
|
||||
]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyDisabledForCurrentAndOtherOrg_ReturnsValid(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
otherOrgUser.UserId = organizationUser.UserId = user.Id;
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task IsCompliantAsync_WithPolicyDisabledForCurrentAndOtherOrgAndIsProvider_ReturnsValid(
|
||||
SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
ProviderUser providerUser,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
providerUser.UserId = otherOrgUser.UserId = organizationUser.UserId = user.Id;
|
||||
|
||||
var request = new AutomaticUserConfirmationPolicyEnforcementRequest(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser],
|
||||
user);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
|
||||
.Returns(new AutomaticUserConfirmationPolicyRequirement([]));
|
||||
|
||||
sutProvider.GetDependency<IProviderUserRepository>()
|
||||
.GetManyByUserAsync(user.Id)
|
||||
.Returns([providerUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.IsCompliantAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
}
|
||||
@@ -1382,4 +1382,90 @@ public class RegisterUserCommandTests
|
||||
.Received(1)
|
||||
.SendOrganizationUserWelcomeEmailAsync(user, organization.DisplayName());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_WithBlockedDomain_ThrowsException(
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization));
|
||||
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_WithOwnClaimedDomain_Succeeds(
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@company-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
// Domain is claimed by THIS organization, so it should be allowed
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", organization.Id)
|
||||
.Returns(false); // Not blocked because organization.Id is excluded
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.CreateUserAsync(user);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegisterSSOAutoProvisionedUserAsync_WithNonClaimedDomain_Succeeds(
|
||||
User user,
|
||||
Organization organization,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@unclaimed-domain.com";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("unclaimed-domain.com", organization.Id)
|
||||
.Returns(false); // Domain is not claimed by any org
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.CreateUserAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey,
|
||||
[Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
||||
{
|
||||
userAsymmetricKeys.AccountKeys = null;
|
||||
// Localize substitutions to this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
|
||||
@@ -202,6 +203,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey,
|
||||
[Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
||||
{
|
||||
userAsymmetricKeys.AccountKeys = null;
|
||||
// Localize substitutions to this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
localFactory.UpdateConfiguration("globalSettings:disableUserRegistration", "true");
|
||||
@@ -233,6 +235,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
|
||||
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
||||
{
|
||||
userAsymmetricKeys.AccountKeys = null;
|
||||
|
||||
// Localize factory to just this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -310,6 +313,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
|
||||
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism, Guid orgSponsorshipId)
|
||||
{
|
||||
userAsymmetricKeys.AccountKeys = null;
|
||||
|
||||
// Localize factory to just this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -386,6 +390,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
|
||||
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism, EmergencyAccess emergencyAccess)
|
||||
{
|
||||
userAsymmetricKeys.AccountKeys = null;
|
||||
|
||||
// Localize factory to just this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -455,6 +460,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
|
||||
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
||||
{
|
||||
userAsymmetricKeys.AccountKeys = null;
|
||||
|
||||
// Localize factory to just this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
|
||||
@@ -24,6 +24,13 @@ namespace Bit.Identity.IntegrationTest.Endpoints;
|
||||
[SutProviderCustomize]
|
||||
public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private static readonly KeysRequestModel TEST_ACCOUNT_KEYS = new KeysRequestModel
|
||||
{
|
||||
AccountKeys = null,
|
||||
PublicKey = "public-key",
|
||||
EncryptedPrivateKey = "encrypted-private-key",
|
||||
};
|
||||
|
||||
private const int SecondsInMinute = 60;
|
||||
private const int MinutesInHour = 60;
|
||||
private const int SecondsInHour = SecondsInMinute * MinutesInHour;
|
||||
@@ -64,6 +71,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypePassword_Success(RegisterFinishRequestModel requestModel)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||
|
||||
@@ -89,6 +97,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(
|
||||
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
requestModel.Email = $"{generatedUsername}@example.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -114,6 +123,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(
|
||||
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
requestModel.Email = $"{generatedUsername}@example.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -140,6 +150,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(
|
||||
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
requestModel.Email = $"{generatedUsername}@example.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -163,6 +174,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(
|
||||
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
requestModel.Email = $"{generatedUsername}@example.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -186,6 +198,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(
|
||||
OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
requestModel.Email = $"{generatedUsername}@example.com";
|
||||
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
@@ -207,6 +220,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypeRefreshToken_Success(RegisterFinishRequestModel requestModel)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel);
|
||||
@@ -229,6 +243,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
[Theory, BitAutoData, RegisterFinishRequestModelCustomize]
|
||||
public async Task TokenEndpoint_GrantTypeClientCredentials_Success(RegisterFinishRequestModel model)
|
||||
{
|
||||
model.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var user = await localFactory.RegisterNewIdentityFactoryUserAsync(model);
|
||||
|
||||
@@ -253,6 +268,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
RegisterFinishRequestModel model,
|
||||
string deviceId)
|
||||
{
|
||||
model.UserAsymmetricKeys.AccountKeys = null;
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
var server = localFactory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
@@ -456,6 +472,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(
|
||||
RegisterFinishRequestModel requestModel)
|
||||
{
|
||||
requestModel.UserAsymmetricKeys = TEST_ACCOUNT_KEYS;
|
||||
const int AmountInOneSecondAllowed = 10;
|
||||
|
||||
// The rule we are testing is 10 requests in 1 second
|
||||
|
||||
Reference in New Issue
Block a user