1
0
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:
Patrick Pimentel
2025-12-15 18:03:41 -05:00
46 changed files with 1780 additions and 873 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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]

View File

@@ -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);
}
}

View File

@@ -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>();
}

View File

@@ -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.");
}
}
}

View File

@@ -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; }
}

View File

@@ -187,7 +187,6 @@ public class Startup
services.AddBillingOperations();
services.AddReportingServices();
services.AddImportServices();
services.AddPhishingDomainServices(globalSettings);
services.AddSendServices();

View File

@@ -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>();
}
}
}

View File

@@ -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"
}
}

View File

@@ -69,9 +69,6 @@
"accessKeySecret": "SECRET",
"region": "SECRET"
},
"phishingDomain": {
"updateUrl": "SECRET"
},
"distributedIpRateLimiting": {
"enabled": true,
"maxRedisTimeoutsThreshold": 10,

View File

@@ -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);
}
}
}

View File

@@ -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
);
}
}

View File

@@ -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.");

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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>();
}
}

View File

@@ -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))

View File

@@ -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)
{

View File

@@ -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 */

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,7 +0,0 @@
namespace Bit.Core.PhishingDomainFeatures.Interfaces;
public interface ICloudPhishingDomainQuery
{
Task<List<string>> GetPhishingDomainsAsync();
Task<string> GetRemoteChecksumAsync();
}

View File

@@ -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();
}

View File

@@ -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");
}
}
}

View File

@@ -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; }

View File

@@ -28,5 +28,4 @@ public interface IGlobalSettings
string DevelopmentDirectory { get; set; }
IWebPushSettings WebPush { get; set; }
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
IPhishingDomainSettings PhishingDomain { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace Bit.Core.Settings;
public interface IPhishingDomainSettings
{
string UpdateUrl { get; set; }
string ChecksumUrl { get; set; }
}

View File

@@ -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"
}
```
```

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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