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 5 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,39 @@
๏ปฟ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>
/// 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; }

/// <summary>
/// Creates a new PersonalOwnershipPolicyRequirement.
/// </summary>
/// <param name="policyDetails">All PolicyDetails relating to the user.</param>
/// <remarks>
/// This is a <see cref="RequirementFactory{T}"/> for the PersonalOwnershipPolicyRequirement.
/// </remarks>
public static PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
{
var filteredPolicies = policyDetails
.ExemptRoles([OrganizationUserType.Owner, OrganizationUserType.Admin])
.ExemptStatus([OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked])
.ExemptProviders()
.ToList();

var result = new PersonalOwnershipPolicyRequirement
{
DisablePersonalOwnership = filteredPolicies.GetPolicyType(PolicyType.PersonalOwnership).Any()
};

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private static void AddPolicyRequirements(this IServiceCollection services)
{
// Register policy requirement factories here
services.AddPolicyRequirement(SendPolicyRequirement.Create);
services.AddPolicyRequirement(PersonalOwnershipPolicyRequirement.Create);
}

/// <summary>
Expand Down
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 @@ 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(
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 @@ public ImportCiphersCommand(
IPushNotificationService pushService,
IPolicyService policyService,
IReferenceEventService referenceEventService,
ICurrentContext currentContext)
ICurrentContext currentContext,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
Expand All @@ -48,9 +54,10 @@ public ImportCiphersCommand(
_policyService = policyService;
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}


public async Task ImportIntoIndividualVaultAsync(
List<Folder> folders,
List<CipherDetails> ciphers,
Expand All @@ -59,8 +66,11 @@ public async Task ImportIntoIndividualVaultAsync(
var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId;

// Make sure the user can save new ciphers to their personal vault
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership);
if (anyPersonalOwnershipPolicies)
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(userId.Value)).DisablePersonalOwnership
: await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, 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
17 changes: 14 additions & 3 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 @@ -139,8 +147,11 @@ public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, Date
else
{
// Make sure the user can save new ciphers to their personal vault
Copy link
Contributor

Choose a reason for hiding this comment

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

Non-blocker: I know it's not part of your scope, but can we replace this comment with a meaningful variable name? I think your variable IsPersonalVaultRestricted is a good option.

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,65 @@
๏ปฟusing AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Xunit;

namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;

public class PersonalOwnershipPolicyRequirementTests
{
[Theory, AutoData]
public void DisablePersonalOwnership_IsFalse_IfNoPersonalOwnershipPolicies(
[PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1,
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should follow the should-when naming pattern for consistency?

Consider: DisablePersonalOwnership_ShouldNotBeEnforced_WhenNoPersonalOwnershipPoliciesExist.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have renamed them to follow my usual pattern of <MethodName>_<WithSomething>_<ExpectedResult>

[PolicyDetails(PolicyType.SendOptions)] PolicyDetails otherPolicy2)
{
var actual = PersonalOwnershipPolicyRequirement.Create([otherPolicy1, otherPolicy2]);

Assert.False(actual.DisablePersonalOwnership);
}

[Theory]
[InlineAutoData(OrganizationUserType.Owner, false)]
[InlineAutoData(OrganizationUserType.Admin, false)]
[InlineAutoData(OrganizationUserType.User, true)]
[InlineAutoData(OrganizationUserType.Custom, true)]
public void DisablePersonalOwnership_TestRoles(
OrganizationUserType userType,
bool shouldBeEnforced,
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails policyDetails)
{
policyDetails.OrganizationUserType = userType;

var actual = PersonalOwnershipPolicyRequirement.Create([policyDetails]);

Assert.Equal(shouldBeEnforced, actual.DisablePersonalOwnership);
}

[Theory, AutoData]
public void DisablePersonalOwnership_Not_EnforcedAgainstProviders(
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using this name or something similar: DisablePersonalOwnership_ShouldNotBeEnforced_WhenUserIsAProvider.

[PolicyDetails(PolicyType.PersonalOwnership, isProvider: true)] PolicyDetails policyDetails)
{
var actual = PersonalOwnershipPolicyRequirement.Create([policyDetails]);

Assert.False(actual.DisablePersonalOwnership);
}

[Theory]
[InlineAutoData(OrganizationUserStatusType.Confirmed, true)]
[InlineAutoData(OrganizationUserStatusType.Accepted, true)]
[InlineAutoData(OrganizationUserStatusType.Invited, false)]
[InlineAutoData(OrganizationUserStatusType.Revoked, false)]
public void DisablePersonalOwnership_TestStatuses(
OrganizationUserStatusType userStatus,
bool shouldBeEnforced,
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails policyDetails)
{
policyDetails.OrganizationUserStatus = userStatus;

var actual = PersonalOwnershipPolicyRequirement.Create([policyDetails]);

Assert.Equal(shouldBeEnforced, 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,36 @@ 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>>();

// Act
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships);

// Assert
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 +105,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));

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