1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 21:23:39 +00:00

Merge branch 'main' into km/account-keys-command

This commit is contained in:
Bernd Schoolmann
2025-12-02 19:58:07 +01:00
committed by GitHub
6 changed files with 11 additions and 369 deletions

View File

@@ -757,11 +757,6 @@ public class CiphersController : Controller
}
}
if (cipher.ArchivedDate.HasValue)
{
throw new BadRequestException("Cannot move an archived item to an organization.");
}
ValidateClientVersionForFido2CredentialSupport(cipher);
var original = cipher.Clone();
@@ -1271,11 +1266,6 @@ public class CiphersController : Controller
_logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", cipher.Id, userId, cipher.EncryptedFor);
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
}
if (cipher.ArchivedDate.HasValue)
{
throw new BadRequestException("Cannot move archived items to an organization.");
}
}
var shareCiphers = new List<(CipherDetails, DateTime?)>();
@@ -1288,11 +1278,6 @@ public class CiphersController : Controller
ValidateClientVersionForFido2CredentialSupport(existingCipher);
if (existingCipher.ArchivedDate.HasValue)
{
throw new BadRequestException("Cannot move archived items to an organization.");
}
shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));
}

View File

@@ -1,7 +1,5 @@
using System.Globalization;
using Bit.Billing.Constants;
using Bit.Billing.Constants;
using Bit.Billing.Jobs;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
@@ -134,11 +132,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
}
case StripeSubscriptionStatus.Active when providerId.HasValue:
{
var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
if (!providerPortalTakeover)
{
break;
}
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
if (provider != null)
{
@@ -321,13 +314,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
Event parsedEvent,
Subscription currentSubscription)
{
var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
if (!providerPortalTakeover)
{
return;
}
var provider = await _providerRepository.GetByIdAsync(providerId);
if (provider == null)
{
@@ -343,22 +329,17 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
{
var previousSubscription = parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() as Subscription;
var updateIsSubscriptionGoingUnpaid = previousSubscription is
{
Status:
if (previousSubscription is
{
Status:
StripeSubscriptionStatus.Trialing or
StripeSubscriptionStatus.Active or
StripeSubscriptionStatus.PastDue
} && currentSubscription is
{
Status: StripeSubscriptionStatus.Unpaid,
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
};
var updateIsManualSuspensionViaMetadata = CheckForManualSuspensionViaMetadata(
previousSubscription, currentSubscription);
if (updateIsSubscriptionGoingUnpaid || updateIsManualSuspensionViaMetadata)
} && currentSubscription is
{
Status: StripeSubscriptionStatus.Unpaid,
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
})
{
if (currentSubscription.TestClock != null)
{
@@ -369,14 +350,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
var subscriptionUpdateOptions = new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) };
if (updateIsManualSuspensionViaMetadata)
{
subscriptionUpdateOptions.Metadata = new Dictionary<string, string>
{
["suspended_provider_via_webhook_at"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)
};
}
await _stripeFacade.UpdateSubscription(currentSubscription.Id, subscriptionUpdateOptions);
}
}
@@ -399,37 +372,4 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
}
}
}
private static bool CheckForManualSuspensionViaMetadata(
Subscription? previousSubscription,
Subscription currentSubscription)
{
/*
* When metadata on a subscription is updated, we'll receive an event that has:
* Previous Metadata: { newlyAddedKey: null }
* Current Metadata: { newlyAddedKey: newlyAddedValue }
*
* As such, our check for a manual suspension must ensure that the 'previous_attributes' does contain the
* 'metadata' property, but also that the "suspend_provider" key in that metadata is set to null.
*
* If we don't do this and instead do a null coalescing check on 'previous_attributes?.metadata?.TryGetValue',
* we'll end up marking an event where 'previous_attributes.metadata' = null (which could be any subscription update
* that does not update the metadata) the same as a manual suspension.
*/
const string key = "suspend_provider";
if (previousSubscription is not { Metadata: not null } ||
!previousSubscription.Metadata.TryGetValue(key, out var previousValue))
{
return false;
}
if (previousValue == null)
{
return !string.IsNullOrEmpty(
currentSubscription.Metadata.TryGetValue(key, out var currentValue) ? currentValue : null);
}
return false;
}
}

View File

@@ -188,7 +188,6 @@ public static class FeatureFlagKeys
/* Billing Team */
public const string TrialPayment = "PM-8163-trial-payment";
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure";
public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog";

View File

