diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs
new file mode 100644
index 000000000000..6f3f017bb99f
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirement.cs
@@ -0,0 +1,26 @@
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+
+///
+/// Policy requirements for the Disable Personal Ownership policy.
+///
+public class PersonalOwnershipPolicyRequirement : IPolicyRequirement
+{
+ ///
+ /// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization.
+ ///
+ public bool DisablePersonalOwnership { get; init; }
+}
+
+public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory
+{
+ public override PolicyType PolicyType => PolicyType.PersonalOwnership;
+
+ public override PersonalOwnershipPolicyRequirement Create(IEnumerable policyDetails)
+ {
+ var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() };
+ return result;
+ }
+}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
index 6c698f9ffcaf..a5ad8aa6def2 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs
@@ -33,5 +33,6 @@ private static void AddPolicyRequirements(this IServiceCollection services)
{
services.AddScoped, DisableSendPolicyRequirementFactory>();
services.AddScoped, SendOptionsPolicyRequirementFactory>();
+ services.AddScoped, PersonalOwnershipPolicyRequirementFactory>();
}
}
diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
index 59d3e5be3486..3c58dca183bb 100644
--- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
+++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
@@ -1,10 +1,13 @@
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.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
+using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.ImportFeatures.Interfaces;
using Bit.Core.Tools.Models.Business;
@@ -26,7 +29,8 @@ public class ImportCiphersCommand : IImportCiphersCommand
private readonly ICollectionRepository _collectionRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
-
+ private readonly IPolicyRequirementQuery _policyRequirementQuery;
+ private readonly IFeatureService _featureService;
public ImportCiphersCommand(
ICipherRepository cipherRepository,
@@ -37,7 +41,9 @@ public ImportCiphersCommand(
IPushNotificationService pushService,
IPolicyService policyService,
IReferenceEventService referenceEventService,
- ICurrentContext currentContext)
+ ICurrentContext currentContext,
+ IPolicyRequirementQuery policyRequirementQuery,
+ IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
@@ -48,9 +54,10 @@ public ImportCiphersCommand(
_policyService = policyService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
+ _policyRequirementQuery = policyRequirementQuery;
+ _featureService = featureService;
}
-
public async Task ImportIntoIndividualVaultAsync(
List folders,
List ciphers,
@@ -58,8 +65,11 @@ public async Task ImportIntoIndividualVaultAsync(
Guid importingUserId)
{
// Make sure the user can save new ciphers to their personal vault
- var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
- if (anyPersonalOwnershipPolicies)
+ var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
+ ? (await _policyRequirementQuery.GetAsync(importingUserId)).DisablePersonalOwnership
+ : await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
+
+ if (isPersonalVaultRestricted)
{
throw new BadRequestException("You cannot import items into your personal vault because you are " +
"a member of an organization which forbids it.");
diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs
index 90c03df90b0a..61d22aa406e2 100644
--- a/src/Core/Vault/Services/Implementations/CipherService.cs
+++ b/src/Core/Vault/Services/Implementations/CipherService.cs
@@ -1,5 +1,7 @@
using System.Text.Json;
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.Context;
using Bit.Core.Enums;
@@ -38,6 +40,8 @@ public class CipherService : ICipherService
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
+ private readonly IPolicyRequirementQuery _policyRequirementQuery;
+ private readonly IFeatureService _featureService;
public CipherService(
ICipherRepository cipherRepository,
@@ -54,7 +58,9 @@ public CipherService(
IPolicyService policyService,
GlobalSettings globalSettings,
IReferenceEventService referenceEventService,
- ICurrentContext currentContext)
+ ICurrentContext currentContext,
+ IPolicyRequirementQuery policyRequirementQuery,
+ IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
@@ -71,6 +77,8 @@ public CipherService(
_globalSettings = globalSettings;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
+ _policyRequirementQuery = policyRequirementQuery;
+ _featureService = featureService;
}
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
@@ -138,9 +146,11 @@ public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, Date
}
else
{
- // Make sure the user can save new ciphers to their personal vault
- var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
- if (anyPersonalOwnershipPolicies)
+ var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
+ ? (await _policyRequirementQuery.GetAsync(savingUserId)).DisablePersonalOwnership
+ : await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
+
+ if (isPersonalVaultRestricted)
{
throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.");
}
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs
new file mode 100644
index 000000000000..2ce75ca61e8c
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PersonalOwnershipPolicyRequirementFactoryTests.cs
@@ -0,0 +1,31 @@
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+using Bit.Core.Test.AdminConsole.AutoFixture;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using Xunit;
+
+namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
+
+[SutProviderCustomize]
+public class PersonalOwnershipPolicyRequirementFactoryTests
+{
+ [Theory, BitAutoData]
+ public void DisablePersonalOwnership_WithNoPolicies_ReturnsFalse(SutProvider sutProvider)
+ {
+ var actual = sutProvider.Sut.Create([]);
+
+ Assert.False(actual.DisablePersonalOwnership);
+ }
+
+ [Theory, BitAutoData]
+ public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue(
+ [PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
+ SutProvider sutProvider)
+ {
+ var actual = sutProvider.Sut.Create(policies);
+
+ Assert.True(actual.DisablePersonalOwnership);
+ }
+}
diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs
index 5e7a30d8146f..89e6d152cc95 100644
--- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs
+++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs
@@ -1,10 +1,13 @@
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.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
+using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.CipherFixtures;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.ImportFeatures;
@@ -18,7 +21,6 @@
using NSubstitute;
using Xunit;
-
namespace Bit.Core.Test.Tools.ImportFeatures;
[UserCipherCustomize]
@@ -51,6 +53,34 @@ public async Task ImportIntoIndividualVaultAsync_Success(
await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId);
}
+ [Theory, BitAutoData]
+ public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Success(
+ Guid importingUserId,
+ List ciphers,
+ SutProvider sutProvider)
+ {
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+ .Returns(true);
+
+ sutProvider.GetDependency()
+ .GetAsync(importingUserId)
+ .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
+
+ sutProvider.GetDependency()
+ .GetManyByUserIdAsync(importingUserId)
+ .Returns(new List());
+
+ var folders = new List { new Folder { UserId = importingUserId } };
+
+ var folderRelationships = new List>();
+
+ await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
+
+ await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>());
+ await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId);
+ }
+
[Theory, BitAutoData]
public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
List folders,
@@ -73,6 +103,32 @@ public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
}
+ [Theory, BitAutoData]
+ public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_ThrowsBadRequestException(
+ List folders,
+ List ciphers,
+ SutProvider sutProvider)
+ {
+ var userId = Guid.NewGuid();
+ folders.ForEach(f => f.UserId = userId);
+ ciphers.ForEach(c => c.UserId = userId);
+
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+ .Returns(true);
+
+ sutProvider.GetDependency()
+ .GetAsync(userId)
+ .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
+
+ var folderRelationships = new List>();
+
+ var exception = await Assert.ThrowsAsync(() =>
+ sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId));
+
+ Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
+ }
+
[Theory, BitAutoData]
public async Task ImportIntoOrganizationalVaultAsync_Success(
Organization organization,
diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs
index 1803c980c24d..c0493f43e6ae 100644
--- a/test/Core.Test/Vault/Services/CipherServiceTests.cs
+++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs
@@ -1,4 +1,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.Entities;
using Bit.Core.Enums;
@@ -104,6 +108,98 @@ public async Task SaveDetailsAsync_CorrectRevisionDate_Passes(string revisionDat
await sutProvider.GetDependency().Received(1).ReplaceAsync(cipherDetails);
}
+ [Theory]
+ [BitAutoData]
+ public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyEnabled_Throws(
+ SutProvider sutProvider,
+ CipherDetails cipher,
+ Guid savingUserId)
+ {
+ cipher.Id = default;
+ cipher.UserId = savingUserId;
+ cipher.OrganizationId = null;
+
+ sutProvider.GetDependency()
+ .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
+ .Returns(true);
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
+ Assert.Contains("restricted from saving items to your personal vault", exception.Message);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyDisabled_Succeeds(
+ SutProvider sutProvider,
+ CipherDetails cipher,
+ Guid savingUserId)
+ {
+ cipher.Id = default;
+ cipher.UserId = savingUserId;
+ cipher.OrganizationId = null;
+
+ sutProvider.GetDependency()
+ .AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
+ .Returns(false);
+
+ await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(cipher);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_Throws(
+ SutProvider sutProvider,
+ CipherDetails cipher,
+ Guid savingUserId)
+ {
+ cipher.Id = default;
+ cipher.UserId = savingUserId;
+ cipher.OrganizationId = null;
+
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+ .Returns(true);
+
+ sutProvider.GetDependency()
+ .GetAsync(savingUserId)
+ .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
+
+ var exception = await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
+ Assert.Contains("restricted from saving items to your personal vault", exception.Message);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Succeeds(
+ SutProvider sutProvider,
+ CipherDetails cipher,
+ Guid savingUserId)
+ {
+ cipher.Id = default;
+ cipher.UserId = savingUserId;
+ cipher.OrganizationId = null;
+
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PolicyRequirements)
+ .Returns(true);
+
+ sutProvider.GetDependency()
+ .GetAsync(savingUserId)
+ .Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
+
+ await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(cipher);
+ }
+
[Theory]
[BitAutoData("")]
[BitAutoData("Correct Time")]