Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-18235] Add PersonalOwnershipPolicyRequirement #5439

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ private static void AddPolicyRequirements(this IServiceCollection services)
{
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
}
}
20 changes: 15 additions & 5 deletions src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -26,7 +29,8 @@
private readonly ICollectionRepository _collectionRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;

private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;

public ImportCiphersCommand(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐ŸŒฑ This constructor now has 10 dependencies. That's a good indication that this code is doing too much and a refactoring is in order.

โ“ What would you recommend changing if you could? Dream big!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent observation! I think an easy thing to do would be to separate the individual vault import and the org vault import each into its own command. From there we could extract the validation logic into a validation class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djsmith85 โ˜๐Ÿป

ICipherRepository cipherRepository,
Expand All @@ -37,7 +41,9 @@
IPushNotificationService pushService,
IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
ICurrentContext currentContext,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
Expand All @@ -48,18 +54,22 @@
_policyService = policyService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}


public async Task ImportIntoIndividualVaultAsync(
List<Folder> folders,
List<CipherDetails> ciphers,
IEnumerable<KeyValuePair<int, int>> folderRelationships,
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.");
Expand Down Expand Up @@ -119,7 +129,7 @@
{
var org = collections.Count > 0 ?
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);

Check warning on line 132 in src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs

View workflow job for this annotation

GitHub Actions / Quality scan

'ciphers.FirstOrDefault(c => c.OrganizationId.HasValue)' is null on at least one execution path. (https://rules.sonarsource.com/csharp/RSPEC-2259)
var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);

if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
Expand Down
18 changes: 14 additions & 4 deletions src/Core/Vault/Services/Implementations/CipherService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -54,7 +58,9 @@ public CipherService(
IPolicyService policyService,
GlobalSettings globalSettings,
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
ICurrentContext currentContext,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
Expand All @@ -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,
Expand Down Expand Up @@ -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<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.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,7 +21,6 @@
using NSubstitute;
using Xunit;


namespace Bit.Core.Test.Tools.ImportFeatures;

[UserCipherCustomize]
Expand Down Expand Up @@ -51,6 +53,34 @@ public async Task ImportIntoIndividualVaultAsync_Success(
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,
Expand All @@ -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<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,
Expand Down
96 changes: 96 additions & 0 deletions test/Core.Test/Vault/Services/CipherServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -104,6 +108,98 @@ public async Task SaveDetailsAsync_CorrectRevisionDate_Passes(string revisionDat
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")]
Expand Down
Loading