@@ -990,11 +990,6 @@ public class CipherService : ICipherService
throw new BadRequestException("One or more ciphers do not belong to you.");
}
if (cipher.ArchivedDate.HasValue)
{
throw new BadRequestException("Cipher cannot be shared with organization because it is archived.");
}
var attachments = cipher.GetAttachments();
var hasAttachments = attachments?.Any() ?? false;
var org = await _organizationRepository.GetByIdAsync(organizationId);

View File

@@ -1790,118 +1790,6 @@ public class CiphersControllerTests
);
}
[Theory, BitAutoData]
public async Task PutShareMany_ArchivedCipher_ThrowsBadRequestException(
Guid organizationId,
Guid userId,
CipherWithIdRequestModel request,
SutProvider<CiphersController> sutProvider)
{
request.EncryptedFor = userId;
request.OrganizationId = organizationId.ToString();
request.ArchivedDate = DateTime.UtcNow;
var model = new CipherBulkShareRequestModel
{
Ciphers = [request],
CollectionIds = [Guid.NewGuid().ToString()]
};
sutProvider.GetDependency<ICurrentContext>()
.OrganizationUser(organizationId)
.Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>()
.GetProperUserId(default)
.ReturnsForAnyArgs(userId);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PutShareMany(model)
);
Assert.Equal("Cannot move archived items to an organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task PutShareMany_ExistingCipherArchived_ThrowsBadRequestException(
Guid organizationId,
Guid userId,
CipherWithIdRequestModel request,
SutProvider<CiphersController> sutProvider)
{
// Request model does not have ArchivedDate (only the existing cipher does)
request.EncryptedFor = userId;
request.OrganizationId = organizationId.ToString();
request.ArchivedDate = null;
var model = new CipherBulkShareRequestModel
{
Ciphers = [request],
CollectionIds = [Guid.NewGuid().ToString()]
};
// The existing cipher from the repository IS archived
var existingCipher = new CipherDetails
{
Id = request.Id!.Value,
UserId = userId,
Type = CipherType.Login,
Data = JsonSerializer.Serialize(new CipherLoginData()),
ArchivedDate = DateTime.UtcNow
};
sutProvider.GetDependency<ICurrentContext>()
.OrganizationUser(organizationId)
.Returns(Task.FromResult(true));
sutProvider.GetDependency<IUserService>()
.GetProperUserId(default)
.ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(userId, withOrganizations: false)
.Returns(Task.FromResult((ICollection<CipherDetails>)[existingCipher]));
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PutShareMany(model)
);
Assert.Equal("Cannot move archived items to an organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task PutShare_ArchivedCipher_ThrowsBadRequestException(
Guid cipherId,
Guid organizationId,
User user,
CipherShareRequestModel model,
SutProvider<CiphersController> sutProvider)
{
model.Cipher.OrganizationId = organizationId.ToString();
model.Cipher.EncryptedFor = user.Id;
var cipher = new Cipher
{
Id = cipherId,
UserId = user.Id,
ArchivedDate = DateTime.UtcNow.AddDays(-1),
Type = CipherType.Login,
Data = JsonSerializer.Serialize(new CipherLoginData())
};
sutProvider.GetDependency<IUserService>()
.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(user);
sutProvider.GetDependency<ICipherRepository>()
.GetByIdAsync(cipherId)
.Returns(cipher);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationUser(organizationId)
.Returns(Task.FromResult(true));
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PutShare(cipherId, model)
);
Assert.Equal("Cannot move an archived item to an organization.", exception.Message);
}
[Theory, BitAutoData]
public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException(
SecretVerificationRequestModel model,

View File

@@ -1,7 +1,6 @@
using Bit.Billing.Constants;
using Bit.Billing.Services;
using Bit.Billing.Services.Implementations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
@@ -126,79 +125,6 @@ public class SubscriptionUpdatedHandlerTests
Arg.Is<ITrigger>(t => t.Key.Name == $"cancel-trigger-{subscriptionId}"));
}
[Fact]
public async Task
HandleAsync_UnpaidProviderSubscription_WithManualSuspensionViaMetadata_DisablesProviderAndSchedulesCancellation()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_test123";
var previousSubscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Active,
Metadata = new Dictionary<string, string>
{
["suspend_provider"] = null // This is the key part - metadata exists, but value is null
}
};
var currentSubscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }
]
},
Metadata = new Dictionary<string, string>
{
["providerId"] = providerId.ToString(),
["suspend_provider"] = "true" // Now has a value, indicating manual suspension
},
TestClock = null
};
var parsedEvent = new Event
{
Id = "evt_test123",
Type = HandledStripeWebhook.SubscriptionUpdated,
Data = new EventData
{
Object = currentSubscription,
PreviousAttributes = JObject.FromObject(previousSubscription)
}
};
var provider = new Provider { Id = providerId, Enabled = true };
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover).Returns(true);
_stripeEventService.GetSubscription(parsedEvent, true, Arg.Any<List<string>>()).Returns(currentSubscription);
_stripeEventUtilityService.GetIdsFromMetadata(currentSubscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository.GetByIdAsync(providerId).Returns(provider);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
// Verify that UpdateSubscription was called with both CancelAt and the new metadata
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
options.Metadata != null &&
options.Metadata.ContainsKey("suspended_provider_via_webhook_at")));
}
[Fact]
public async Task
HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation()
@@ -243,7 +169,6 @@ public class SubscriptionUpdatedHandlerTests
var provider = new Provider { Id = providerId, Enabled = true };
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover).Returns(true);
_stripeEventService.GetSubscription(parsedEvent, true, Arg.Any<List<string>>()).Returns(currentSubscription);
_stripeEventUtilityService.GetIdsFromMetadata(currentSubscription.Metadata)
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
@@ -256,13 +181,12 @@ public class SubscriptionUpdatedHandlerTests
Assert.False(provider.Enabled);
await _providerService.Received(1).UpdateAsync(provider);
// Verify that UpdateSubscription was called with CancelAt but WITHOUT suspension metadata
// Verify that UpdateSubscription was called with CancelAt
await _stripeFacade.Received(1).UpdateSubscription(
subscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options =>
options.CancelAt.HasValue &&
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&
(options.Metadata == null || !options.Metadata.ContainsKey("suspended_provider_via_webhook_at"))));
options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1)));
}
[Fact]
@@ -306,9 +230,6 @@ public class SubscriptionUpdatedHandlerTests
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
@@ -353,9 +274,6 @@ public class SubscriptionUpdatedHandlerTests
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
@@ -401,9 +319,6 @@ public class SubscriptionUpdatedHandlerTests
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns(provider);
@@ -416,48 +331,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WhenFeatureFlagDisabled_DoesNothing()
{
// Arrange
var providerId = Guid.NewGuid();
var subscriptionId = "sub_123";
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
var subscription = new Subscription
{
Id = subscriptionId,
Status = StripeSubscriptionStatus.Unpaid,
Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }
]
},
Metadata = new Dictionary<string, string> { { "providerId", providerId.ToString() } },
LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }
};
var parsedEvent = new Event { Data = new EventData() };
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(subscription);
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(false);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());
await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());
}
[Fact]
public async Task HandleAsync_UnpaidProviderSubscription_WhenProviderNotFound_DoesNothing()
{
@@ -489,9 +362,6 @@ public class SubscriptionUpdatedHandlerTests
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
_providerRepository.GetByIdAsync(providerId)
.Returns((Provider)null);
@@ -777,8 +647,6 @@ public class SubscriptionUpdatedHandlerTests
_stripeFacade
.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
.Returns(newSubscription);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -800,9 +668,6 @@ public class SubscriptionUpdatedHandlerTests
.Received(1)
.UpdateSubscription(newSubscription.Id,
Arg.Is<SubscriptionUpdateOptions>(options => options.CancelAtPeriodEnd == false));
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
@@ -823,8 +688,6 @@ public class SubscriptionUpdatedHandlerTests
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -843,9 +706,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
@@ -866,8 +726,6 @@ public class SubscriptionUpdatedHandlerTests
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -886,9 +744,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
@@ -909,8 +764,6 @@ public class SubscriptionUpdatedHandlerTests
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -929,9 +782,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
@@ -953,8 +803,6 @@ public class SubscriptionUpdatedHandlerTests
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -975,9 +823,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
@@ -997,8 +842,6 @@ public class SubscriptionUpdatedHandlerTests
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -1019,9 +862,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade
.DidNotReceive()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
[Fact]
@@ -1040,8 +880,6 @@ public class SubscriptionUpdatedHandlerTests
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
@@ -1062,9 +900,6 @@ public class SubscriptionUpdatedHandlerTests
await _stripeFacade
.DidNotReceive()
.UpdateSubscription(Arg.Any<string>());
_featureService
.Received(1)
.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
}
private static (Guid providerId, Subscription newSubscription, Provider provider, Event parsedEvent)