diff --git a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj
index a84813fd7c..4fc79f2025 100644
--- a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj
+++ b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj
@@ -9,7 +9,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs
index 7bfd13c408..4a1becc0bf 100644
--- a/src/Api/AdminConsole/Controllers/PoliciesController.cs
+++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs
@@ -16,6 +16,7 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
+using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
namespace Bit.Api.AdminConsole.Controllers;
@@ -55,17 +56,16 @@ public class PoliciesController : Controller
}
[HttpGet("{type}")]
- public async Task Get(string orgId, int type)
+ public async Task Get(Guid orgId, int type)
{
- var orgIdGuid = new Guid(orgId);
- if (!await _currentContext.ManagePolicies(orgIdGuid))
+ if (!await _currentContext.ManagePolicies(orgId))
{
throw new NotFoundException();
}
- var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgIdGuid, (PolicyType)type);
+ var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
if (policy == null)
{
- throw new NotFoundException();
+ return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false });
}
return new PolicyResponseModel(policy);
diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs
index a0c01752a8..d9dfbafc79 100644
--- a/src/Api/Auth/Controllers/AccountsController.cs
+++ b/src/Api/Auth/Controllers/AccountsController.cs
@@ -148,6 +148,13 @@ public class AccountsController : Controller
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
+ // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
+ if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
+ && await _userService.IsManagedByAnyOrganizationAsync(user.Id))
+ {
+ throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
+ }
+
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
}
@@ -165,6 +172,13 @@ public class AccountsController : Controller
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
+ // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
+ if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
+ && await _userService.IsManagedByAnyOrganizationAsync(user.Id))
+ {
+ throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
+ }
+
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
model.NewMasterPasswordHash, model.Token, model.Key);
if (result.Succeeded)
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index a1cb3e2c66..b64d46b5b1 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -148,6 +148,7 @@ public static class FeatureFlagKeys
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
public const string GeneratorToolsModernization = "generator-tools-modernization";
+ public const string NewDeviceVerification = "new-device-verification";
public static List GetAllKeys()
{
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 5174543c6f..8d8822b34a 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -21,11 +21,11 @@
-
-
+
+
-
+
@@ -35,15 +35,15 @@
-
-
+
+
-
+
-
+
@@ -58,8 +58,8 @@
-
-
+
+
diff --git a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs
index b6a0ccbedb..6dd7f42c63 100644
--- a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs
+++ b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs
@@ -1,6 +1,15 @@
-using System.Net.Http.Headers;
+using System.Net;
+using System.Net.Http.Headers;
+using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.IntegrationTest.Factories;
+using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response;
+using Bit.Core;
+using Bit.Core.Billing.Enums;
+using Bit.Core.Enums;
+using Bit.Core.Models.Data;
+using Bit.Core.Services;
+using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.Controllers;
@@ -35,4 +44,82 @@ public class AccountsControllerTest : IClassFixture
Assert.Null(content.PrivateKey);
Assert.NotNull(content.SecurityStamp);
}
+
+ [Fact]
+ public async Task PostEmailToken_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest()
+ {
+ var email = await SetupOrganizationManagedAccount();
+
+ var tokens = await _factory.LoginAsync(email);
+ var client = _factory.CreateClient();
+
+ var model = new EmailTokenRequestModel
+ {
+ NewEmail = $"{Guid.NewGuid()}@example.com",
+ MasterPasswordHash = "master_password_hash"
+ };
+
+ using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email-token")
+ {
+ Content = JsonContent.Create(model)
+ };
+ message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
+ var response = await client.SendAsync(message);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Contains("Cannot change emails for accounts owned by an organization", content);
+ }
+
+ [Fact]
+ public async Task PostEmail_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest()
+ {
+ var email = await SetupOrganizationManagedAccount();
+
+ var tokens = await _factory.LoginAsync(email);
+ var client = _factory.CreateClient();
+
+ var model = new EmailRequestModel
+ {
+ NewEmail = $"{Guid.NewGuid()}@example.com",
+ MasterPasswordHash = "master_password_hash",
+ NewMasterPasswordHash = "master_password_hash",
+ Token = "validtoken",
+ Key = "key"
+ };
+
+ using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email")
+ {
+ Content = JsonContent.Create(model)
+ };
+ message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
+ var response = await client.SendAsync(message);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ Assert.Contains("Cannot change emails for accounts owned by an organization", content);
+ }
+
+ private async Task SetupOrganizationManagedAccount()
+ {
+ _factory.SubstituteService(featureService =>
+ featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true));
+
+ // Create the owner account
+ var ownerEmail = $"{Guid.NewGuid()}@bitwarden.com";
+ await _factory.LoginWithNewAccount(ownerEmail);
+
+ // Create the organization
+ var (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
+ ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
+
+ // Create a new organization member
+ var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
+ OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });
+
+ // Add a verified domain
+ await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
+
+ return email;
+ }
}
diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs
index 83b345e784..64f719e82e 100644
--- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs
+++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs
@@ -105,4 +105,22 @@ public static class OrganizationTestHelpers
return (email, organizationUser);
}
+
+ ///
+ /// Creates a VerifiedDomain for the specified organization.
+ ///
+ public static async Task CreateVerifiedDomainAsync(ApiApplicationFactory factory, Guid organizationId, string domain)
+ {
+ var organizationDomainRepository = factory.GetService();
+
+ var verifiedDomain = new OrganizationDomain
+ {
+ OrganizationId = organizationId,
+ DomainName = domain,
+ Txt = "btw+test18383838383"
+ };
+ verifiedDomain.SetVerifiedDate();
+
+ await organizationDomainRepository.CreateAsync(verifiedDomain);
+ }
}
diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
index a16a9cb55f..4127c92eed 100644
--- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
+++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
@@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Auth.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
+using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
@@ -143,6 +144,21 @@ public class AccountsControllerTests : IDisposable
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
}
+ [Fact]
+ public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldInitiateEmailChange()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ ConfigureUserServiceToAcceptPasswordFor(user);
+ _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
+ _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
+ var newEmail = "example@user.com";
+
+ await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });
+
+ await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
+ }
+
[Fact]
public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
{
@@ -165,6 +181,22 @@ public class AccountsControllerTests : IDisposable
);
}
+ [Fact]
+ public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ ConfigureUserServiceToAcceptPasswordFor(user);
+ _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
+ _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
+
+ var result = await Assert.ThrowsAsync(
+ () => _sut.PostEmailToken(new EmailTokenRequestModel())
+ );
+
+ Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
+ }
+
[Fact]
public async Task PostEmail_ShouldChangeUserEmail()
{
@@ -178,6 +210,21 @@ public class AccountsControllerTests : IDisposable
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
}
+ [Fact]
+ public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ _userService.ChangeEmailAsync(user, default, default, default, default, default)
+ .Returns(Task.FromResult(IdentityResult.Success));
+ _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
+ _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
+
+ await _sut.PostEmail(new EmailRequestModel());
+
+ await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
+ }
+
[Fact]
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
{
@@ -201,6 +248,21 @@ public class AccountsControllerTests : IDisposable
);
}
+ [Fact]
+ public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
+ _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
+
+ var result = await Assert.ThrowsAsync(
+ () => _sut.PostEmail(new EmailRequestModel())
+ );
+
+ Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
+ }
+
[Fact]
public async Task PostVerifyEmail_ShouldSendEmailVerification()
{
diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs
index ec69104e52..77cc5ea02c 100644
--- a/test/Api.Test/Controllers/PoliciesControllerTests.cs
+++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs
@@ -3,8 +3,10 @@ using System.Text.Json;
using Bit.Api.AdminConsole.Controllers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Api.Response;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.Repositories;
+using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@@ -132,4 +134,71 @@ public class PoliciesControllerTests
// Act & Assert
await Assert.ThrowsAsync(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
}
+
+ [Theory]
+ [BitAutoData]
+ public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
+ SutProvider sutProvider, Guid orgId, Policy policy, int type)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .ManagePolicies(orgId)
+ .Returns(true);
+
+ policy.Type = (PolicyType)type;
+ policy.Enabled = true;
+ policy.Data = null;
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
+ .Returns(policy);
+
+ // Act
+ var result = await sutProvider.Sut.Get(orgId, type);
+
+ // Assert
+ Assert.IsType(result);
+ Assert.Equal(policy.Id, result.Id);
+ Assert.Equal(policy.Type, result.Type);
+ Assert.Equal(policy.Enabled, result.Enabled);
+ Assert.Equal(policy.OrganizationId, result.OrganizationId);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefaultPolicy(
+ SutProvider sutProvider, Guid orgId, int type)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .ManagePolicies(orgId)
+ .Returns(true);
+
+ sutProvider.GetDependency()
+ .GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
+ .Returns((Policy)null);
+
+ // Act
+ var result = await sutProvider.Sut.Get(orgId, type);
+
+ // Assert
+ Assert.IsType(result);
+ Assert.Equal(result.Type, (PolicyType)type);
+ Assert.False(result.Enabled);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(
+ SutProvider sutProvider, Guid orgId, int type)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .ManagePolicies(orgId)
+ .Returns(false);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() => sutProvider.Sut.Get(orgId, type));
+ }
+
}
diff --git a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj
index 10240727c7..d7a7bb9a01 100644
--- a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj
+++ b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj
@@ -10,7 +10,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj
index fd4c3be765..159572f387 100644
--- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj
+++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj
@@ -1,4 +1,4 @@
-
+
enable
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/test/IntegrationTestCommon/IntegrationTestCommon.csproj b/test/IntegrationTestCommon/IntegrationTestCommon.csproj
index 6647105609..3e8e55524b 100644
--- a/test/IntegrationTestCommon/IntegrationTestCommon.csproj
+++ b/test/IntegrationTestCommon/IntegrationTestCommon.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/util/Migrator/Migrator.csproj b/util/Migrator/Migrator.csproj
index 7893a81c0d..25f5f255a2 100644
--- a/util/Migrator/Migrator.csproj
+++ b/util/Migrator/Migrator.csproj
@@ -1,4 +1,4 @@
-
+
@@ -7,7 +7,7 @@
-
+
diff --git a/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj b/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj
index ebf0d05d8e..d316e56161 100644
--- a/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj
+++ b/util/MsSqlMigratorUtility/MsSqlMigratorUtility.csproj
@@ -1,18 +1,18 @@
-
-
-
- Exe
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Exe
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj
index 13897d6374..6366d46d3d 100644
--- a/util/Setup/Setup.csproj
+++ b/util/Setup/Setup.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -11,7 +11,7 @@
-
+