1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-04 12:40:22 -05:00

[PM-18235] Add PersonalOwnershipPolicyRequirement (#5439)

* Add PersonalOwnershipPolicyRequirement for managing personal ownership policy

* Add tests for PersonalOwnershipPolicyRequirement

* Register PersonalOwnershipPolicyRequirement in policy requirement factory

* Update ImportCiphersCommand to check PersonalOwnershipPolicyRequirement if the PolicyRequirements flag is enabled

Update unit tests

* Update CipherService to support PersonalOwnershipPolicyRequirement with feature flag

- Add support for checking personal ownership policy using PolicyRequirementQuery when feature flag is enabled
- Update CipherService constructor to inject new dependencies
- Add tests for personal vault restrictions with and without feature flag

* Clean up redundant "Arrange", "Act", and "Assert" comments in test methods

* Refactor PersonalOwnershipPolicyRequirementTests method names for clarity

- Improve test method names to better describe their purpose and behavior
- Rename methods to follow a more descriptive naming convention
- No functional changes to the test logic

* Remove commented code explaining policy check

* Refactor PersonalOwnership Policy Requirement implementation

- Add PersonalOwnershipPolicyRequirementFactory to replace static Create method
- Simplify policy requirement creation logic
- Update PolicyServiceCollectionExtensions to register new factory
- Update ImportCiphersCommand to use correct user ID parameter
- Remove redundant PersonalOwnershipPolicyRequirementTests

* Remove redundant PersonalOwnershipPolicyRequirementTests

* Remove unnecessary tests from PersonalOwnershipPolicyRequirementFactoryTests
This commit is contained in:
Rui Tomé 2025-03-26 09:40:13 +00:00 committed by GitHub
parent d563f3f78a
commit f04a3d638b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 240 additions and 10 deletions

View File

@ -0,0 +1,26 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
/// <summary>
/// Policy requirements for the Disable Personal Ownership policy.
/// </summary>
public class PersonalOwnershipPolicyRequirement : IPolicyRequirement
{
/// <summary>
/// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization.
/// </summary>
public bool DisablePersonalOwnership { get; init; }
}
public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<PersonalOwnershipPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.PersonalOwnership;
public override PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
{
var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() };
return result;
}
}

View File

@ -34,5 +34,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
}
}

View File

@ -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 class ImportCiphersCommand : IImportCiphersCommand
IPushNotificationService pushService,
IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
ICurrentContext currentContext,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
@ -48,9 +54,10 @@ public class ImportCiphersCommand : IImportCiphersCommand
_policyService = policyService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}
public async Task ImportIntoIndividualVaultAsync(
List<Folder> folders,
List<CipherDetails> ciphers,
@ -58,8 +65,11 @@ public class ImportCiphersCommand : IImportCiphersCommand
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<PersonalOwnershipPolicyRequirement>(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.");

View File

@ -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;
@ -41,6 +43,8 @@ public class CipherService : ICipherService
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;
public CipherService(
ICipherRepository cipherRepository,
@ -58,7 +62,9 @@ public class CipherService : ICipherService
GlobalSettings globalSettings,
IReferenceEventService referenceEventService,
ICurrentContext currentContext,
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
@ -76,6 +82,8 @@ public class CipherService : ICipherService
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
@ -143,9 +151,11 @@ public class CipherService : ICipherService
}
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<PersonalOwnershipPolicyRequirement>(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.");
}

View File

@ -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<PersonalOwnershipPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.DisablePersonalOwnership);
}
[Theory, BitAutoData]
public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue(
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(policies);
Assert.True(actual.DisablePersonalOwnership);
}
}

View File

@ -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 Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Tools.ImportFeatures;
[UserCipherCustomize]
@ -51,6 +53,34 @@ public class ImportCiphersAsyncCommandTests
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
}
[Theory, BitAutoData]
public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Success(
Guid importingUserId,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> sutProvider)
{
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
sutProvider.GetDependency<IFolderRepository>()
.GetManyByUserIdAsync(importingUserId)
.Returns(new List<Folder>());
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
var folderRelationships = new List<KeyValuePair<int, int>>();
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
}
[Theory, BitAutoData]
public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
List<Folder> folders,
@ -73,6 +103,32 @@ public class ImportCiphersAsyncCommandTests
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<Folder> folders,
List<CipherDetails> ciphers,
SutProvider<ImportCiphersCommand> sutProvider)
{
var userId = Guid.NewGuid();
folders.ForEach(f => f.UserId = userId);
ciphers.ForEach(c => c.UserId = userId);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<PersonalOwnershipPolicyRequirement>(userId)
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
var folderRelationships = new List<KeyValuePair<int, int>>();
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
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,

View File

@ -1,5 +1,9 @@
using System.Text.Json;
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;
@ -107,6 +111,98 @@ public class CipherServiceTests
await sutProvider.GetDependency<ICipherRepository>().Received(1).ReplaceAsync(cipherDetails);
}
[Theory]
[BitAutoData]
public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyEnabled_Throws(
SutProvider<CipherService> sutProvider,
CipherDetails cipher,
Guid savingUserId)
{
cipher.Id = default;
cipher.UserId = savingUserId;
cipher.OrganizationId = null;
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
.Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => 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<CipherService> sutProvider,
CipherDetails cipher,
Guid savingUserId)
{
cipher.Id = default;
cipher.UserId = savingUserId;
cipher.OrganizationId = null;
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
.Returns(false);
await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.CreateAsync(cipher);
}
[Theory]
[BitAutoData]
public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_Throws(
SutProvider<CipherService> sutProvider,
CipherDetails cipher,
Guid savingUserId)
{
cipher.Id = default;
cipher.UserId = savingUserId;
cipher.OrganizationId = null;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => 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<CipherService> sutProvider,
CipherDetails cipher,
Guid savingUserId)
{
cipher.Id = default;
cipher.UserId = savingUserId;
cipher.OrganizationId = null;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
await sutProvider.GetDependency<ICipherRepository>()
.Received(1)
.CreateAsync(cipher);
}
[Theory]
[BitAutoData("")]
[BitAutoData("Correct Time")]