Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -72,6 +72,14 @@ public bool RequiresDefaultCollectionOnConfirm(Guid organizationId)
{
return _policyDetails.Any(p => p.OrganizationId == organizationId);
}

/// <summary>
/// Determines if the policy is enforced by the specified organization.
/// </summary>
public bool EnforcedByOrg(Guid organizationId)
{
return _policyDetails.Any(p => p.OrganizationId == organizationId);
}
}

public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection)
Expand Down
46 changes: 40 additions & 6 deletions src/Core/Vault/Services/Implementations/CipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#nullable disable

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;
Expand Down Expand Up @@ -999,18 +1000,51 @@ private async Task ValidateCipherCanBeShared(
throw new BadRequestException("Could not find organization.");
}

if (hasAttachments && !org.MaxStorageGb.HasValue)
// Ignore storage limits if the organization has data ownership policy enabled.
// Allows users to seamlessly migrate their data into the organization without being blocked by storage limits.
// Organization admins will need to manage storage after migration should overages occur.
var ignoreStorageLimits = await OrganizationDataOwnershipPolicyEnabledAsync(sharingUserId, org);

if (!ignoreStorageLimits)
{
throw new BadRequestException("This organization cannot use attachments.");
if (hasAttachments && !org.MaxStorageGb.HasValue)
{
throw new BadRequestException("This organization cannot use attachments.");
}

var storageAdjustment = attachments?.Sum(a => a.Value.Size) ?? 0;
if (org.StorageBytesRemaining() < storageAdjustment)
{
throw new BadRequestException("Not enough storage available for this organization.");
}
}

var storageAdjustment = attachments?.Sum(a => a.Value.Size) ?? 0;
if (org.StorageBytesRemaining() < storageAdjustment)
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
}

/// <summary>
/// Checks if the Organization Data Ownership Policy is enabled for the given user and organization.
/// </summary>
private async Task<bool> OrganizationDataOwnershipPolicyEnabledAsync(Guid userId, Organization organization)
{
if (!organization.UsePolicies)
{
throw new BadRequestException("Not enough storage available for this organization.");
return false;
}

ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
var requirement = await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId);

return requirement.State == OrganizationDataOwnershipState.Enabled &&
requirement.EnforcedByOrg(organization.Id);
}

var policies =
await _policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.OrganizationDataOwnership);

return policies.Any(p => p.OrganizationId == organization.Id);
}

private async Task ValidateViewPasswordUserAsync(Cipher cipher)
Expand Down
154 changes: 154 additions & 0 deletions test/Core.Test/Vault/Services/CipherServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand Down Expand Up @@ -1190,6 +1191,7 @@ public async Task ShareManyAsync_PaidOrgWithAttachment_Passes(SutProvider<Cipher
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
UsePolicies = true,
PlanType = PlanType.EnterpriseAnnually,
MaxStorageGb = 100
});
Expand All @@ -1206,6 +1208,158 @@ await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAs
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}

/// <summary>
/// Can be removed after <see cref="FeatureFlagKeys.PolicyRequirements" /> is removed.
/// </summary>
[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimitBypass_Passes_LegacyPolicyService(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
ciphers.FirstOrDefault().Attachments =
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";

var cipherInfos = ciphers.Select(c => (c,
(DateTime?)c.RevisionDate));
var sharingUserId = ciphers.First().UserId.Value;

sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(sharingUserId, PolicyType.OrganizationDataOwnership)
.Returns(new List<OrganizationUserPolicyDetails>()
{
new()
{
OrganizationId = organizationId,
PolicyType = PolicyType.OrganizationDataOwnership,
PolicyEnabled = true
}
});

await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}

/// <summary>
/// Can be removed after <see cref="FeatureFlagKeys.PolicyRequirements" /> is removed.
/// </summary>
[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimit_Enforced_LegacyPolicyService(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
ciphers.FirstOrDefault().Attachments =
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";

var cipherInfos = ciphers.Select(c => (c,
(DateTime?)c.RevisionDate));
var sharingUserId = ciphers.First().UserId.Value;

sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(sharingUserId, PolicyType.OrganizationDataOwnership)
.Returns([]);

var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
);
Assert.Contains("Not enough storage available for this organization.", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}

[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimitBypass_Passes(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
ciphers.FirstOrDefault().Attachments =
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";

var cipherInfos = ciphers.Select(c => (c,
(DateTime?)c.RevisionDate));
var sharingUserId = ciphers.First().UserId.Value;

// Remove after FeatureFlagKeys.PolicyRequirements is removed.
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);

sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(
OrganizationDataOwnershipState.Enabled,
[new PolicyDetails
{
OrganizationId = organizationId,
PolicyType = PolicyType.OrganizationDataOwnership
}]));

await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId);
await sutProvider.GetDependency<ICipherRepository>().Received(1).UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}

[Theory, BitAutoData]
public async Task ShareManyAsync_StorageLimit_Enforced(SutProvider<CipherService> sutProvider,
IEnumerable<CipherDetails> ciphers, Guid organizationId, List<Guid> collectionIds)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
.Returns(new Organization
{
Id = organizationId,
PlanType = PlanType.EnterpriseAnnually,
UsePolicies = true,
MaxStorageGb = 3,
Storage = 3221225472 // 3 GB used, so 0 remaining
});
ciphers.FirstOrDefault().Attachments =
"{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\","
+ "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}";

var cipherInfos = ciphers.Select(c => (c,
(DateTime?)c.RevisionDate));
var sharingUserId = ciphers.First().UserId.Value;

// Remove after FeatureFlagKeys.PolicyRequirements is removed.
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);

sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));

var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId)
);
Assert.Contains("Not enough storage available for this organization.", exception.Message);
await sutProvider.GetDependency<ICipherRepository>().DidNotReceive().UpdateCiphersAsync(sharingUserId,
Arg.Is<IEnumerable<Cipher>>(arg => !arg.Except(ciphers).Any()));
}

private class SaveDetailsAsyncDependencies
{
public CipherDetails CipherDetails { get; set; }
Expand Down
Loading