mirror of
https://github.com/bitwarden/server
synced 2026-01-12 05:24:15 +00:00
[PM-28747] Storage limit bypass for enforce organization ownership policy (#6759)
* [PM-28747] Bypass storage limit when enforce organization data ownership policy is enabled * [PM-28747] Unit tests for storage limit enforcement * [PM-28747] Add feature flag check * [PM-28747] Simplify ignore storage limits policy enforcement * [PM-28747] Add additional test cases
This commit is contained in:
@@ -72,6 +72,17 @@ public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
return _policyDetails.Any(p => p.OrganizationId == organizationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool IgnoreStorageLimitsOnMigration(Guid organizationId)
|
||||
{
|
||||
return _policyDetails.Any(p => p.OrganizationId == organizationId &&
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed);
|
||||
}
|
||||
}
|
||||
|
||||
public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection)
|
||||
|
||||
@@ -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;
|
||||
@@ -999,20 +1000,43 @@ public class CipherService : ICipherService
|
||||
throw new BadRequestException("Could not find organization.");
|
||||
}
|
||||
|
||||
if (hasAttachments && !org.MaxStorageGb.HasValue)
|
||||
if (!await IgnoreStorageLimitsOnMigrationAsync(sharingUserId, org))
|
||||
{
|
||||
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)
|
||||
{
|
||||
throw new BadRequestException("Not enough storage available for this organization.");
|
||||
}
|
||||
}
|
||||
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the storage limit for the org should be ignored due to the Organization Data Ownership Policy
|
||||
/// </summary>
|
||||
private async Task<bool> IgnoreStorageLimitsOnMigrationAsync(Guid userId, Organization organization)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!organization.UsePolicies)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requirement = await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId);
|
||||
|
||||
return requirement.IgnoreStorageLimitsOnMigration(organization.Id);
|
||||
}
|
||||
|
||||
private async Task ValidateViewPasswordUserAsync(Cipher cipher)
|
||||
{
|
||||
if (cipher.Data == null || !cipher.OrganizationId.HasValue)
|
||||
|
||||
@@ -1190,6 +1190,7 @@ public class CipherServiceTests
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)
|
||||
.Returns(new Organization
|
||||
{
|
||||
UsePolicies = true,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
MaxStorageGb = 100
|
||||
});
|
||||
@@ -1206,6 +1207,140 @@ public class CipherServiceTests
|
||||
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;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(sharingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Enabled,
|
||||
[new PolicyDetails
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
|
||||
}]));
|
||||
|
||||
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;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ShareManyAsync_StorageLimit_Enforced_WhenFeatureFlagDisabled(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<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(false);
|
||||
|
||||
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_StorageLimit_Enforced_WhenUsePoliciesDisabled(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 = false,
|
||||
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<IFeatureService>().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true);
|
||||
|
||||
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; }
|
||||
|
||||
Reference in New Issue
Block a user