From 794240f108783ddc169615820ba426eccdc2d6ca Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:58:57 -0600 Subject: [PATCH 01/68] [PM-29732] (fix) storage job no longer ignores trialing and past_due statuses (#6737) --- .../Jobs/ReconcileAdditionalStorageJob.cs | 40 ++--- .../ReconcileAdditionalStorageJobTests.cs | 152 +++++++++++++++++- 2 files changed, 163 insertions(+), 29 deletions(-) diff --git a/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs b/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs index d891fc18ff..312ed3122b 100644 --- a/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs +++ b/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs @@ -39,15 +39,11 @@ public class ReconcileAdditionalStorageJob( logger.LogInformation("Starting ReconcileAdditionalStorageJob (live mode: {LiveMode})", liveMode); var priceIds = new[] { _storageGbMonthlyPriceId, _storageGbAnnuallyPriceId, _personalStorageGbAnnuallyPriceId }; + var stripeStatusesToProcess = new[] { StripeConstants.SubscriptionStatus.Active, StripeConstants.SubscriptionStatus.Trialing, StripeConstants.SubscriptionStatus.PastDue }; foreach (var priceId in priceIds) { - var options = new SubscriptionListOptions - { - Limit = 100, - Status = StripeConstants.SubscriptionStatus.Active, - Price = priceId - }; + var options = new SubscriptionListOptions { Limit = 100, Price = priceId }; await foreach (var subscription in stripeFacade.ListSubscriptionsAutoPagingAsync(options)) { @@ -64,7 +60,7 @@ public class ReconcileAdditionalStorageJob( failures.Count > 0 ? $", Failures: {Environment.NewLine}{string.Join(Environment.NewLine, failures)}" : string.Empty - ); + ); return; } @@ -73,6 +69,12 @@ public class ReconcileAdditionalStorageJob( continue; } + if (!stripeStatusesToProcess.Contains(subscription.Status)) + { + logger.LogInformation("Skipping subscription with unsupported status: {SubscriptionId} - {Status}", subscription.Id, subscription.Status); + continue; + } + logger.LogInformation("Processing subscription: {SubscriptionId}", subscription.Id); subscriptionsFound++; @@ -133,7 +135,7 @@ public class ReconcileAdditionalStorageJob( failures.Count > 0 ? $", Failures: {Environment.NewLine}{string.Join(Environment.NewLine, failures)}" : string.Empty - ); + ); } private SubscriptionUpdateOptions? BuildSubscriptionUpdateOptions( @@ -145,15 +147,7 @@ public class ReconcileAdditionalStorageJob( return null; } - var updateOptions = new SubscriptionUpdateOptions - { - ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, - Metadata = new Dictionary - { - [StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString("o") - }, - Items = [] - }; + var updateOptions = new SubscriptionUpdateOptions { ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, Metadata = new Dictionary { [StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString("o") }, Items = [] }; var hasUpdates = false; @@ -172,11 +166,7 @@ public class ReconcileAdditionalStorageJob( newQuantity, item.Price.Id); - updateOptions.Items.Add(new SubscriptionItemOptions - { - Id = item.Id, - Quantity = newQuantity - }); + updateOptions.Items.Add(new SubscriptionItemOptions { Id = item.Id, Quantity = newQuantity }); } else { @@ -185,11 +175,7 @@ public class ReconcileAdditionalStorageJob( currentQuantity, item.Price.Id); - updateOptions.Items.Add(new SubscriptionItemOptions - { - Id = item.Id, - Deleted = true - }); + updateOptions.Items.Add(new SubscriptionItemOptions { Id = item.Id, Deleted = true }); } } diff --git a/test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs b/test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs index deb164f232..b3540246b0 100644 --- a/test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs +++ b/test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs @@ -62,7 +62,7 @@ public class ReconcileAdditionalStorageJobTests // Assert _stripeFacade.Received(3).ListSubscriptionsAutoPagingAsync( - Arg.Is(o => o.Status == "active")); + Arg.Is(o => o.Limit == 100)); } #endregion @@ -553,6 +553,152 @@ public class ReconcileAdditionalStorageJobTests #endregion + #region Subscription Status Filtering Tests + + [Fact] + public async Task Execute_ActiveStatusSubscription_ProcessesSubscription() + { + // Arrange + var context = CreateJobExecutionContext(); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); + + var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Active); + + _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any()) + .Returns(AsyncEnumerable.Create(subscription)); + _stripeFacade.UpdateSubscription(Arg.Any(), Arg.Any()) + .Returns(subscription); + + // Act + await _sut.Execute(context); + + // Assert + await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any()); + } + + [Fact] + public async Task Execute_TrialingStatusSubscription_ProcessesSubscription() + { + // Arrange + var context = CreateJobExecutionContext(); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); + + var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Trialing); + + _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any()) + .Returns(AsyncEnumerable.Create(subscription)); + _stripeFacade.UpdateSubscription(Arg.Any(), Arg.Any()) + .Returns(subscription); + + // Act + await _sut.Execute(context); + + // Assert + await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any()); + } + + [Fact] + public async Task Execute_PastDueStatusSubscription_ProcessesSubscription() + { + // Arrange + var context = CreateJobExecutionContext(); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); + + var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.PastDue); + + _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any()) + .Returns(AsyncEnumerable.Create(subscription)); + _stripeFacade.UpdateSubscription(Arg.Any(), Arg.Any()) + .Returns(subscription); + + // Act + await _sut.Execute(context); + + // Assert + await _stripeFacade.Received(1).UpdateSubscription("sub_123", Arg.Any()); + } + + [Fact] + public async Task Execute_CanceledStatusSubscription_SkipsSubscription() + { + // Arrange + var context = CreateJobExecutionContext(); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); + + var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Canceled); + + _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any()) + .Returns(AsyncEnumerable.Create(subscription)); + + // Act + await _sut.Execute(context); + + // Assert + await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!); + } + + [Fact] + public async Task Execute_IncompleteStatusSubscription_SkipsSubscription() + { + // Arrange + var context = CreateJobExecutionContext(); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); + + var subscription = CreateSubscription("sub_123", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Incomplete); + + _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any()) + .Returns(AsyncEnumerable.Create(subscription)); + + // Act + await _sut.Execute(context); + + // Assert + await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!); + } + + [Fact] + public async Task Execute_MixedSubscriptionStatuses_OnlyProcessesValidStatuses() + { + // Arrange + var context = CreateJobExecutionContext(); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true); + _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); + + var activeSubscription = CreateSubscription("sub_active", "storage-gb-monthly", quantity: 10, status: StripeConstants.SubscriptionStatus.Active); + var trialingSubscription = CreateSubscription("sub_trialing", "storage-gb-monthly", quantity: 8, status: StripeConstants.SubscriptionStatus.Trialing); + var pastDueSubscription = CreateSubscription("sub_pastdue", "storage-gb-monthly", quantity: 6, status: StripeConstants.SubscriptionStatus.PastDue); + var canceledSubscription = CreateSubscription("sub_canceled", "storage-gb-monthly", quantity: 5, status: StripeConstants.SubscriptionStatus.Canceled); + var incompleteSubscription = CreateSubscription("sub_incomplete", "storage-gb-monthly", quantity: 4, status: StripeConstants.SubscriptionStatus.Incomplete); + + _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any()) + .Returns(AsyncEnumerable.Create(activeSubscription, trialingSubscription, pastDueSubscription, canceledSubscription, incompleteSubscription)); + _stripeFacade.UpdateSubscription(Arg.Any(), Arg.Any()) + .Returns(callInfo => callInfo.Arg() switch + { + "sub_active" => activeSubscription, + "sub_trialing" => trialingSubscription, + "sub_pastdue" => pastDueSubscription, + _ => null + }); + + // Act + await _sut.Execute(context); + + // Assert + await _stripeFacade.Received(1).UpdateSubscription("sub_active", Arg.Any()); + await _stripeFacade.Received(1).UpdateSubscription("sub_trialing", Arg.Any()); + await _stripeFacade.Received(1).UpdateSubscription("sub_pastdue", Arg.Any()); + await _stripeFacade.DidNotReceive().UpdateSubscription("sub_canceled", Arg.Any()); + await _stripeFacade.DidNotReceive().UpdateSubscription("sub_incomplete", Arg.Any()); + } + + #endregion + #region Cancellation Tests [Fact] @@ -598,7 +744,8 @@ public class ReconcileAdditionalStorageJobTests string id, string priceId, long? quantity = null, - Dictionary? metadata = null) + Dictionary? metadata = null, + string status = StripeConstants.SubscriptionStatus.Active) { var price = new Price { Id = priceId }; var item = new SubscriptionItem @@ -611,6 +758,7 @@ public class ReconcileAdditionalStorageJobTests return new Subscription { Id = id, + Status = status, Metadata = metadata, Items = new StripeList { From 04efe402bebc5aef3b06cd40c37474be0d114634 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:12:56 -0600 Subject: [PATCH 02/68] [PM-28128] Create transaction for bank transfer charges (#6691) * Create transaction for charges that were the result of a bank transfer * Claude feedback * Run dotnet format --- .../Services/IStripeEventUtilityService.cs | 2 +- src/Billing/Services/IStripeFacade.cs | 6 ++ .../Implementations/ChargeRefundedHandler.cs | 2 +- .../Implementations/ChargeSucceededHandler.cs | 2 +- .../StripeEventUtilityService.cs | 71 ++++++++++++++++++- .../Services/Implementations/StripeFacade.cs | 8 +++ 6 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/Billing/Services/IStripeEventUtilityService.cs b/src/Billing/Services/IStripeEventUtilityService.cs index a5f536ad11..058f56c887 100644 --- a/src/Billing/Services/IStripeEventUtilityService.cs +++ b/src/Billing/Services/IStripeEventUtilityService.cs @@ -36,7 +36,7 @@ public interface IStripeEventUtilityService /// /// /// /// - Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId); + Task FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId); /// /// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe. diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs index f821eeed5f..c7073b9cf9 100644 --- a/src/Billing/Services/IStripeFacade.cs +++ b/src/Billing/Services/IStripeFacade.cs @@ -20,6 +20,12 @@ public interface IStripeFacade RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + IAsyncEnumerable GetCustomerCashBalanceTransactions( + string customerId, + CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default); + Task UpdateCustomer( string customerId, CustomerUpdateOptions customerUpdateOptions = null, diff --git a/src/Billing/Services/Implementations/ChargeRefundedHandler.cs b/src/Billing/Services/Implementations/ChargeRefundedHandler.cs index 905491b6c5..8cc3cb2ce6 100644 --- a/src/Billing/Services/Implementations/ChargeRefundedHandler.cs +++ b/src/Billing/Services/Implementations/ChargeRefundedHandler.cs @@ -38,7 +38,7 @@ public class ChargeRefundedHandler : IChargeRefundedHandler { // Attempt to create a transaction for the charge if it doesn't exist var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge); - var tx = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId); + var tx = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId); try { parentTransaction = await _transactionRepository.CreateAsync(tx); diff --git a/src/Billing/Services/Implementations/ChargeSucceededHandler.cs b/src/Billing/Services/Implementations/ChargeSucceededHandler.cs index bd8ea7def2..20c4dcfa98 100644 --- a/src/Billing/Services/Implementations/ChargeSucceededHandler.cs +++ b/src/Billing/Services/Implementations/ChargeSucceededHandler.cs @@ -46,7 +46,7 @@ public class ChargeSucceededHandler : IChargeSucceededHandler return; } - var transaction = _stripeEventUtilityService.FromChargeToTransaction(charge, organizationId, userId, providerId); + var transaction = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId); if (!transaction.PaymentMethodType.HasValue) { _logger.LogWarning("Charge success from unsupported source/method. {ChargeId}", charge.Id); diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index ba3e79abc6..53512427c0 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -124,7 +124,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService /// /// /// /// - public Transaction FromChargeToTransaction(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId) + public async Task FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId) { var transaction = new Transaction { @@ -209,6 +209,24 @@ public class StripeEventUtilityService : IStripeEventUtilityService transaction.PaymentMethodType = PaymentMethodType.BankAccount; transaction.Details = $"ACH => {achCreditTransfer.BankName}, {achCreditTransfer.AccountNumber}"; } + else if (charge.PaymentMethodDetails.CustomerBalance != null) + { + var bankTransferType = await GetFundingBankTransferTypeAsync(charge); + + if (!string.IsNullOrEmpty(bankTransferType)) + { + transaction.PaymentMethodType = PaymentMethodType.BankAccount; + transaction.Details = bankTransferType switch + { + "eu_bank_transfer" => "EU Bank Transfer", + "gb_bank_transfer" => "GB Bank Transfer", + "jp_bank_transfer" => "JP Bank Transfer", + "mx_bank_transfer" => "MX Bank Transfer", + "us_bank_transfer" => "US Bank Transfer", + _ => "Bank Transfer" + }; + } + } break; } @@ -406,4 +424,55 @@ public class StripeEventUtilityService : IStripeEventUtilityService throw; } } + + /// + /// Retrieves the bank transfer type that funded a charge paid via customer balance. + /// + /// The charge to analyze. + /// + /// The bank transfer type (e.g., "us_bank_transfer", "eu_bank_transfer") if the charge was funded + /// by a bank transfer via customer balance, otherwise null. + /// + private async Task GetFundingBankTransferTypeAsync(Charge charge) + { + if (charge is not + { + CustomerId: not null, + PaymentIntentId: not null, + PaymentMethodDetails: { Type: "customer_balance" } + }) + { + return null; + } + + var cashBalanceTransactions = _stripeFacade.GetCustomerCashBalanceTransactions(charge.CustomerId); + + string bankTransferType = null; + var matchingPaymentIntentFound = false; + + await foreach (var cashBalanceTransaction in cashBalanceTransactions) + { + switch (cashBalanceTransaction) + { + case { Type: "funded", Funded: not null }: + { + bankTransferType = cashBalanceTransaction.Funded.BankTransfer.Type; + break; + } + case { Type: "applied_to_payment", AppliedToPayment: not null } + when cashBalanceTransaction.AppliedToPayment.PaymentIntentId == charge.PaymentIntentId: + { + matchingPaymentIntentFound = true; + break; + } + } + + if (matchingPaymentIntentFound && !string.IsNullOrEmpty(bankTransferType)) + { + return bankTransferType; + } + } + + return null; + } } diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs index bb72091bc6..49cde981cd 100644 --- a/src/Billing/Services/Implementations/StripeFacade.cs +++ b/src/Billing/Services/Implementations/StripeFacade.cs @@ -11,6 +11,7 @@ public class StripeFacade : IStripeFacade { private readonly ChargeService _chargeService = new(); private readonly CustomerService _customerService = new(); + private readonly CustomerCashBalanceTransactionService _customerCashBalanceTransactionService = new(); private readonly EventService _eventService = new(); private readonly InvoiceService _invoiceService = new(); private readonly PaymentMethodService _paymentMethodService = new(); @@ -41,6 +42,13 @@ public class StripeFacade : IStripeFacade CancellationToken cancellationToken = default) => await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken); + public IAsyncEnumerable GetCustomerCashBalanceTransactions( + string customerId, + CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + => _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken); + public async Task UpdateCustomer( string customerId, CustomerUpdateOptions customerUpdateOptions = null, From 00c4ac2df1aef3e7564424cfed0372e69167f338 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Wed, 17 Dec 2025 09:19:37 -0600 Subject: [PATCH 03/68] [PM-29840] Correcting Auto-Confirm Org Accept User Flow (#6740) * Populating org user userId and adding to allOrgUser list. * Having validator check organization user existence based off email or userid. --- .../OrganizationUsers/AcceptOrgUserCommand.cs | 4 +++- .../AutomaticUserConfirmationPolicyEnforcementValidator.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index c763cc0cc2..50f194b578 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -270,7 +270,9 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand ICollection allOrgUsers, User user) { var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync( - new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId, allOrgUsers, user))) + new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId, + allOrgUsers.Append(orgUser), + user))) .Match( error => error.Message, _ => string.Empty diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementValidator.cs index 633b84d2b9..e5c980ea24 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementValidator.cs @@ -19,7 +19,8 @@ public class AutomaticUserConfirmationPolicyEnforcementValidator( var currentOrganizationUser = request.AllOrganizationUsers .FirstOrDefault(x => x.OrganizationId == request.OrganizationId - && x.UserId == request.User.Id); + // invited users do not have a userId but will have email + && (x.UserId == request.User.Id || x.Email == request.User.Email)); if (currentOrganizationUser is null) { From bbe682dae92097fd14ebc78d7f23c80a0eea46ef Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:34:12 -0500 Subject: [PATCH 04/68] refactor(IdentityTokenResponse): [Auth/PM-3287] Remove deprecated resetMasterPassword property from IdentityTokenResponse (#6676) --- .../RequestValidators/BaseRequestValidator.cs | 1 - .../CustomTokenRequestValidator.cs | 17 ----------------- .../Endpoints/IdentityServerTests.cs | 1 - 3 files changed, 19 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 0bdf1d89c2..b0f3311b2c 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -671,7 +671,6 @@ public abstract class BaseRequestValidator where T : class customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); - customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); customResponse.Add("Kdf", (byte)user.Kdf); customResponse.Add("KdfIterations", user.KdfIterations); customResponse.Add("KdfMemory", user.KdfMemory); diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 38a4813ecd..5eee4199b2 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -4,7 +4,6 @@ using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.IdentityServer; -using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -155,23 +154,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator var root = body.RootElement; AssertRefreshTokenExists(root); AssertHelper.AssertJsonProperty(root, "ForcePasswordReset", JsonValueKind.False); - AssertHelper.AssertJsonProperty(root, "ResetMasterPassword", JsonValueKind.False); var kdf = AssertHelper.AssertJsonProperty(root, "Kdf", JsonValueKind.Number).GetInt32(); Assert.Equal(0, kdf); var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32(); From 886ba9ae6d4da5d8796658cf7c7226c2cd7ec65e Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:43:53 -0500 Subject: [PATCH 05/68] Refactor IntegrationHandlerResult to provide more detail around failures (#6736) * Refactor IntegrationHandlerResult to provide more detail around failures * ServiceUnavailable now retryable, more explicit http status handling, more consistency with different handlers, additional xmldocs * Address PR feedback --- .../IntegrationFailureCategory.cs | 37 +++++ .../IntegrationHandlerResult.cs | 82 ++++++++++- .../Services/IIntegrationHandler.cs | 117 ++++++++++------ ...ureServiceBusIntegrationListenerService.cs | 11 ++ .../RabbitMqIntegrationListenerService.cs | 22 ++- .../SlackIntegrationHandler.cs | 68 +++++++--- .../TeamsIntegrationHandler.cs | 51 +++++-- .../IntegrationHandlerResultTests.cs | 128 ++++++++++++++++++ ...rviceBusIntegrationListenerServiceTests.cs | 35 +++-- .../DatadogIntegrationHandlerTests.cs | 2 +- .../Services/IntegrationHandlerTests.cs | 108 ++++++++++++++- ...RabbitMqIntegrationListenerServiceTests.cs | 25 ++-- .../Services/SlackIntegrationHandlerTests.cs | 4 +- .../Services/TeamsIntegrationHandlerTests.cs | 82 ++++++++++- .../WebhookIntegrationHandlerTests.cs | 4 +- 15 files changed, 663 insertions(+), 113 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs create mode 100644 test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs new file mode 100644 index 0000000000..544e671d51 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs @@ -0,0 +1,37 @@ +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +/// +/// Categories of event integration failures used for classification and retry logic. +/// +public enum IntegrationFailureCategory +{ + /// + /// Service is temporarily unavailable (503, upstream outage, maintenance). + /// + ServiceUnavailable, + + /// + /// Authentication failed (401, 403, invalid_auth, token issues). + /// + AuthenticationFailed, + + /// + /// Configuration error (invalid config, channel_not_found, etc.). + /// + ConfigurationError, + + /// + /// Rate limited (429, rate_limited). + /// + RateLimited, + + /// + /// Transient error (timeouts, 500, network errors). + /// + TransientError, + + /// + /// Permanent failure unrelated to authentication/config (e.g., unrecoverable payload/format issue). + /// + PermanentFailure +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs index 8db054561b..375f2489cb 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs @@ -1,16 +1,84 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +/// +/// Represents the result of an integration handler operation, including success status, +/// failure categorization, and retry metadata. Use the factory method +/// for successful operations or for failures with automatic retry-ability +/// determination based on the failure category. +/// public class IntegrationHandlerResult { - public IntegrationHandlerResult(bool success, IIntegrationMessage message) + /// + /// True if the integration send succeeded, false otherwise. + /// + public bool Success { get; } + + /// + /// The integration message that was processed. + /// + public IIntegrationMessage Message { get; } + + /// + /// Optional UTC date/time indicating when a failed operation should be retried. + /// Will be used by the retry queue to delay re-sending the message. + /// Usually set based on the Retry-After header from rate-limited responses. + /// + public DateTime? DelayUntilDate { get; private init; } + + /// + /// Category of the failure. Null for successful results. + /// + public IntegrationFailureCategory? Category { get; private init; } + + /// + /// Detailed failure reason or error message. Empty for successful results. + /// + public string? FailureReason { get; private init; } + + /// + /// Indicates whether the operation is retryable. + /// Computed from the failure category. + /// + public bool Retryable => Category switch + { + IntegrationFailureCategory.RateLimited => true, + IntegrationFailureCategory.TransientError => true, + IntegrationFailureCategory.ServiceUnavailable => true, + IntegrationFailureCategory.AuthenticationFailed => false, + IntegrationFailureCategory.ConfigurationError => false, + IntegrationFailureCategory.PermanentFailure => false, + null => false, + _ => false + }; + + /// + /// Creates a successful result. + /// + public static IntegrationHandlerResult Succeed(IIntegrationMessage message) + { + return new IntegrationHandlerResult(success: true, message: message); + } + + /// + /// Creates a failed result with a failure category and reason. + /// + public static IntegrationHandlerResult Fail( + IIntegrationMessage message, + IntegrationFailureCategory category, + string failureReason, + DateTime? delayUntil = null) + { + return new IntegrationHandlerResult(success: false, message: message) + { + Category = category, + FailureReason = failureReason, + DelayUntilDate = delayUntil + }; + } + + private IntegrationHandlerResult(bool success, IIntegrationMessage message) { Success = success; Message = message; } - - public bool Success { get; set; } = false; - public bool Retryable { get; set; } = false; - public IIntegrationMessage Message { get; set; } - public DateTime? DelayUntilDate { get; set; } - public string FailureReason { get; set; } = string.Empty; } diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs index bb10dc01b9..c36081cb52 100644 --- a/src/Core/AdminConsole/Services/IIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs @@ -29,46 +29,87 @@ public abstract class IntegrationHandlerBase : IIntegrationHandler IntegrationMessage message, TimeProvider timeProvider) { - var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); - - if (response.IsSuccessStatusCode) return result; - - switch (response.StatusCode) + if (response.IsSuccessStatusCode) { - case HttpStatusCode.TooManyRequests: - case HttpStatusCode.RequestTimeout: - case HttpStatusCode.InternalServerError: - case HttpStatusCode.BadGateway: - case HttpStatusCode.ServiceUnavailable: - case HttpStatusCode.GatewayTimeout: - result.Retryable = true; - result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}"; - - if (response.Headers.TryGetValues("Retry-After", out var values)) - { - var value = values.FirstOrDefault(); - if (int.TryParse(value, out var seconds)) - { - // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. - result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime; - } - else if (DateTimeOffset.TryParseExact(value, - "r", // "r" is the round-trip format: RFC1123 - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, - out var retryDate)) - { - // Retry-after was specified as a date. Adjust DelayUntilDate to the specified date. - result.DelayUntilDate = retryDate.UtcDateTime; - } - } - break; - default: - result.Retryable = false; - result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; - break; + return IntegrationHandlerResult.Succeed(message); } - return result; + var category = ClassifyHttpStatusCode(response.StatusCode); + var failureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; + + if (category is not (IntegrationFailureCategory.RateLimited + or IntegrationFailureCategory.TransientError + or IntegrationFailureCategory.ServiceUnavailable) || + !response.Headers.TryGetValues("Retry-After", out var values) + ) + { + return IntegrationHandlerResult.Fail(message: message, category: category, failureReason: failureReason); + } + + // Handle Retry-After header for rate-limited and retryable errors + DateTime? delayUntil = null; + var value = values.FirstOrDefault(); + if (int.TryParse(value, out var seconds)) + { + // Retry-after was specified in seconds + delayUntil = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime; + } + else if (DateTimeOffset.TryParseExact(value, + "r", // "r" is the round-trip format: RFC1123 + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var retryDate)) + { + // Retry-after was specified as a date + delayUntil = retryDate.UtcDateTime; + } + + return IntegrationHandlerResult.Fail( + message, + category, + failureReason, + delayUntil + ); + } + + /// + /// Classifies an as an to drive + /// retry behavior and operator-facing failure reporting. + /// + /// The HTTP status code. + /// The corresponding . + protected static IntegrationFailureCategory ClassifyHttpStatusCode(HttpStatusCode statusCode) + { + var explicitCategory = statusCode switch + { + HttpStatusCode.Unauthorized => IntegrationFailureCategory.AuthenticationFailed, + HttpStatusCode.Forbidden => IntegrationFailureCategory.AuthenticationFailed, + HttpStatusCode.NotFound => IntegrationFailureCategory.ConfigurationError, + HttpStatusCode.Gone => IntegrationFailureCategory.ConfigurationError, + HttpStatusCode.MovedPermanently => IntegrationFailureCategory.ConfigurationError, + HttpStatusCode.TemporaryRedirect => IntegrationFailureCategory.ConfigurationError, + HttpStatusCode.PermanentRedirect => IntegrationFailureCategory.ConfigurationError, + HttpStatusCode.TooManyRequests => IntegrationFailureCategory.RateLimited, + HttpStatusCode.RequestTimeout => IntegrationFailureCategory.TransientError, + HttpStatusCode.InternalServerError => IntegrationFailureCategory.TransientError, + HttpStatusCode.BadGateway => IntegrationFailureCategory.TransientError, + HttpStatusCode.GatewayTimeout => IntegrationFailureCategory.TransientError, + HttpStatusCode.ServiceUnavailable => IntegrationFailureCategory.ServiceUnavailable, + HttpStatusCode.NotImplemented => IntegrationFailureCategory.PermanentFailure, + _ => (IntegrationFailureCategory?)null + }; + + if (explicitCategory is not null) + { + return explicitCategory.Value; + } + + return (int)statusCode switch + { + >= 300 and <= 399 => IntegrationFailureCategory.ConfigurationError, + >= 400 and <= 499 => IntegrationFailureCategory.ConfigurationError, + >= 500 and <= 599 => IntegrationFailureCategory.ServiceUnavailable, + _ => IntegrationFailureCategory.ServiceUnavailable + }; } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs index 633a53296b..c97c5f7efe 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs @@ -85,6 +85,17 @@ public class AzureServiceBusIntegrationListenerService : Backgro { // Non-recoverable failure or exceeded the max number of retries // Return false to indicate this message should be dead-lettered + _logger.LogWarning( + "Integration failure - non-recoverable error or max retries exceeded. " + + "MessageId: {MessageId}, IntegrationType: {IntegrationType}, OrganizationId: {OrgId}, " + + "FailureCategory: {Category}, Reason: {Reason}, RetryCount: {RetryCount}, MaxRetries: {MaxRetries}", + message.MessageId, + message.IntegrationType, + message.OrganizationId, + result.Category, + result.FailureReason, + message.RetryCount, + _maxRetries); return false; } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs index b426032c92..0762edc040 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs @@ -106,14 +106,32 @@ public class RabbitMqIntegrationListenerService : BackgroundServ { // Exceeded the max number of retries; fail and send to dead letter queue await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); - _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); + _logger.LogWarning( + "Integration failure - max retries exceeded. " + + "MessageId: {MessageId}, IntegrationType: {IntegrationType}, OrganizationId: {OrgId}, " + + "FailureCategory: {Category}, Reason: {Reason}, RetryCount: {RetryCount}, MaxRetries: {MaxRetries}", + message.MessageId, + message.IntegrationType, + message.OrganizationId, + result.Category, + result.FailureReason, + message.RetryCount, + _maxRetries); } } else { // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); - _logger.LogWarning("Non-retryable failure. Sent to DLQ."); + _logger.LogWarning( + "Integration failure - non-retryable. " + + "MessageId: {MessageId}, IntegrationType: {IntegrationType}, OrganizationId: {OrgId}, " + + "FailureCategory: {Category}, Reason: {Reason}", + message.MessageId, + message.IntegrationType, + message.OrganizationId, + result.Category, + result.FailureReason); } // Message has been sent to retry or dead letter queues. diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs index 16c756c8c4..e681140afe 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs @@ -6,15 +6,6 @@ public class SlackIntegrationHandler( ISlackService slackService) : IntegrationHandlerBase { - private static readonly HashSet _retryableErrors = new(StringComparer.Ordinal) - { - "internal_error", - "message_limit_exceeded", - "rate_limited", - "ratelimited", - "service_unavailable" - }; - public override async Task HandleAsync(IntegrationMessage message) { var slackResponse = await slackService.SendSlackMessageByChannelIdAsync( @@ -25,24 +16,61 @@ public class SlackIntegrationHandler( if (slackResponse is null) { - return new IntegrationHandlerResult(success: false, message: message) - { - FailureReason = "Slack response was null" - }; + return IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.TransientError, + "Slack response was null" + ); } if (slackResponse.Ok) { - return new IntegrationHandlerResult(success: true, message: message); + return IntegrationHandlerResult.Succeed(message); } - var result = new IntegrationHandlerResult(success: false, message: message) { FailureReason = slackResponse.Error }; + var category = ClassifySlackError(slackResponse.Error); + return IntegrationHandlerResult.Fail( + message, + category, + slackResponse.Error + ); + } - if (_retryableErrors.Contains(slackResponse.Error)) + /// + /// Classifies a Slack API error code string as an to drive + /// retry behavior and operator-facing failure reporting. + /// + /// + /// + /// Slack responses commonly return an error string when ok is false. This method maps + /// known Slack error codes to failure categories. + /// + /// + /// Any unrecognized error codes default to to avoid + /// incorrectly marking new/unknown Slack failures as non-retryable. + /// + /// + /// The Slack error code string (e.g. invalid_auth, rate_limited). + /// The corresponding . + private static IntegrationFailureCategory ClassifySlackError(string error) + { + return error switch { - result.Retryable = true; - } - - return result; + "invalid_auth" => IntegrationFailureCategory.AuthenticationFailed, + "access_denied" => IntegrationFailureCategory.AuthenticationFailed, + "token_expired" => IntegrationFailureCategory.AuthenticationFailed, + "token_revoked" => IntegrationFailureCategory.AuthenticationFailed, + "account_inactive" => IntegrationFailureCategory.AuthenticationFailed, + "not_authed" => IntegrationFailureCategory.AuthenticationFailed, + "channel_not_found" => IntegrationFailureCategory.ConfigurationError, + "is_archived" => IntegrationFailureCategory.ConfigurationError, + "rate_limited" => IntegrationFailureCategory.RateLimited, + "ratelimited" => IntegrationFailureCategory.RateLimited, + "message_limit_exceeded" => IntegrationFailureCategory.RateLimited, + "internal_error" => IntegrationFailureCategory.TransientError, + "service_unavailable" => IntegrationFailureCategory.ServiceUnavailable, + "fatal_error" => IntegrationFailureCategory.ServiceUnavailable, + _ => IntegrationFailureCategory.TransientError + }; } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs index 41d60bd69c..9e3645a99f 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Rest; namespace Bit.Core.Services; @@ -18,24 +19,48 @@ public class TeamsIntegrationHandler( channelId: message.Configuration.ChannelId ); - return new IntegrationHandlerResult(success: true, message: message); + return IntegrationHandlerResult.Succeed(message); } catch (HttpOperationException ex) { - var result = new IntegrationHandlerResult(success: false, message: message); - var statusCode = (int)ex.Response.StatusCode; - result.Retryable = statusCode is 429 or >= 500 and < 600; - result.FailureReason = ex.Message; - - return result; + var category = ClassifyHttpStatusCode(ex.Response.StatusCode); + return IntegrationHandlerResult.Fail( + message, + category, + ex.Message + ); + } + catch (ArgumentException ex) + { + return IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.ConfigurationError, + ex.Message + ); + } + catch (UriFormatException ex) + { + return IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.ConfigurationError, + ex.Message + ); + } + catch (JsonException ex) + { + return IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.PermanentFailure, + ex.Message + ); } catch (Exception ex) { - var result = new IntegrationHandlerResult(success: false, message: message); - result.Retryable = false; - result.FailureReason = ex.Message; - - return result; + return IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.TransientError, + ex.Message + ); } } } diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs new file mode 100644 index 0000000000..6925a978eb --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs @@ -0,0 +1,128 @@ +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations; + +public class IntegrationHandlerResultTests +{ + [Theory, BitAutoData] + public void Succeed_SetsSuccessTrue_CategoryNull(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Succeed(message); + + Assert.True(result.Success); + Assert.Null(result.Category); + Assert.Equal(message, result.Message); + Assert.Null(result.FailureReason); + } + + [Theory, BitAutoData] + public void Fail_WithCategory_SetsSuccessFalse_CategorySet(IntegrationMessage message) + { + var category = IntegrationFailureCategory.AuthenticationFailed; + var failureReason = "Invalid credentials"; + + var result = IntegrationHandlerResult.Fail(message, category, failureReason); + + Assert.False(result.Success); + Assert.Equal(category, result.Category); + Assert.Equal(failureReason, result.FailureReason); + Assert.Equal(message, result.Message); + } + + [Theory, BitAutoData] + public void Fail_WithDelayUntil_SetsDelayUntilDate(IntegrationMessage message) + { + var delayUntil = DateTime.UtcNow.AddMinutes(5); + + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.RateLimited, + "Rate limited", + delayUntil + ); + + Assert.Equal(delayUntil, result.DelayUntilDate); + } + + [Theory, BitAutoData] + public void Retryable_RateLimited_ReturnsTrue(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.RateLimited, + "Rate limited" + ); + + Assert.True(result.Retryable); + } + + [Theory, BitAutoData] + public void Retryable_TransientError_ReturnsTrue(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.TransientError, + "Temporary network issue" + ); + + Assert.True(result.Retryable); + } + + [Theory, BitAutoData] + public void Retryable_AuthenticationFailed_ReturnsFalse(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.AuthenticationFailed, + "Invalid token" + ); + + Assert.False(result.Retryable); + } + + [Theory, BitAutoData] + public void Retryable_ConfigurationError_ReturnsFalse(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.ConfigurationError, + "Channel not found" + ); + + Assert.False(result.Retryable); + } + + [Theory, BitAutoData] + public void Retryable_ServiceUnavailable_ReturnsTrue(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.ServiceUnavailable, + "Service is down" + ); + + Assert.True(result.Retryable); + } + + [Theory, BitAutoData] + public void Retryable_PermanentFailure_ReturnsFalse(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Fail( + message, + IntegrationFailureCategory.PermanentFailure, + "Permanent failure" + ); + + Assert.False(result.Retryable); + } + + [Theory, BitAutoData] + public void Retryable_SuccessCase_ReturnsFalse(IntegrationMessage message) + { + var result = IntegrationHandlerResult.Succeed(message); + + Assert.False(result.Retryable); + } +} diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs index 23627f3962..9e46a3a99a 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -78,8 +78,10 @@ public class AzureServiceBusIntegrationListenerServiceTests var sutProvider = GetSutProvider(); message.RetryCount = 0; - var result = new IntegrationHandlerResult(false, message); - result.Retryable = false; + var result = IntegrationHandlerResult.Fail( + message: message, + category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable + failureReason: "403"); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -89,6 +91,12 @@ public class AzureServiceBusIntegrationListenerServiceTests await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any()); + _logger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")), + Arg.Any(), + Arg.Any>()); } [Theory, BitAutoData] @@ -96,9 +104,10 @@ public class AzureServiceBusIntegrationListenerServiceTests { var sutProvider = GetSutProvider(); message.RetryCount = _config.MaxRetries; - var result = new IntegrationHandlerResult(false, message); - result.Retryable = true; - + var result = IntegrationHandlerResult.Fail( + message: message, + category: IntegrationFailureCategory.TransientError, // Retryable + failureReason: "403"); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -108,6 +117,12 @@ public class AzureServiceBusIntegrationListenerServiceTests await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any()); + _logger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => (o.ToString() ?? "").Contains("Integration failure - non-recoverable error or max retries exceeded.")), + Arg.Any(), + Arg.Any>()); } [Theory, BitAutoData] @@ -116,8 +131,10 @@ public class AzureServiceBusIntegrationListenerServiceTests var sutProvider = GetSutProvider(); message.RetryCount = 0; - var result = new IntegrationHandlerResult(false, message); - result.Retryable = true; + var result = IntegrationHandlerResult.Fail( + message: message, + category: IntegrationFailureCategory.TransientError, // Retryable + failureReason: "403"); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -133,7 +150,7 @@ public class AzureServiceBusIntegrationListenerServiceTests public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage message) { var sutProvider = GetSutProvider(); - var result = new IntegrationHandlerResult(true, message); + var result = IntegrationHandlerResult.Succeed(message); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -156,7 +173,7 @@ public class AzureServiceBusIntegrationListenerServiceTests _logger.Received(1).Log( LogLevel.Error, Arg.Any(), - Arg.Any(), + Arg.Is(o => (o.ToString() ?? "").Contains("Unhandled error processing ASB message")), Arg.Any(), Arg.Any>()); diff --git a/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs index 5f0a9915bf..9cb21f012a 100644 --- a/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs @@ -51,7 +51,7 @@ public class DatadogIntegrationHandlerTests Assert.True(result.Success); Assert.Equal(result.Message, message); - Assert.Empty(result.FailureReason); + Assert.Null(result.FailureReason); sutProvider.GetDependency().Received(1).CreateClient( Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName)) diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index f6f587cfd7..b3bbcb7ef2 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using System.Net; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Bit.Core.Services; using Xunit; @@ -7,7 +8,6 @@ namespace Bit.Core.Test.Services; public class IntegrationHandlerTests { - [Fact] public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage() { @@ -33,13 +33,113 @@ public class IntegrationHandlerTests Assert.Equal(expected.IntegrationType, typedResult.IntegrationType); } + [Theory] + [InlineData(HttpStatusCode.Unauthorized)] + [InlineData(HttpStatusCode.Forbidden)] + public void ClassifyHttpStatusCode_AuthenticationFailed(HttpStatusCode code) + { + Assert.Equal( + IntegrationFailureCategory.AuthenticationFailed, + TestIntegrationHandler.Classify(code)); + } + + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.Gone)] + [InlineData(HttpStatusCode.MovedPermanently)] + [InlineData(HttpStatusCode.TemporaryRedirect)] + [InlineData(HttpStatusCode.PermanentRedirect)] + public void ClassifyHttpStatusCode_ConfigurationError(HttpStatusCode code) + { + Assert.Equal( + IntegrationFailureCategory.ConfigurationError, + TestIntegrationHandler.Classify(code)); + } + + [Fact] + public void ClassifyHttpStatusCode_TooManyRequests_IsRateLimited() + { + Assert.Equal( + IntegrationFailureCategory.RateLimited, + TestIntegrationHandler.Classify(HttpStatusCode.TooManyRequests)); + } + + [Fact] + public void ClassifyHttpStatusCode_RequestTimeout_IsTransient() + { + Assert.Equal( + IntegrationFailureCategory.TransientError, + TestIntegrationHandler.Classify(HttpStatusCode.RequestTimeout)); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.BadGateway)] + [InlineData(HttpStatusCode.GatewayTimeout)] + public void ClassifyHttpStatusCode_Common5xx_AreTransient(HttpStatusCode code) + { + Assert.Equal( + IntegrationFailureCategory.TransientError, + TestIntegrationHandler.Classify(code)); + } + + [Fact] + public void ClassifyHttpStatusCode_ServiceUnavailable_IsServiceUnavailable() + { + Assert.Equal( + IntegrationFailureCategory.ServiceUnavailable, + TestIntegrationHandler.Classify(HttpStatusCode.ServiceUnavailable)); + } + + [Fact] + public void ClassifyHttpStatusCode_NotImplemented_IsPermanentFailure() + { + Assert.Equal( + IntegrationFailureCategory.PermanentFailure, + TestIntegrationHandler.Classify(HttpStatusCode.NotImplemented)); + } + + [Fact] + public void FClassifyHttpStatusCode_Unhandled3xx_IsConfigurationError() + { + Assert.Equal( + IntegrationFailureCategory.ConfigurationError, + TestIntegrationHandler.Classify(HttpStatusCode.Found)); + } + + [Fact] + public void ClassifyHttpStatusCode_Unhandled4xx_IsConfigurationError() + { + Assert.Equal( + IntegrationFailureCategory.ConfigurationError, + TestIntegrationHandler.Classify(HttpStatusCode.BadRequest)); + } + + [Fact] + public void ClassifyHttpStatusCode_Unhandled5xx_IsServiceUnavailable() + { + Assert.Equal( + IntegrationFailureCategory.ServiceUnavailable, + TestIntegrationHandler.Classify(HttpStatusCode.HttpVersionNotSupported)); + } + + [Fact] + public void ClassifyHttpStatusCode_UnknownCode_DefaultsToServiceUnavailable() + { + // cast an out-of-range value to ensure default path is stable + Assert.Equal( + IntegrationFailureCategory.ServiceUnavailable, + TestIntegrationHandler.Classify((HttpStatusCode)799)); + } + private class TestIntegrationHandler : IntegrationHandlerBase { public override Task HandleAsync( IntegrationMessage message) { - var result = new IntegrationHandlerResult(success: true, message: message); - return Task.FromResult(result); + return Task.FromResult(IntegrationHandlerResult.Succeed(message: message)); } + + public static IntegrationFailureCategory Classify(HttpStatusCode code) => ClassifyHttpStatusCode(code); } } diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs index 5fcd121252..71985889f8 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -86,8 +86,10 @@ public class RabbitMqIntegrationListenerServiceTests new BasicProperties(), body: Encoding.UTF8.GetBytes(message.ToJson()) ); - var result = new IntegrationHandlerResult(false, message); - result.Retryable = false; + var result = IntegrationHandlerResult.Fail( + message: message, + category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable + failureReason: "403"); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -105,7 +107,7 @@ public class RabbitMqIntegrationListenerServiceTests _logger.Received().Log( LogLevel.Warning, Arg.Any(), - Arg.Is(o => (o.ToString() ?? "").Contains("Non-retryable failure")), + Arg.Is(o => (o.ToString() ?? "").Contains("Integration failure - non-retryable.")), Arg.Any(), Arg.Any>()); @@ -133,8 +135,10 @@ public class RabbitMqIntegrationListenerServiceTests new BasicProperties(), body: Encoding.UTF8.GetBytes(message.ToJson()) ); - var result = new IntegrationHandlerResult(false, message); - result.Retryable = true; + var result = IntegrationHandlerResult.Fail( + message: message, + category: IntegrationFailureCategory.TransientError, // Retryable + failureReason: "403"); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -151,7 +155,7 @@ public class RabbitMqIntegrationListenerServiceTests _logger.Received().Log( LogLevel.Warning, Arg.Any(), - Arg.Is(o => (o.ToString() ?? "").Contains("Max retry attempts reached")), + Arg.Is(o => (o.ToString() ?? "").Contains("Integration failure - max retries exceeded.")), Arg.Any(), Arg.Any>()); @@ -179,9 +183,10 @@ public class RabbitMqIntegrationListenerServiceTests new BasicProperties(), body: Encoding.UTF8.GetBytes(message.ToJson()) ); - var result = new IntegrationHandlerResult(false, message); - result.Retryable = true; - result.DelayUntilDate = _now.AddMinutes(1); + var result = IntegrationHandlerResult.Fail( + message: message, + category: IntegrationFailureCategory.TransientError, // Retryable + failureReason: "403"); _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); @@ -220,7 +225,7 @@ public class RabbitMqIntegrationListenerServiceTests new BasicProperties(), body: Encoding.UTF8.GetBytes(message.ToJson()) ); - var result = new IntegrationHandlerResult(true, message); + var result = IntegrationHandlerResult.Succeed(message); _handler.HandleAsync(Arg.Any()).Returns(result); await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); diff --git a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs index e2e459ceb3..e455100995 100644 --- a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs @@ -110,7 +110,7 @@ public class SlackIntegrationHandlerTests } [Fact] - public async Task HandleAsync_NullResponse_ReturnsNonRetryableFailure() + public async Task HandleAsync_NullResponse_ReturnsRetryableFailure() { var sutProvider = GetSutProvider(); var message = new IntegrationMessage() @@ -126,7 +126,7 @@ public class SlackIntegrationHandlerTests var result = await sutProvider.Sut.HandleAsync(message); Assert.False(result.Success); - Assert.False(result.Retryable); + Assert.True(result.Retryable); // Null response is classified as TransientError (retryable) Assert.Equal("Slack response was null", result.FailureReason); Assert.Equal(result.Message, message); diff --git a/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs index b744a6aa69..11056ec2cc 100644 --- a/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -42,9 +43,77 @@ public class TeamsIntegrationHandlerTests ); } + [Theory, BitAutoData] + public async Task HandleAsync_ArgumentException_ReturnsConfigurationError(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + sutProvider.GetDependency() + .SendMessageToChannelAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new ArgumentException("argument error")); + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } [Theory, BitAutoData] - public async Task HandleAsync_HttpExceptionNonRetryable_ReturnsFalseAndNotRetryable(IntegrationMessage message) + public async Task HandleAsync_JsonException_ReturnsPermanentFailure(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + sutProvider.GetDependency() + .SendMessageToChannelAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new JsonException("JSON error")); + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.Equal(IntegrationFailureCategory.PermanentFailure, result.Category); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } + + [Theory, BitAutoData] + public async Task HandleAsync_UriFormatException_ReturnsConfigurationError(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + sutProvider.GetDependency() + .SendMessageToChannelAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new UriFormatException("Bad URI")); + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } + + [Theory, BitAutoData] + public async Task HandleAsync_HttpExceptionForbidden_ReturnsAuthenticationFailed(IntegrationMessage message) { var sutProvider = GetSutProvider(); message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); @@ -62,6 +131,7 @@ public class TeamsIntegrationHandlerTests var result = await sutProvider.Sut.HandleAsync(message); Assert.False(result.Success); + Assert.Equal(IntegrationFailureCategory.AuthenticationFailed, result.Category); Assert.False(result.Retryable); Assert.Equal(result.Message, message); @@ -73,7 +143,7 @@ public class TeamsIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleAsync_HttpExceptionRetryable_ReturnsFalseAndRetryable(IntegrationMessage message) + public async Task HandleAsync_HttpExceptionTooManyRequests_ReturnsRateLimited(IntegrationMessage message) { var sutProvider = GetSutProvider(); message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); @@ -92,6 +162,7 @@ public class TeamsIntegrationHandlerTests var result = await sutProvider.Sut.HandleAsync(message); Assert.False(result.Success); + Assert.Equal(IntegrationFailureCategory.RateLimited, result.Category); Assert.True(result.Retryable); Assert.Equal(result.Message, message); @@ -103,7 +174,7 @@ public class TeamsIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleAsync_UnknownException_ReturnsFalseAndNotRetryable(IntegrationMessage message) + public async Task HandleAsync_UnknownException_ReturnsTransientError(IntegrationMessage message) { var sutProvider = GetSutProvider(); message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); @@ -114,7 +185,8 @@ public class TeamsIntegrationHandlerTests var result = await sutProvider.Sut.HandleAsync(message); Assert.False(result.Success); - Assert.False(result.Retryable); + Assert.Equal(IntegrationFailureCategory.TransientError, result.Category); + Assert.True(result.Retryable); Assert.Equal(result.Message, message); await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 53a3598d47..05aa46681a 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -51,7 +51,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Success); Assert.Equal(result.Message, message); - Assert.Empty(result.FailureReason); + Assert.Null(result.FailureReason); sutProvider.GetDependency().Received(1).CreateClient( Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) @@ -79,7 +79,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Success); Assert.Equal(result.Message, message); - Assert.Empty(result.FailureReason); + Assert.Null(result.FailureReason); sutProvider.GetDependency().Received(1).CreateClient( Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) From de504d800b0513c5dcddc8e23ba386d31c7a7ea1 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Wed, 17 Dec 2025 11:34:17 -0600 Subject: [PATCH 06/68] [PM-24055] - Collection Users and Groups null on Public response (#6713) * Integration test around getting and saving collection with group/user permissions * This adds groups to the collections returned. * Added new stored procedures so we don't accidentally wipe out access due to null parameters. * wrapping all calls in transaction in the event that there is an error. --- .../Models/Response/GroupResponseModel.cs | 7 + .../Response/CollectionResponseModel.cs | 7 + .../Controllers/CollectionsController.cs | 9 +- .../Repositories/CollectionRepository.cs | 116 ++++++++++++-- .../Collection_UpdateWithGroups.sql | 74 +++++++++ .../Collection_UpdateWithUsers.sql | 74 +++++++++ .../Public/CollectionsControllerTests.cs | 117 ++++++++++++++ .../Helpers/OrganizationTestHelpers.cs | 6 +- .../CollectionRepositoryReplaceTests.cs | 65 ++++++++ ...10_00_AddGroupAndUserCollectionUpdates.sql | 151 ++++++++++++++++++ 10 files changed, 609 insertions(+), 17 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql create mode 100644 src/Sql/dbo/Stored Procedures/Collection_UpdateWithUsers.sql create mode 100644 test/Api.IntegrationTest/Controllers/Public/CollectionsControllerTests.cs create mode 100644 util/Migrator/DbScripts/2025-12-10_00_AddGroupAndUserCollectionUpdates.sql diff --git a/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs index c12616b4cc..e164f3c4ea 100644 --- a/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Data; @@ -13,6 +14,12 @@ namespace Bit.Api.AdminConsole.Public.Models.Response; /// public class GroupResponseModel : GroupBaseModel, IResponseModel { + [JsonConstructor] + public GroupResponseModel() + { + + } + public GroupResponseModel(Group group, IEnumerable collections) { if (group == null) diff --git a/src/Api/Models/Public/Response/CollectionResponseModel.cs b/src/Api/Models/Public/Response/CollectionResponseModel.cs index 04ae565a27..9e830aeea8 100644 --- a/src/Api/Models/Public/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Public/Response/CollectionResponseModel.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Core.Entities; using Bit.Core.Models.Data; @@ -13,6 +14,12 @@ namespace Bit.Api.Models.Public.Response; /// public class CollectionResponseModel : CollectionBaseModel, IResponseModel { + [JsonConstructor] + public CollectionResponseModel() + { + + } + public CollectionResponseModel(Collection collection, IEnumerable groups) { if (collection == null) diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index 8615113906..a567062a5e 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -65,10 +65,11 @@ public class CollectionsController : Controller [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] public async Task List() { - var collections = await _collectionRepository.GetManySharedCollectionsByOrganizationIdAsync( - _currentContext.OrganizationId.Value); - // TODO: Get all CollectionGroup associations for the organization and marry them up here for the response. - var collectionResponses = collections.Select(c => new CollectionResponseModel(c, null)); + var collections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(_currentContext.OrganizationId.Value); + + var collectionResponses = collections.Select(c => + new CollectionResponseModel(c.Item1, c.Item2.Groups)); + var response = new ListResponseModel(collectionResponses); return new JsonResult(response); } diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index c2a59f75aa..9985b41d56 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -226,7 +226,6 @@ public class CollectionRepository : Repository, ICollectionRep { obj.SetNewId(); - var objWithGroupsAndUsers = JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; objWithGroupsAndUsers.Groups = groups != null ? groups.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); @@ -243,18 +242,52 @@ public class CollectionRepository : Repository, ICollectionRep public async Task ReplaceAsync(Collection obj, IEnumerable? groups, IEnumerable? users) { - var objWithGroupsAndUsers = JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; - - objWithGroupsAndUsers.Groups = groups != null ? groups.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); - objWithGroupsAndUsers.Users = users != null ? users.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); - - using (var connection = new SqlConnection(ConnectionString)) + await using var connection = new SqlConnection(ConnectionString); + await connection.OpenAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + try { - var results = await connection.ExecuteAsync( - $"[{Schema}].[Collection_UpdateWithGroupsAndUsers]", - objWithGroupsAndUsers, - commandType: CommandType.StoredProcedure); + if (groups == null && users == null) + { + await connection.ExecuteAsync( + $"[{Schema}].[Collection_Update]", + obj, + commandType: CommandType.StoredProcedure, + transaction: transaction); + } + else if (groups != null && users == null) + { + await connection.ExecuteAsync( + $"[{Schema}].[Collection_UpdateWithGroups]", + new CollectionWithGroups(obj, groups), + commandType: CommandType.StoredProcedure, + transaction: transaction); + } + else if (groups == null && users != null) + { + await connection.ExecuteAsync( + $"[{Schema}].[Collection_UpdateWithUsers]", + new CollectionWithUsers(obj, users), + commandType: CommandType.StoredProcedure, + transaction: transaction); + } + else if (groups != null && users != null) + { + await connection.ExecuteAsync( + $"[{Schema}].[Collection_UpdateWithGroupsAndUsers]", + new CollectionWithGroupsAndUsers(obj, groups, users), + commandType: CommandType.StoredProcedure, + transaction: transaction); + } + + await transaction.CommitAsync(); } + catch + { + await transaction.RollbackAsync(); + throw; + } + } public async Task DeleteManyAsync(IEnumerable collectionIds) @@ -424,9 +457,70 @@ public class CollectionRepository : Repository, ICollectionRep public class CollectionWithGroupsAndUsers : Collection { + public CollectionWithGroupsAndUsers() { } + + public CollectionWithGroupsAndUsers(Collection collection, + IEnumerable groups, + IEnumerable users) + { + Id = collection.Id; + Name = collection.Name; + OrganizationId = collection.OrganizationId; + CreationDate = collection.CreationDate; + RevisionDate = collection.RevisionDate; + Type = collection.Type; + ExternalId = collection.ExternalId; + DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail; + Groups = groups.ToArrayTVP(); + Users = users.ToArrayTVP(); + } + [DisallowNull] public DataTable? Groups { get; set; } [DisallowNull] public DataTable? Users { get; set; } } + + public class CollectionWithGroups : Collection + { + public CollectionWithGroups() { } + + public CollectionWithGroups(Collection collection, IEnumerable groups) + { + Id = collection.Id; + Name = collection.Name; + OrganizationId = collection.OrganizationId; + CreationDate = collection.CreationDate; + RevisionDate = collection.RevisionDate; + Type = collection.Type; + ExternalId = collection.ExternalId; + DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail; + Groups = groups.ToArrayTVP(); + } + + [DisallowNull] + public DataTable? Groups { get; set; } + } + + public class CollectionWithUsers : Collection + { + public CollectionWithUsers() { } + + public CollectionWithUsers(Collection collection, IEnumerable users) + { + + Id = collection.Id; + Name = collection.Name; + OrganizationId = collection.OrganizationId; + CreationDate = collection.CreationDate; + RevisionDate = collection.RevisionDate; + Type = collection.Type; + ExternalId = collection.ExternalId; + DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail; + Users = users.ToArrayTVP(); + } + + [DisallowNull] + public DataTable? Users { get; set; } + } } diff --git a/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql new file mode 100644 index 0000000000..7f7fc2e0d7 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql @@ -0,0 +1,74 @@ +CREATE PROCEDURE [dbo].[Collection_UpdateWithGroups] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + + -- Groups + -- Delete groups that are no longer in source + DELETE + cg + FROM + [dbo].[CollectionGroup] cg + LEFT JOIN + @Groups g ON cg.GroupId = g.Id + WHERE + cg.CollectionId = @Id + AND g.Id IS NULL; + + -- Update existing groups + UPDATE + cg + SET + cg.ReadOnly = g.ReadOnly, + cg.HidePasswords = g.HidePasswords, + cg.Manage = g.Manage + FROM + [dbo].[CollectionGroup] cg + INNER JOIN + @Groups g ON cg.GroupId = g.Id + WHERE + cg.CollectionId = @Id + AND ( + cg.ReadOnly != g.ReadOnly + OR cg.HidePasswords != g.HidePasswords + OR cg.Manage != g.Manage + ); + + -- Insert new groups + INSERT INTO [dbo].[CollectionGroup] + ( + [CollectionId], + [GroupId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + g.Id, + g.ReadOnly, + g.HidePasswords, + g.Manage + FROM + @Groups g + INNER JOIN + [dbo].[Group] grp ON grp.Id = g.Id + LEFT JOIN + [dbo].[CollectionGroup] cg ON cg.CollectionId = @Id AND cg.GroupId = g.Id + WHERE + grp.OrganizationId = @OrganizationId + AND cg.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END diff --git a/src/Sql/dbo/Stored Procedures/Collection_UpdateWithUsers.sql b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithUsers.sql new file mode 100644 index 0000000000..60fccc51d5 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithUsers.sql @@ -0,0 +1,74 @@ +CREATE PROCEDURE [dbo].[Collection_UpdateWithUsers] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Users AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + + -- Users + -- Delete users that are no longer in source + DELETE + cu + FROM + [dbo].[CollectionUser] cu + LEFT JOIN + @Users u ON cu.OrganizationUserId = u.Id + WHERE + cu.CollectionId = @Id + AND u.Id IS NULL; + + -- Update existing users + UPDATE + cu + SET + cu.ReadOnly = u.ReadOnly, + cu.HidePasswords = u.HidePasswords, + cu.Manage = u.Manage + FROM + [dbo].[CollectionUser] cu + INNER JOIN + @Users u ON cu.OrganizationUserId = u.Id + WHERE + cu.CollectionId = @Id + AND ( + cu.ReadOnly != u.ReadOnly + OR cu.HidePasswords != u.HidePasswords + OR cu.Manage != u.Manage + ); + + -- Insert new users + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + u.Id, + u.ReadOnly, + u.HidePasswords, + u.Manage + FROM + @Users u + INNER JOIN + [dbo].[OrganizationUser] ou ON ou.Id = u.Id + LEFT JOIN + [dbo].[CollectionUser] cu ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id + WHERE + ou.OrganizationId = @OrganizationId + AND cu.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END diff --git a/test/Api.IntegrationTest/Controllers/Public/CollectionsControllerTests.cs b/test/Api.IntegrationTest/Controllers/Public/CollectionsControllerTests.cs new file mode 100644 index 0000000000..a729abb849 --- /dev/null +++ b/test/Api.IntegrationTest/Controllers/Public/CollectionsControllerTests.cs @@ -0,0 +1,117 @@ +using Bit.Api.AdminConsole.Public.Models.Request; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.Models.Public.Request; +using Bit.Api.Models.Public.Response; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Xunit; + +namespace Bit.Api.IntegrationTest.Controllers.Public; + +public class CollectionsControllerTests : IClassFixture, IAsyncLifetime +{ + + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + private string _ownerEmail = null!; + private Organization _organization = null!; + + public CollectionsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService(_ => { }); + _factory.SubstituteService(_ => { }); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, + plan: PlanType.EnterpriseAnnually, + ownerEmail: _ownerEmail, + passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + + await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task CreateCollectionWithMultipleUsersAndVariedPermissions_Success() + { + // Arrange + _organization.AllowAdminAccessToAllCollectionItems = true; + await _factory.GetService().UpsertAsync(_organization); + + var groupRepository = _factory.GetService(); + var group = await groupRepository.CreateAsync(new Group + { + OrganizationId = _organization.Id, + Name = "CollectionControllerTests.CreateCollectionWithMultipleUsersAndVariedPermissions_Success", + ExternalId = $"CollectionControllerTests.CreateCollectionWithMultipleUsersAndVariedPermissions_Success{Guid.NewGuid()}", + }); + + var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync( + _factory, + _organization.Id, + OrganizationUserType.User); + + var collection = await OrganizationTestHelpers.CreateCollectionAsync( + _factory, + _organization.Id, + "Shared Collection with a group", + externalId: "shared-collection-with-group", + groups: + [ + new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true } + ], + users: + [ + new CollectionAccessSelection { Id = user.Id, ReadOnly = false, HidePasswords = false, Manage = true } + ]); + + var getCollectionsResponse = await _client.GetFromJsonAsync>("public/collections"); + var getCollectionResponse = await _client.GetFromJsonAsync($"public/collections/{collection.Id}"); + + var firstCollection = getCollectionsResponse.Data.First(x => x.ExternalId == "shared-collection-with-group"); + + var update = new CollectionUpdateRequestModel + { + ExternalId = firstCollection.ExternalId, + Groups = firstCollection.Groups?.Select(x => new AssociationWithPermissionsRequestModel + { + Id = x.Id, + ReadOnly = x.ReadOnly, + HidePasswords = x.HidePasswords, + Manage = x.Manage + }), + }; + + await _client.PutAsJsonAsync($"public/collections/{firstCollection.Id}", update); + + var result = await _factory.GetService() + .GetByIdWithAccessAsync(firstCollection.Id); + + Assert.NotNull(result); + Assert.NotEmpty(result.Item2.Groups); + Assert.NotEmpty(result.Item2.Users); + } +} diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index bcde370b24..887ef989ce 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -159,14 +159,16 @@ public static class OrganizationTestHelpers Guid organizationId, string name, IEnumerable? users = null, - IEnumerable? groups = null) + IEnumerable? groups = null, + string? externalId = null) { var collectionRepository = factory.GetService(); var collection = new Collection { OrganizationId = organizationId, Name = name, - Type = CollectionType.SharedCollection + Type = CollectionType.SharedCollection, + ExternalId = externalId }; await collectionRepository.CreateAsync(collection, groups, users); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs index df01276493..de4fd53a68 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs @@ -144,4 +144,69 @@ public class CollectionRepositoryReplaceTests await userRepository.DeleteAsync(user); await organizationRepository.DeleteAsync(organization); } + + [Theory, DatabaseData] + public async Task ReplaceAsync_WhenNotPassingGroupsOrUsers_DoesNotDeleteAccess( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user1 = await userRepository.CreateTestUserAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + + var user2 = await userRepository.CreateTestUserAsync(); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + var group1 = await groupRepository.CreateTestGroupAsync(organization); + var group2 = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, + ] + ); + + // Act + collection.Name = "Updated Collection Name"; + + await collectionRepository.ReplaceAsync(collection, null, null); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Updated Collection Name", actualCollection.Name); + + var groups = actualAccess.Groups.ToArray(); + Assert.Equal(2, groups.Length); + Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly); + Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly); + + var users = actualAccess.Users.ToArray(); + + Assert.Equal(2, users.Length); + Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly); + Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly); + + // Clean up data + await userRepository.DeleteAsync(user1); + await userRepository.DeleteAsync(user2); + await organizationRepository.DeleteAsync(organization); + } } diff --git a/util/Migrator/DbScripts/2025-12-10_00_AddGroupAndUserCollectionUpdates.sql b/util/Migrator/DbScripts/2025-12-10_00_AddGroupAndUserCollectionUpdates.sql new file mode 100644 index 0000000000..162be5a7b2 --- /dev/null +++ b/util/Migrator/DbScripts/2025-12-10_00_AddGroupAndUserCollectionUpdates.sql @@ -0,0 +1,151 @@ +CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithUsers] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Users AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + + -- Users + -- Delete users that are no longer in source + DELETE + cu + FROM + [dbo].[CollectionUser] cu + LEFT JOIN + @Users u ON cu.OrganizationUserId = u.Id + WHERE + cu.CollectionId = @Id + AND u.Id IS NULL; + + -- Update existing users + UPDATE + cu + SET + cu.ReadOnly = u.ReadOnly, + cu.HidePasswords = u.HidePasswords, + cu.Manage = u.Manage + FROM + [dbo].[CollectionUser] cu + INNER JOIN + @Users u ON cu.OrganizationUserId = u.Id + WHERE + cu.CollectionId = @Id + AND ( + cu.ReadOnly != u.ReadOnly + OR cu.HidePasswords != u.HidePasswords + OR cu.Manage != u.Manage + ); + + -- Insert new users + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + u.Id, + u.ReadOnly, + u.HidePasswords, + u.Manage + FROM + @Users u + INNER JOIN + [dbo].[OrganizationUser] ou ON ou.Id = u.Id + LEFT JOIN + [dbo].[CollectionUser] cu ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id + WHERE + ou.OrganizationId = @OrganizationId + AND cu.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithGroups] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Groups AS [dbo].[CollectionAccessSelectionType] READONLY, + @DefaultUserCollectionEmail NVARCHAR(256) = NULL, + @Type TINYINT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type + + -- Groups + -- Delete groups that are no longer in source + DELETE + cg + FROM + [dbo].[CollectionGroup] cg + LEFT JOIN + @Groups g ON cg.GroupId = g.Id + WHERE + cg.CollectionId = @Id + AND g.Id IS NULL; + + -- Update existing groups + UPDATE + cg + SET + cg.ReadOnly = g.ReadOnly, + cg.HidePasswords = g.HidePasswords, + cg.Manage = g.Manage + FROM + [dbo].[CollectionGroup] cg + INNER JOIN + @Groups g ON cg.GroupId = g.Id + WHERE + cg.CollectionId = @Id + AND ( + cg.ReadOnly != g.ReadOnly + OR cg.HidePasswords != g.HidePasswords + OR cg.Manage != g.Manage + ); + + -- Insert new groups + INSERT INTO [dbo].[CollectionGroup] + ( + [CollectionId], + [GroupId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + @Id, + g.Id, + g.ReadOnly, + g.HidePasswords, + g.Manage + FROM + @Groups g + INNER JOIN + [dbo].[Group] grp ON grp.Id = g.Id + LEFT JOIN + [dbo].[CollectionGroup] cg ON cg.CollectionId = @Id AND cg.GroupId = g.Id + WHERE + grp.OrganizationId = @OrganizationId + AND cg.CollectionId IS NULL; + + EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId +END +GO From 19ee4a0054e1af84b3e7e632dda513fb2cdad108 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:42:33 -0500 Subject: [PATCH 07/68] [deps] BRE: Update rabbitmq Docker tag to v4.2.0 (#4026) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- dev/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 3554306ddb..c82da051b4 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -99,7 +99,7 @@ services: - idp rabbitmq: - image: rabbitmq:4.1.3-management + image: rabbitmq:4.2.0-management ports: - "5672:5672" - "15672:15672" From b3437b3b305c7b90107813dbfa2d26b3a64fc8f9 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:00:05 -0500 Subject: [PATCH 08/68] Update requirements for RabbitMQ and Azure Service Bus configuration (#6741) --- ...IntegrationsServiceCollectionExtensions.cs | 26 ++++-- ...grationServiceCollectionExtensionsTests.cs | 82 +++++++++++++++---- 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs b/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs index 5dce52d907..ebeef44484 100644 --- a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs @@ -528,17 +528,21 @@ public static class EventIntegrationsServiceCollectionExtensions /// True if all required RabbitMQ settings are present; otherwise, false. /// /// Requires all the following settings to be configured: - /// - EventLogging.RabbitMq.HostName - /// - EventLogging.RabbitMq.Username - /// - EventLogging.RabbitMq.Password - /// - EventLogging.RabbitMq.EventExchangeName + /// + /// EventLogging.RabbitMq.HostName + /// EventLogging.RabbitMq.Username + /// EventLogging.RabbitMq.Password + /// EventLogging.RabbitMq.EventExchangeName + /// EventLogging.RabbitMq.IntegrationExchangeName + /// /// internal static bool IsRabbitMqEnabled(GlobalSettings settings) { return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) && CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.IntegrationExchangeName); } /// @@ -547,13 +551,17 @@ public static class EventIntegrationsServiceCollectionExtensions /// The global settings containing Azure Service Bus configuration. /// True if all required Azure Service Bus settings are present; otherwise, false. /// - /// Requires both of the following settings to be configured: - /// - EventLogging.AzureServiceBus.ConnectionString - /// - EventLogging.AzureServiceBus.EventTopicName + /// Requires all of the following settings to be configured: + /// + /// EventLogging.AzureServiceBus.ConnectionString + /// EventLogging.AzureServiceBus.EventTopicName + /// EventLogging.AzureServiceBus.IntegrationTopicName + /// /// internal static bool IsAzureServiceBusEnabled(GlobalSettings settings) { return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName); + CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName) && + CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.IntegrationTopicName); } } diff --git a/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs b/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs index 08fcd23969..0ca2d55c78 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs +++ b/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs @@ -200,7 +200,8 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings)); @@ -214,7 +215,8 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = null, ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings)); @@ -228,7 +230,8 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = null, ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings)); @@ -242,21 +245,38 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = null, - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings)); } [Fact] - public void IsRabbitMqEnabled_MissingExchangeName_ReturnsFalse() + public void IsRabbitMqEnabled_MissingEventExchangeName_ReturnsFalse() { var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = null, + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" + }); + + Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings)); + } + + [Fact] + public void IsRabbitMqEnabled_MissingIntegrationExchangeName_ReturnsFalse() + { + var globalSettings = CreateGlobalSettings(new Dictionary + { + ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", + ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", + ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = null }); Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings)); @@ -268,7 +288,8 @@ public class EventIntegrationServiceCollectionExtensionsTests var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" }); Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings)); @@ -280,19 +301,34 @@ public class EventIntegrationServiceCollectionExtensionsTests var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = null, - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" }); Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings)); } [Fact] - public void IsAzureServiceBusEnabled_MissingTopicName_ReturnsFalse() + public void IsAzureServiceBusEnabled_MissingEventTopicName_ReturnsFalse() { var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = null, + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" + }); + + Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings)); + } + + [Fact] + public void IsAzureServiceBusEnabled_MissingIntegrationTopicName_ReturnsFalse() + { + var globalSettings = CreateGlobalSettings(new Dictionary + { + ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = null }); Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings)); @@ -601,7 +637,8 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); // Add prerequisites @@ -624,7 +661,8 @@ public class EventIntegrationServiceCollectionExtensionsTests var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" }); // Add prerequisites @@ -650,8 +688,10 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration", ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" }); // Add prerequisites @@ -694,7 +734,8 @@ public class EventIntegrationServiceCollectionExtensionsTests var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" }); services.AddEventWriteServices(globalSettings); @@ -712,7 +753,8 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); services.AddEventWriteServices(globalSettings); @@ -769,10 +811,12 @@ public class EventIntegrationServiceCollectionExtensionsTests { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration", ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); services.AddEventWriteServices(globalSettings); @@ -789,7 +833,8 @@ public class EventIntegrationServiceCollectionExtensionsTests var globalSettings = CreateGlobalSettings(new Dictionary { ["GlobalSettings:EventLogging:AzureServiceBus:ConnectionString"] = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", - ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events" + ["GlobalSettings:EventLogging:AzureServiceBus:EventTopicName"] = "events", + ["GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName"] = "integration" }); // Add prerequisites @@ -826,7 +871,8 @@ public class EventIntegrationServiceCollectionExtensionsTests ["GlobalSettings:EventLogging:RabbitMq:HostName"] = "localhost", ["GlobalSettings:EventLogging:RabbitMq:Username"] = "user", ["GlobalSettings:EventLogging:RabbitMq:Password"] = "pass", - ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange" + ["GlobalSettings:EventLogging:RabbitMq:EventExchangeName"] = "exchange", + ["GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName"] = "integration" }); // Add prerequisites From 3cb8472fd226989a4f10e9bc6b71ab8000d714a3 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Wed, 17 Dec 2025 19:31:21 +0000 Subject: [PATCH 09/68] adding platform tag to optimze build, avoiding unnecessary emulation (#6745) --- src/Admin/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Admin/Dockerfile b/src/Admin/Dockerfile index 648ff1be91..84248639cf 100644 --- a/src/Admin/Dockerfile +++ b/src/Admin/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Node.js build stage # ############################################### -FROM node:20-alpine3.21 AS node-build +FROM --platform=$BUILDPLATFORM node:20-alpine3.21 AS node-build WORKDIR /app COPY src/Admin/package*.json ./ From 8aa8bba9a65051047d36adee81b0c6978f410e13 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:29:06 -0700 Subject: [PATCH 10/68] Add feature flag for windows desktop autotype GA (#6717) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 95ab009722..97f463b1b3 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -183,6 +183,7 @@ public static class FeatureFlagKeys public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string InlineMenuTotp = "inline-menu-totp"; public const string WindowsDesktopAutotype = "windows-desktop-autotype"; + public const string WindowsDesktopAutotypeGA = "windows-desktop-autotype-ga"; /* Billing Team */ public const string TrialPayment = "PM-8163-trial-payment"; From d03277323fb9957c3d8cc5ae2d17f07efc9ad0f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:56:13 -0500 Subject: [PATCH 11/68] [deps]: Update actions/stale action to v10 (#6335) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/stale-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 83d492645e..c683400a60 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - name: Check - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: stale-issue-label: "needs-reply" stale-pr-label: "needs-changes" From 982957a2beb4b0fd47c2951637375ab263a504bb Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:12:16 -0600 Subject: [PATCH 12/68] [PM-21421] Support legacy > current plan transition when resubscribing (#6728) * Refactor RestartSubscriptionCommand to support legacy > modern plan transition * Run dotnet format * Claude feedback * Claude feedback --- .../Commands/RestartSubscriptionCommand.cs | 188 +++-- .../RestartSubscriptionCommandTests.cs | 657 +++++++++++++++--- 2 files changed, 706 insertions(+), 139 deletions(-) diff --git a/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs b/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs index 7f7be9d1eb..165b8218a9 100644 --- a/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs +++ b/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs @@ -1,12 +1,13 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; using OneOf.Types; using Stripe; @@ -21,14 +22,14 @@ public interface IRestartSubscriptionCommand } public class RestartSubscriptionCommand( + ILogger logger, IOrganizationRepository organizationRepository, - IProviderRepository providerRepository, + IPricingClient pricingClient, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService, - IUserRepository userRepository) : IRestartSubscriptionCommand + ISubscriberService subscriberService) : BaseBillingCommand(logger), IRestartSubscriptionCommand { - public async Task> Run( - ISubscriber subscriber) + public Task> Run( + ISubscriber subscriber) => HandleAsync(async () => { var existingSubscription = await subscriberService.GetSubscription(subscriber); @@ -37,56 +38,147 @@ public class RestartSubscriptionCommand( return new BadRequest("Cannot restart a subscription that is not canceled."); } + await RestartSubscriptionAsync(subscriber, existingSubscription); + + return new None(); + }); + + private Task RestartSubscriptionAsync( + ISubscriber subscriber, + Subscription canceledSubscription) => subscriber switch + { + Organization organization => RestartOrganizationSubscriptionAsync(organization, canceledSubscription), + _ => throw new NotSupportedException("Only organization subscriptions can be restarted") + }; + + private async Task RestartOrganizationSubscriptionAsync( + Organization organization, + Subscription canceledSubscription) + { + var plans = await pricingClient.ListPlans(); + + var oldPlan = plans.FirstOrDefault(plan => plan.Type == organization.PlanType); + + if (oldPlan == null) + { + throw new ConflictException("Could not find plan for organization's plan type"); + } + + var newPlan = oldPlan.Disabled + ? plans.FirstOrDefault(plan => + plan.ProductTier == oldPlan.ProductTier && + plan.IsAnnual == oldPlan.IsAnnual && + !plan.Disabled) + : oldPlan; + + if (newPlan == null) + { + throw new ConflictException("Could not find the current, enabled plan for organization's tier and cadence"); + } + + if (newPlan.Type != oldPlan.Type) + { + organization.PlanType = newPlan.Type; + organization.Plan = newPlan.Name; + organization.SelfHost = newPlan.HasSelfHost; + organization.UsePolicies = newPlan.HasPolicies; + organization.UseGroups = newPlan.HasGroups; + organization.UseDirectory = newPlan.HasDirectory; + organization.UseEvents = newPlan.HasEvents; + organization.UseTotp = newPlan.HasTotp; + organization.Use2fa = newPlan.Has2fa; + organization.UseApi = newPlan.HasApi; + organization.UseSso = newPlan.HasSso; + organization.UseOrganizationDomains = newPlan.HasOrganizationDomains; + organization.UseKeyConnector = newPlan.HasKeyConnector; + organization.UseScim = newPlan.HasScim; + organization.UseResetPassword = newPlan.HasResetPassword; + organization.UsersGetPremium = newPlan.UsersGetPremium; + organization.UseCustomPermissions = newPlan.HasCustomPermissions; + } + + var items = new List(); + + // Password Manager + var passwordManagerItem = canceledSubscription.Items.FirstOrDefault(item => + item.Price.Id == (oldPlan.HasNonSeatBasedPasswordManagerPlan() + ? oldPlan.PasswordManager.StripePlanId + : oldPlan.PasswordManager.StripeSeatPlanId)); + + if (passwordManagerItem == null) + { + throw new ConflictException("Organization's subscription does not have a Password Manager subscription item."); + } + + items.Add(new SubscriptionItemOptions + { + Price = newPlan.HasNonSeatBasedPasswordManagerPlan() ? newPlan.PasswordManager.StripePlanId : newPlan.PasswordManager.StripeSeatPlanId, + Quantity = passwordManagerItem.Quantity + }); + + // Storage + var storageItem = canceledSubscription.Items.FirstOrDefault( + item => item.Price.Id == oldPlan.PasswordManager.StripeStoragePlanId); + + if (storageItem != null) + { + items.Add(new SubscriptionItemOptions + { + Price = newPlan.PasswordManager.StripeStoragePlanId, + Quantity = storageItem.Quantity + }); + } + + // Secrets Manager & Service Accounts + var secretsManagerItem = oldPlan.SecretsManager != null + ? canceledSubscription.Items.FirstOrDefault(item => + item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId) + : null; + + var serviceAccountsItem = oldPlan.SecretsManager != null + ? canceledSubscription.Items.FirstOrDefault(item => + item.Price.Id == oldPlan.SecretsManager.StripeServiceAccountPlanId) + : null; + + if (newPlan.SecretsManager != null) + { + if (secretsManagerItem != null) + { + items.Add(new SubscriptionItemOptions + { + Price = newPlan.SecretsManager.StripeSeatPlanId, + Quantity = secretsManagerItem.Quantity + }); + } + + if (serviceAccountsItem != null) + { + items.Add(new SubscriptionItemOptions + { + Price = newPlan.SecretsManager.StripeServiceAccountPlanId, + Quantity = serviceAccountsItem.Quantity + }); + } + } + var options = new SubscriptionCreateOptions { AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, CollectionMethod = CollectionMethod.ChargeAutomatically, - Customer = existingSubscription.CustomerId, - Items = existingSubscription.Items.Select(subscriptionItem => new SubscriptionItemOptions - { - Price = subscriptionItem.Price.Id, - Quantity = subscriptionItem.Quantity - }).ToList(), - Metadata = existingSubscription.Metadata, + Customer = canceledSubscription.CustomerId, + Items = items, + Metadata = canceledSubscription.Metadata, OffSession = true, TrialPeriodDays = 0 }; var subscription = await stripeAdapter.CreateSubscriptionAsync(options); - await EnableAsync(subscriber, subscription); - return new None(); - } - private async Task EnableAsync(ISubscriber subscriber, Subscription subscription) - { - switch (subscriber) - { - case Organization organization: - { - organization.GatewaySubscriptionId = subscription.Id; - organization.Enabled = true; - organization.ExpirationDate = subscription.GetCurrentPeriodEnd(); - organization.RevisionDate = DateTime.UtcNow; - await organizationRepository.ReplaceAsync(organization); - break; - } - case Provider provider: - { - provider.GatewaySubscriptionId = subscription.Id; - provider.Enabled = true; - provider.RevisionDate = DateTime.UtcNow; - await providerRepository.ReplaceAsync(provider); - break; - } - case User user: - { - user.GatewaySubscriptionId = subscription.Id; - user.Premium = true; - user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); - user.RevisionDate = DateTime.UtcNow; - await userRepository.ReplaceAsync(user); - break; - } - } + organization.GatewaySubscriptionId = subscription.Id; + organization.Enabled = true; + organization.ExpirationDate = subscription.GetCurrentPeriodEnd(); + organization.RevisionDate = DateTime.UtcNow; + + await organizationRepository.ReplaceAsync(organization); } } diff --git a/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs b/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs index 41f8839eb4..9f34c37b3c 100644 --- a/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs +++ b/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs @@ -1,11 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Subscriptions.Commands; using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Test.Billing.Mocks; using NSubstitute; using Stripe; using Xunit; @@ -17,20 +20,19 @@ using static StripeConstants; public class RestartSubscriptionCommandTests { private readonly IOrganizationRepository _organizationRepository = Substitute.For(); - private readonly IProviderRepository _providerRepository = Substitute.For(); + private readonly IPricingClient _pricingClient = Substitute.For(); private readonly IStripeAdapter _stripeAdapter = Substitute.For(); private readonly ISubscriberService _subscriberService = Substitute.For(); - private readonly IUserRepository _userRepository = Substitute.For(); private readonly RestartSubscriptionCommand _command; public RestartSubscriptionCommandTests() { _command = new RestartSubscriptionCommand( + Substitute.For>(), _organizationRepository, - _providerRepository, + _pricingClient, _stripeAdapter, - _subscriberService, - _userRepository); + _subscriberService); } [Fact] @@ -63,11 +65,56 @@ public class RestartSubscriptionCommandTests } [Fact] - public async Task Run_Organization_Success_ReturnsNone() + public async Task Run_Provider_ReturnsUnhandledWithNotSupportedException() + { + var provider = new Provider { Id = Guid.NewGuid() }; + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123" + }; + + _subscriberService.GetSubscription(provider).Returns(existingSubscription); + + var result = await _command.Run(provider); + + Assert.True(result.IsT3); + var unhandled = result.AsT3; + Assert.IsType(unhandled.Exception); + } + + [Fact] + public async Task Run_User_ReturnsUnhandledWithNotSupportedException() + { + var user = new User { Id = Guid.NewGuid() }; + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123" + }; + + _subscriberService.GetSubscription(user).Returns(existingSubscription); + + var result = await _command.Run(user); + + Assert.True(result.IsT3); + var unhandled = result.AsT3; + Assert.IsType(unhandled.Exception); + } + + [Fact] + public async Task Run_Organization_MissingPasswordManagerItem_ReturnsUnhandledWithConflictException() { var organizationId = Guid.NewGuid(); - var organization = new Organization { Id = organizationId }; - var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.EnterpriseAnnually + }; + + var plan = MockPlans.Get(PlanType.EnterpriseAnnually); var existingSubscription = new Subscription { @@ -77,11 +124,122 @@ public class RestartSubscriptionCommandTests { Data = [ - new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }, - new SubscriptionItem { Price = new Price { Id = "price_2" }, Quantity = 2 } + new SubscriptionItem { Price = new Price { Id = "some-other-price-id" }, Quantity = 10 } ] }, - Metadata = new Dictionary { ["key"] = "value" } + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([plan]); + + var result = await _command.Run(organization); + + Assert.True(result.IsT3); + var unhandled = result.AsT3; + Assert.IsType(unhandled.Exception); + Assert.Equal("Organization's subscription does not have a Password Manager subscription item.", unhandled.Exception.Message); + } + + [Fact] + public async Task Run_Organization_PlanNotFound_ReturnsUnhandledWithConflictException() + { + var organizationId = Guid.NewGuid(); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.EnterpriseAnnually + }; + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = "some-price-id" }, Quantity = 10 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + // Return a plan list that doesn't contain the organization's plan type + _pricingClient.ListPlans().Returns([MockPlans.Get(PlanType.TeamsAnnually)]); + + var result = await _command.Run(organization); + + Assert.True(result.IsT3); + var unhandled = result.AsT3; + Assert.IsType(unhandled.Exception); + Assert.Equal("Could not find plan for organization's plan type", unhandled.Exception.Message); + } + + [Fact] + public async Task Run_Organization_DisabledPlanWithNoEnabledReplacement_ReturnsUnhandledWithConflictException() + { + var organizationId = Guid.NewGuid(); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.EnterpriseAnnually2023 + }; + + var oldPlan = new DisabledEnterprisePlan2023(true); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_old", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + // Return only the disabled plan, with no enabled replacement + _pricingClient.ListPlans().Returns([oldPlan]); + + var result = await _command.Run(organization); + + Assert.True(result.IsT3); + var unhandled = result.AsT3; + Assert.IsType(unhandled.Exception); + Assert.Equal("Could not find the current, enabled plan for organization's tier and cadence", unhandled.Exception.Message); + } + + [Fact] + public async Task Run_Organization_WithNonDisabledPlan_PasswordManagerOnly_Success() + { + var organizationId = Guid.NewGuid(); + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.EnterpriseAnnually + }; + + var plan = MockPlans.Get(PlanType.EnterpriseAnnually); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 10 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } }; var newSubscription = new Subscription @@ -89,30 +247,26 @@ public class RestartSubscriptionCommandTests Id = "sub_new", Items = new StripeList { - Data = - [ - new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } - ] + Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }] } }; _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([plan]); _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); var result = await _command.Run(organization); Assert.True(result.IsT0); - await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is((SubscriptionCreateOptions options) => + await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is(options => options.AutomaticTax.Enabled == true && options.CollectionMethod == CollectionMethod.ChargeAutomatically && options.Customer == "cus_123" && - options.Items.Count == 2 && - options.Items[0].Price == "price_1" && - options.Items[0].Quantity == 1 && - options.Items[1].Price == "price_2" && - options.Items[1].Quantity == 2 && - options.Metadata["key"] == "value" && + options.Items.Count == 1 && + options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.Items[0].Quantity == 10 && + options.Metadata["organizationId"] == organizationId.ToString() && options.OffSession == true && options.TrialPeriodDays == 0)); @@ -120,96 +274,417 @@ public class RestartSubscriptionCommandTests org.Id == organizationId && org.GatewaySubscriptionId == "sub_new" && org.Enabled == true && - org.ExpirationDate == currentPeriodEnd)); + org.ExpirationDate == currentPeriodEnd && + org.PlanType == PlanType.EnterpriseAnnually)); } [Fact] - public async Task Run_Provider_Success_ReturnsNone() + public async Task Run_Organization_WithNonDisabledPlan_WithStorage_Success() { - var providerId = Guid.NewGuid(); - var provider = new Provider { Id = providerId }; - - var existingSubscription = new Subscription - { - Status = SubscriptionStatus.Canceled, - CustomerId = "cus_123", - Items = new StripeList - { - Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }] - }, - Metadata = new Dictionary() - }; - - var newSubscription = new Subscription - { - Id = "sub_new", - Items = new StripeList - { - Data = - [ - new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) } - ] - } - }; - - _subscriberService.GetSubscription(provider).Returns(existingSubscription); - _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); - - var result = await _command.Run(provider); - - Assert.True(result.IsT0); - - await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any()); - - await _providerRepository.Received(1).ReplaceAsync(Arg.Is(prov => - prov.Id == providerId && - prov.GatewaySubscriptionId == "sub_new" && - prov.Enabled == true)); - } - - [Fact] - public async Task Run_User_Success_ReturnsNone() - { - var userId = Guid.NewGuid(); - var user = new User { Id = userId }; + var organizationId = Guid.NewGuid(); var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.TeamsAnnually + }; + + var plan = MockPlans.Get(PlanType.TeamsAnnually); var existingSubscription = new Subscription { Status = SubscriptionStatus.Canceled, - CustomerId = "cus_123", - Items = new StripeList - { - Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }] - }, - Metadata = new Dictionary() - }; - - var newSubscription = new Subscription - { - Id = "sub_new", + CustomerId = "cus_456", Items = new StripeList { Data = [ - new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 5 }, + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 3 } ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + var newSubscription = new Subscription + { + Id = "sub_new_2", + Items = new StripeList + { + Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }] } }; - _subscriberService.GetSubscription(user).Returns(existingSubscription); + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([plan]); _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); - var result = await _command.Run(user); + var result = await _command.Run(organization); Assert.True(result.IsT0); - await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any()); + await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is(options => + options.Items.Count == 2 && + options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.Items[0].Quantity == 5 && + options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId && + options.Items[1].Quantity == 3)); - await _userRepository.Received(1).ReplaceAsync(Arg.Is(u => - u.Id == userId && - u.GatewaySubscriptionId == "sub_new" && - u.Premium == true && - u.PremiumExpirationDate == currentPeriodEnd)); + await _organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => + org.Id == organizationId && + org.GatewaySubscriptionId == "sub_new_2" && + org.Enabled == true)); + } + + [Fact] + public async Task Run_Organization_WithSecretsManager_Success() + { + var organizationId = Guid.NewGuid(); + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.EnterpriseMonthly + }; + + var plan = MockPlans.Get(PlanType.EnterpriseMonthly); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_789", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 15 }, + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 2 }, + new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 10 }, + new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId }, Quantity = 100 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + var newSubscription = new Subscription + { + Id = "sub_new_3", + Items = new StripeList + { + Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }] + } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([plan]); + _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(organization); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is(options => + options.Items.Count == 4 && + options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.Items[0].Quantity == 15 && + options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId && + options.Items[1].Quantity == 2 && + options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId && + options.Items[2].Quantity == 10 && + options.Items[3].Price == plan.SecretsManager.StripeServiceAccountPlanId && + options.Items[3].Quantity == 100)); + + await _organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => + org.Id == organizationId && + org.GatewaySubscriptionId == "sub_new_3" && + org.Enabled == true)); + } + + [Fact] + public async Task Run_Organization_WithDisabledPlan_UpgradesToNewPlan_Success() + { + var organizationId = Guid.NewGuid(); + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.EnterpriseAnnually2023 + }; + + var oldPlan = new DisabledEnterprisePlan2023(true); + var newPlan = MockPlans.Get(PlanType.EnterpriseAnnually); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_old", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 }, + new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeStoragePlanId }, Quantity = 5 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + var newSubscription = new Subscription + { + Id = "sub_upgraded", + Items = new StripeList + { + Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }] + } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([oldPlan, newPlan]); + _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(organization); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is(options => + options.Items.Count == 2 && + options.Items[0].Price == newPlan.PasswordManager.StripeSeatPlanId && + options.Items[0].Quantity == 20 && + options.Items[1].Price == newPlan.PasswordManager.StripeStoragePlanId && + options.Items[1].Quantity == 5)); + + await _organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => + org.Id == organizationId && + org.GatewaySubscriptionId == "sub_upgraded" && + org.Enabled == true && + org.PlanType == PlanType.EnterpriseAnnually && + org.Plan == newPlan.Name && + org.SelfHost == newPlan.HasSelfHost && + org.UsePolicies == newPlan.HasPolicies && + org.UseGroups == newPlan.HasGroups && + org.UseDirectory == newPlan.HasDirectory && + org.UseEvents == newPlan.HasEvents && + org.UseTotp == newPlan.HasTotp && + org.Use2fa == newPlan.Has2fa && + org.UseApi == newPlan.HasApi && + org.UseSso == newPlan.HasSso && + org.UseOrganizationDomains == newPlan.HasOrganizationDomains && + org.UseKeyConnector == newPlan.HasKeyConnector && + org.UseScim == newPlan.HasScim && + org.UseResetPassword == newPlan.HasResetPassword && + org.UsersGetPremium == newPlan.UsersGetPremium && + org.UseCustomPermissions == newPlan.HasCustomPermissions)); + } + + [Fact] + public async Task Run_Organization_WithStorageAndSecretManagerButNoServiceAccounts_Success() + { + var organizationId = Guid.NewGuid(); + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.TeamsAnnually + }; + + var plan = MockPlans.Get(PlanType.TeamsAnnually); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_complex", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 12 }, + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 8 }, + new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 6 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + var newSubscription = new Subscription + { + Id = "sub_complex", + Items = new StripeList + { + Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }] + } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([plan]); + _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(organization); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is(options => + options.Items.Count == 3 && + options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.Items[0].Quantity == 12 && + options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId && + options.Items[1].Quantity == 8 && + options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId && + options.Items[2].Quantity == 6)); + + await _organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => + org.Id == organizationId && + org.GatewaySubscriptionId == "sub_complex" && + org.Enabled == true)); + } + + [Fact] + public async Task Run_Organization_WithSecretsManagerOnly_NoServiceAccounts_Success() + { + var organizationId = Guid.NewGuid(); + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + var organization = new Organization + { + Id = organizationId, + PlanType = PlanType.TeamsMonthly + }; + + var plan = MockPlans.Get(PlanType.TeamsMonthly); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_sm", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 8 }, + new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 5 } + ] + }, + Metadata = new Dictionary { ["organizationId"] = organizationId.ToString() } + }; + + var newSubscription = new Subscription + { + Id = "sub_sm", + Items = new StripeList + { + Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }] + } + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _pricingClient.ListPlans().Returns([plan]); + _stripeAdapter.CreateSubscriptionAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(organization); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is(options => + options.Items.Count == 2 && + options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.Items[0].Quantity == 8 && + options.Items[1].Price == plan.SecretsManager.StripeSeatPlanId && + options.Items[1].Quantity == 5)); + + await _organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => + org.Id == organizationId && + org.GatewaySubscriptionId == "sub_sm" && + org.Enabled == true)); + } + + private record DisabledEnterprisePlan2023 : Bit.Core.Models.StaticStore.Plan + { + public DisabledEnterprisePlan2023(bool isAnnual) + { + Type = PlanType.EnterpriseAnnually2023; + ProductTier = ProductTierType.Enterprise; + Name = "Enterprise (Annually) 2023"; + IsAnnual = isAnnual; + NameLocalizationKey = "planNameEnterprise"; + DescriptionLocalizationKey = "planDescEnterprise"; + CanBeUsedByBusiness = true; + TrialPeriodDays = 7; + HasPolicies = true; + HasSelfHost = true; + HasGroups = true; + HasDirectory = true; + HasEvents = true; + HasTotp = true; + Has2fa = true; + HasApi = true; + HasSso = true; + HasOrganizationDomains = true; + HasKeyConnector = true; + HasScim = true; + HasResetPassword = true; + UsersGetPremium = true; + HasCustomPermissions = true; + UpgradeSortOrder = 4; + DisplaySortOrder = 4; + LegacyYear = 2024; + Disabled = true; + + PasswordManager = new PasswordManagerFeatures(isAnnual); + SecretsManager = new SecretsManagerFeatures(isAnnual); + } + + private record SecretsManagerFeatures : SecretsManagerPlanFeatures + { + public SecretsManagerFeatures(bool isAnnual) + { + BaseSeats = 0; + BasePrice = 0; + BaseServiceAccount = 200; + HasAdditionalSeatsOption = true; + HasAdditionalServiceAccountOption = true; + AllowSeatAutoscale = true; + AllowServiceAccountsAutoscale = true; + + if (isAnnual) + { + StripeSeatPlanId = "secrets-manager-enterprise-seat-annually-2023"; + StripeServiceAccountPlanId = "secrets-manager-service-account-2023-annually"; + SeatPrice = 144; + AdditionalPricePerServiceAccount = 12; + } + else + { + StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly-2023"; + StripeServiceAccountPlanId = "secrets-manager-service-account-2023-monthly"; + SeatPrice = 13; + AdditionalPricePerServiceAccount = 1; + } + } + } + + private record PasswordManagerFeatures : PasswordManagerPlanFeatures + { + public PasswordManagerFeatures(bool isAnnual) + { + BaseSeats = 0; + BaseStorageGb = 1; + HasAdditionalStorageOption = true; + HasAdditionalSeatsOption = true; + AllowSeatAutoscale = true; + + if (isAnnual) + { + AdditionalStoragePricePerGb = 4; + StripeStoragePlanId = "storage-gb-annually"; + StripeSeatPlanId = "2023-enterprise-org-seat-annually-old"; + SeatPrice = 72; + } + else + { + StripeSeatPlanId = "2023-enterprise-seat-monthly-old"; + StripeStoragePlanId = "storage-gb-monthly"; + SeatPrice = 7; + AdditionalStoragePricePerGb = 0.5M; + } + } + } } } From 25eface1b9ebee3d6b86e51532ccbbc49c79cb08 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:35:56 +0100 Subject: [PATCH 13/68] Remove the feature flag (#6720) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 97f463b1b3..12c144c142 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -192,7 +192,6 @@ public static class FeatureFlagKeys public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; - public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page"; public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service"; public const string PM23341_Milestone_2 = "pm-23341-milestone-2"; public const string PM26462_Milestone_3 = "pm-26462-milestone-3"; From 2707a965de664f1a88d3f0c3a0325d6592546469 Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Thu, 18 Dec 2025 19:19:19 +0100 Subject: [PATCH 14/68] Remove additional code review prompt file (#6754) --- .claude/prompts/review-code.md | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .claude/prompts/review-code.md diff --git a/.claude/prompts/review-code.md b/.claude/prompts/review-code.md deleted file mode 100644 index 4e5f40b274..0000000000 --- a/.claude/prompts/review-code.md +++ /dev/null @@ -1,25 +0,0 @@ -Please review this pull request with a focus on: - -- Code quality and best practices -- Potential bugs or issues -- Security implications -- Performance considerations - -Note: The PR branch is already checked out in the current working directory. - -Provide a comprehensive review including: - -- Summary of changes since last review -- Critical issues found (be thorough) -- Suggested improvements (be thorough) -- Good practices observed (be concise - list only the most notable items without elaboration) -- Action items for the author -- Leverage collapsible
sections where appropriate for lengthy explanations or code snippets to enhance human readability - -When reviewing subsequent commits: - -- Track status of previously identified issues (fixed/unfixed/reopened) -- Identify NEW problems introduced since last review -- Note if fixes introduced new issues - -IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively. From 3511ece8990a73984da6dc855b5bf9d379d95aad Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 18 Dec 2025 10:20:46 -0800 Subject: [PATCH 15/68] [PM-28746] Add support for Organization_ItemOrganization_Accepted/Declined event types (#6747) --- src/Events/Controllers/CollectController.cs | 24 +++++++++++++- .../Controllers/CollectControllerTests.cs | 33 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Events/Controllers/CollectController.cs b/src/Events/Controllers/CollectController.cs index bae1575134..3902522665 100644 --- a/src/Events/Controllers/CollectController.cs +++ b/src/Events/Controllers/CollectController.cs @@ -21,17 +21,21 @@ public class CollectController : Controller private readonly IEventService _eventService; private readonly ICipherRepository _cipherRepository; private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; public CollectController( ICurrentContext currentContext, IEventService eventService, ICipherRepository cipherRepository, - IOrganizationRepository organizationRepository) + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository + ) { _currentContext = currentContext; _eventService = eventService; _cipherRepository = cipherRepository; _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; } [HttpPost] @@ -54,6 +58,24 @@ public class CollectController : Controller await _eventService.LogUserEventAsync(_currentContext.UserId.Value, eventModel.Type, eventModel.Date); break; + case EventType.Organization_ItemOrganization_Accepted: + case EventType.Organization_ItemOrganization_Declined: + if (!eventModel.OrganizationId.HasValue || !_currentContext.UserId.HasValue) + { + continue; + } + + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(eventModel.OrganizationId.Value, _currentContext.UserId.Value); + + if (orgUser == null) + { + continue; + } + + await _eventService.LogOrganizationUserEventAsync(orgUser, eventModel.Type, eventModel.Date); + + continue; + // Cipher events case EventType.Cipher_ClientAutofilled: case EventType.Cipher_ClientCopiedHiddenField: diff --git a/test/Events.Test/Controllers/CollectControllerTests.cs b/test/Events.Test/Controllers/CollectControllerTests.cs index 325442d50e..b6fa018623 100644 --- a/test/Events.Test/Controllers/CollectControllerTests.cs +++ b/test/Events.Test/Controllers/CollectControllerTests.cs @@ -1,6 +1,7 @@ using AutoFixture.Xunit2; using Bit.Core.AdminConsole.Entities; using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; @@ -9,6 +10,7 @@ using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Events.Controllers; using Bit.Events.Models; +using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Mvc; using NSubstitute; @@ -21,6 +23,7 @@ public class CollectControllerTests private readonly IEventService _eventService; private readonly ICipherRepository _cipherRepository; private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; public CollectControllerTests() { @@ -28,12 +31,14 @@ public class CollectControllerTests _eventService = Substitute.For(); _cipherRepository = Substitute.For(); _organizationRepository = Substitute.For(); + _organizationUserRepository = Substitute.For(); _sut = new CollectController( _currentContext, _eventService, _cipherRepository, - _organizationRepository + _organizationRepository, + _organizationUserRepository ); } @@ -74,6 +79,32 @@ public class CollectControllerTests await _eventService.Received(1).LogUserEventAsync(userId, EventType.User_ClientExportedVault, eventDate); } + [Theory] + [BitAutoData(EventType.Organization_ItemOrganization_Accepted)] + [BitAutoData(EventType.Organization_ItemOrganization_Declined)] + public async Task Post_Organization_ItemOrganization_LogsOrganizationUserEvent( + EventType type, Guid userId, Guid orgId, OrganizationUser orgUser) + { + _currentContext.UserId.Returns(userId); + orgUser.OrganizationId = orgId; + _organizationUserRepository.GetByOrganizationAsync(orgId, userId).Returns(orgUser); + var eventDate = DateTime.UtcNow; + var events = new List + { + new EventModel + { + Type = type, + OrganizationId = orgId, + Date = eventDate + } + }; + + var result = await _sut.Post(events); + + Assert.IsType(result); + await _eventService.Received(1).LogOrganizationUserEventAsync(orgUser, type, eventDate); + } + [Theory] [AutoData] public async Task Post_CipherAutofilled_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails) From 2b742b0343e9a01e99d674476fb16ea79c506214 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:32:03 +0100 Subject: [PATCH 16/68] [PM-27605] Populate MaxStorageGbIncreased for storage increase from 1GB to 5GB. (#6579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add transition migration to populate MaxStorageGbIncreased This migration populates the MaxStorageGbIncreased column for Users and Organizations by setting it to MaxStorageGb + 4, representing the storage increase from 1GB to 5GB. This migration depends on PM-27603 being deployed first to create the MaxStorageGbIncreased column. Target release: January 6th, 2026 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Using batched updates to reduce lock * Add changes base on ticket adjustment * Added the dependency check * Add temporary index for performance * Resolved the conflict * resolve the syntax error * Update the migration script * Rename the file to updated date * Revert the existing merge file change * revert the change * revert renaming * rename file to updated date * Add the column after renaming * Rename other migration file to set current date --------- Co-authored-by: Claude Co-authored-by: Alex Morask Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../Stored Procedures/Organization_Create.sql | 6 +- .../Stored Procedures/Organization_Update.sql | 3 +- src/Sql/dbo/Stored Procedures/User_Create.sql | 6 +- src/Sql/dbo/Stored Procedures/User_Update.sql | 3 +- ...12-12_00_PopulateMaxStorageGbIncreased.sql | 121 ++++ ...oredProceduresForMaxStorageGbIncreased.sql | 600 ++++++++++++++++++ 6 files changed, 733 insertions(+), 6 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-12-12_00_PopulateMaxStorageGbIncreased.sql create mode 100644 util/Migrator/DbScripts/2025-12-12_00_UpdateStoredProceduresForMaxStorageGbIncreased.sql diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index decd406280..4fc4681648 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -128,7 +128,8 @@ BEGIN [UseAdminSponsoredFamilies], [SyncSeats], [UseAutomaticUserConfirmation], - [UsePhishingBlocker] + [UsePhishingBlocker], + [MaxStorageGbIncreased] ) VALUES ( @@ -193,6 +194,7 @@ BEGIN @UseAdminSponsoredFamilies, @SyncSeats, @UseAutomaticUserConfirmation, - @UsePhishingBlocker + @UsePhishingBlocker, + @MaxStorageGb ); END diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index 9fd1b59460..946cf03e94 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -128,7 +128,8 @@ BEGIN [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies, [SyncSeats] = @SyncSeats, [UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation, - [UsePhishingBlocker] = @UsePhishingBlocker + [UsePhishingBlocker] = @UsePhishingBlocker, + [MaxStorageGbIncreased] = @MaxStorageGb WHERE [Id] = @Id; END diff --git a/src/Sql/dbo/Stored Procedures/User_Create.sql b/src/Sql/dbo/Stored Procedures/User_Create.sql index 2573bf1a0a..cf0c12d1c5 100644 --- a/src/Sql/dbo/Stored Procedures/User_Create.sql +++ b/src/Sql/dbo/Stored Procedures/User_Create.sql @@ -96,7 +96,8 @@ BEGIN [VerifyDevices], [SecurityState], [SecurityVersion], - [SignedPublicKey] + [SignedPublicKey], + [MaxStorageGbIncreased] ) VALUES ( @@ -145,6 +146,7 @@ BEGIN @VerifyDevices, @SecurityState, @SecurityVersion, - @SignedPublicKey + @SignedPublicKey, + @MaxStorageGb ) END diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql index 5097bc538e..05e0d4b4de 100644 --- a/src/Sql/dbo/Stored Procedures/User_Update.sql +++ b/src/Sql/dbo/Stored Procedures/User_Update.sql @@ -96,7 +96,8 @@ BEGIN [VerifyDevices] = @VerifyDevices, [SecurityState] = @SecurityState, [SecurityVersion] = @SecurityVersion, - [SignedPublicKey] = @SignedPublicKey + [SignedPublicKey] = @SignedPublicKey, + [MaxStorageGbIncreased] = @MaxStorageGb WHERE [Id] = @Id END diff --git a/util/Migrator/DbScripts/2025-12-12_00_PopulateMaxStorageGbIncreased.sql b/util/Migrator/DbScripts/2025-12-12_00_PopulateMaxStorageGbIncreased.sql new file mode 100644 index 0000000000..8e3031ccf4 --- /dev/null +++ b/util/Migrator/DbScripts/2025-12-12_00_PopulateMaxStorageGbIncreased.sql @@ -0,0 +1,121 @@ + -- ======================================== + -- Dependency Validation + -- ======================================== + IF NOT EXISTS ( + SELECT 1 FROM sys.columns + WHERE object_id = OBJECT_ID('[dbo].[User]') + AND name = 'MaxStorageGbIncreased' + ) + BEGIN + RAISERROR('MaxStorageGbIncreased column does not exist in User table. PM-27603 must be deployed first.', 16, 1); + RETURN; + END; + + IF NOT EXISTS ( + SELECT 1 FROM sys.columns + WHERE object_id = OBJECT_ID('[dbo].[Organization]') + AND name = 'MaxStorageGbIncreased' + ) + BEGIN + RAISERROR('MaxStorageGbIncreased column does not exist in Organization table. PM-27603 must be deployed first.', 16, 1); + RETURN; + END; + GO + + -- ======================================== + -- User Table Migration + -- ======================================== + + -- Create temporary index for performance + IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID('dbo.User') + AND name = 'IX_TEMP_User_MaxStorageGb_MaxStorageGbIncreased' + ) + BEGIN + PRINT 'Creating temporary index on User table...'; + CREATE INDEX IX_TEMP_User_MaxStorageGb_MaxStorageGbIncreased + ON [dbo].[User]([MaxStorageGb], [MaxStorageGbIncreased]); + PRINT 'Temporary index created.'; + END + GO + + -- Populate MaxStorageGbIncreased for Users in batches + DECLARE @BatchSize INT = 5000; + DECLARE @RowsAffected INT = 1; + DECLARE @TotalUpdated INT = 0; + + PRINT 'Starting User table update...'; + + WHILE @RowsAffected > 0 + BEGIN + UPDATE TOP (@BatchSize) [dbo].[User] + SET [MaxStorageGbIncreased] = [MaxStorageGb] + 4 + WHERE [MaxStorageGb] IS NOT NULL + AND [MaxStorageGbIncreased] IS NULL; + + SET @RowsAffected = @@ROWCOUNT; + SET @TotalUpdated = @TotalUpdated + @RowsAffected; + + PRINT 'Users updated: ' + CAST(@TotalUpdated AS VARCHAR(10)); + + WAITFOR DELAY '00:00:00.100'; -- 100ms delay to reduce contention + END + + PRINT 'User table update complete. Total rows updated: ' + CAST(@TotalUpdated AS VARCHAR(10)); + GO + + -- Drop temporary index + DROP INDEX IF EXISTS [dbo].[User].[IX_TEMP_User_MaxStorageGb_MaxStorageGbIncreased]; + PRINT 'Temporary index on User table dropped.'; + GO + + -- ======================================== + -- Organization Table Migration + -- ======================================== + + -- Create temporary index for performance + IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID('dbo.Organization') + AND name = 'IX_TEMP_Organization_MaxStorageGb_MaxStorageGbIncreased' + ) + BEGIN + PRINT 'Creating temporary index on Organization table...'; + CREATE INDEX IX_TEMP_Organization_MaxStorageGb_MaxStorageGbIncreased + ON [dbo].[Organization]([MaxStorageGb], [MaxStorageGbIncreased]); + PRINT 'Temporary index created.'; + END + GO + + -- Populate MaxStorageGbIncreased for Organizations in batches + DECLARE @BatchSize INT = 5000; + DECLARE @RowsAffected INT = 1; + DECLARE @TotalUpdated INT = 0; + + PRINT 'Starting Organization table update...'; + + WHILE @RowsAffected > 0 + BEGIN + UPDATE TOP (@BatchSize) [dbo].[Organization] + SET [MaxStorageGbIncreased] = [MaxStorageGb] + 4 + WHERE [MaxStorageGb] IS NOT NULL + AND [MaxStorageGbIncreased] IS NULL; + + SET @RowsAffected = @@ROWCOUNT; + SET @TotalUpdated = @TotalUpdated + @RowsAffected; + + PRINT 'Organizations updated: ' + CAST(@TotalUpdated AS VARCHAR(10)); + + WAITFOR DELAY '00:00:00.100'; -- 100ms delay to reduce contention + END + + PRINT 'Organization table update complete. Total rows updated: ' + CAST(@TotalUpdated AS VARCHAR(10)); + GO + + -- Drop temporary index + DROP INDEX IF EXISTS [dbo].[Organization].[IX_TEMP_Organization_MaxStorageGb_MaxStorageGbIncreased]; + PRINT 'Temporary index on Organization table dropped.'; + GO \ No newline at end of file diff --git a/util/Migrator/DbScripts/2025-12-12_00_UpdateStoredProceduresForMaxStorageGbIncreased.sql b/util/Migrator/DbScripts/2025-12-12_00_UpdateStoredProceduresForMaxStorageGbIncreased.sql new file mode 100644 index 0000000000..b679314e40 --- /dev/null +++ b/util/Migrator/DbScripts/2025-12-12_00_UpdateStoredProceduresForMaxStorageGbIncreased.sql @@ -0,0 +1,600 @@ +-- Update stored procedures to set MaxStorageGbIncreased column +-- This ensures that going forward, MaxStorageGbIncreased is kept in sync with MaxStorageGb + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT= null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, + @UseAdminSponsoredFamilies BIT = 0, + @SyncSeats BIT = 0, + @UseAutomaticUserConfirmation BIT = 0, + @UsePhishingBlocker BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector], + [UseScim], + [UseCustomPermissions], + [UseSecretsManager], + [Status], + [UsePasswordManager], + [SmSeats], + [SmServiceAccounts], + [MaxAutoscaleSmSeats], + [MaxAutoscaleSmServiceAccounts], + [SecretsManagerBeta], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies], + [SyncSeats], + [UseAutomaticUserConfirmation], + [UsePhishingBlocker], + [MaxStorageGbIncreased] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector, + @UseScim, + @UseCustomPermissions, + @UseSecretsManager, + @Status, + @UsePasswordManager, + @SmSeats, + @SmServiceAccounts, + @MaxAutoscaleSmSeats, + @MaxAutoscaleSmServiceAccounts, + @SecretsManagerBeta, + @LimitCollectionCreation, + @LimitCollectionDeletion, + @AllowAdminAccessToAllCollectionItems, + @UseRiskInsights, + @LimitItemDeletion, + @UseOrganizationDomains, + @UseAdminSponsoredFamilies, + @SyncSeats, + @UseAutomaticUserConfirmation, + @UsePhishingBlocker, + @MaxStorageGb + ); +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT = null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, + @UseAdminSponsoredFamilies BIT = 0, + @SyncSeats BIT = 0, + @UseAutomaticUserConfirmation BIT = 0, + @UsePhishingBlocker BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector, + [UseScim] = @UseScim, + [UseCustomPermissions] = @UseCustomPermissions, + [UseSecretsManager] = @UseSecretsManager, + [Status] = @Status, + [UsePasswordManager] = @UsePasswordManager, + [SmSeats] = @SmSeats, + [SmServiceAccounts] = @SmServiceAccounts, + [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, + [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, + [SecretsManagerBeta] = @SecretsManagerBeta, + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, + [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, + [UseRiskInsights] = @UseRiskInsights, + [LimitItemDeletion] = @LimitItemDeletion, + [UseOrganizationDomains] = @UseOrganizationDomains, + [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies, + [SyncSeats] = @SyncSeats, + [UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation, + [UsePhishingBlocker] = @UsePhishingBlocker, + [MaxStorageGbIncreased] = @MaxStorageGb + WHERE + [Id] = @Id; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[User_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Name NVARCHAR(50), + @Email NVARCHAR(256), + @EmailVerified BIT, + @MasterPassword NVARCHAR(300), + @MasterPasswordHint NVARCHAR(50), + @Culture NVARCHAR(10), + @SecurityStamp NVARCHAR(50), + @TwoFactorProviders NVARCHAR(MAX), + @TwoFactorRecoveryCode NVARCHAR(32), + @EquivalentDomains NVARCHAR(MAX), + @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), + @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), + @PublicKey NVARCHAR(MAX), + @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @PremiumExpirationDate DATETIME2(7), + @RenewalReminderDate DATETIME2(7), + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @LicenseKey VARCHAR(100), + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT = NULL, + @KdfParallelism INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30), + @ForcePasswordReset BIT = 0, + @UsesKeyConnector BIT = 0, + @FailedLoginCount INT = 0, + @LastFailedLoginDate DATETIME2(7), + @AvatarColor VARCHAR(7) = NULL, + @LastPasswordChangeDate DATETIME2(7) = NULL, + @LastKdfChangeDate DATETIME2(7) = NULL, + @LastKeyRotationDate DATETIME2(7) = NULL, + @LastEmailChangeDate DATETIME2(7) = NULL, + @VerifyDevices BIT = 1, + @SecurityState VARCHAR(MAX) = NULL, + @SecurityVersion INT = NULL, + @SignedPublicKey VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[User] + ( + [Id], + [Name], + [Email], + [EmailVerified], + [MasterPassword], + [MasterPasswordHint], + [Culture], + [SecurityStamp], + [TwoFactorProviders], + [TwoFactorRecoveryCode], + [EquivalentDomains], + [ExcludedGlobalEquivalentDomains], + [AccountRevisionDate], + [Key], + [PublicKey], + [PrivateKey], + [Premium], + [PremiumExpirationDate], + [RenewalReminderDate], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [LicenseKey], + [Kdf], + [KdfIterations], + [CreationDate], + [RevisionDate], + [ApiKey], + [ForcePasswordReset], + [UsesKeyConnector], + [FailedLoginCount], + [LastFailedLoginDate], + [AvatarColor], + [KdfMemory], + [KdfParallelism], + [LastPasswordChangeDate], + [LastKdfChangeDate], + [LastKeyRotationDate], + [LastEmailChangeDate], + [VerifyDevices], + [SecurityState], + [SecurityVersion], + [SignedPublicKey], + [MaxStorageGbIncreased] + ) + VALUES + ( + @Id, + @Name, + @Email, + @EmailVerified, + @MasterPassword, + @MasterPasswordHint, + @Culture, + @SecurityStamp, + @TwoFactorProviders, + @TwoFactorRecoveryCode, + @EquivalentDomains, + @ExcludedGlobalEquivalentDomains, + @AccountRevisionDate, + @Key, + @PublicKey, + @PrivateKey, + @Premium, + @PremiumExpirationDate, + @RenewalReminderDate, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @LicenseKey, + @Kdf, + @KdfIterations, + @CreationDate, + @RevisionDate, + @ApiKey, + @ForcePasswordReset, + @UsesKeyConnector, + @FailedLoginCount, + @LastFailedLoginDate, + @AvatarColor, + @KdfMemory, + @KdfParallelism, + @LastPasswordChangeDate, + @LastKdfChangeDate, + @LastKeyRotationDate, + @LastEmailChangeDate, + @VerifyDevices, + @SecurityState, + @SecurityVersion, + @SignedPublicKey, + @MaxStorageGb + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[User_Update] + @Id UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @Email NVARCHAR(256), + @EmailVerified BIT, + @MasterPassword NVARCHAR(300), + @MasterPasswordHint NVARCHAR(50), + @Culture NVARCHAR(10), + @SecurityStamp NVARCHAR(50), + @TwoFactorProviders NVARCHAR(MAX), + @TwoFactorRecoveryCode NVARCHAR(32), + @EquivalentDomains NVARCHAR(MAX), + @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), + @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), + @PublicKey NVARCHAR(MAX), + @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @PremiumExpirationDate DATETIME2(7), + @RenewalReminderDate DATETIME2(7), + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @LicenseKey VARCHAR(100), + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT = NULL, + @KdfParallelism INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30), + @ForcePasswordReset BIT = 0, + @UsesKeyConnector BIT = 0, + @FailedLoginCount INT, + @LastFailedLoginDate DATETIME2(7), + @AvatarColor VARCHAR(7), + @LastPasswordChangeDate DATETIME2(7) = NULL, + @LastKdfChangeDate DATETIME2(7) = NULL, + @LastKeyRotationDate DATETIME2(7) = NULL, + @LastEmailChangeDate DATETIME2(7) = NULL, + @VerifyDevices BIT = 1, + @SecurityState VARCHAR(MAX) = NULL, + @SecurityVersion INT = NULL, + @SignedPublicKey VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[User] + SET + [Name] = @Name, + [Email] = @Email, + [EmailVerified] = @EmailVerified, + [MasterPassword] = @MasterPassword, + [MasterPasswordHint] = @MasterPasswordHint, + [Culture] = @Culture, + [SecurityStamp] = @SecurityStamp, + [TwoFactorProviders] = @TwoFactorProviders, + [TwoFactorRecoveryCode] = @TwoFactorRecoveryCode, + [EquivalentDomains] = @EquivalentDomains, + [ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains, + [AccountRevisionDate] = @AccountRevisionDate, + [Key] = @Key, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [Premium] = @Premium, + [PremiumExpirationDate] = @PremiumExpirationDate, + [RenewalReminderDate] = @RenewalReminderDate, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [LicenseKey] = @LicenseKey, + [Kdf] = @Kdf, + [KdfIterations] = @KdfIterations, + [KdfMemory] = @KdfMemory, + [KdfParallelism] = @KdfParallelism, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [ApiKey] = @ApiKey, + [ForcePasswordReset] = @ForcePasswordReset, + [UsesKeyConnector] = @UsesKeyConnector, + [FailedLoginCount] = @FailedLoginCount, + [LastFailedLoginDate] = @LastFailedLoginDate, + [AvatarColor] = @AvatarColor, + [LastPasswordChangeDate] = @LastPasswordChangeDate, + [LastKdfChangeDate] = @LastKdfChangeDate, + [LastKeyRotationDate] = @LastKeyRotationDate, + [LastEmailChangeDate] = @LastEmailChangeDate, + [VerifyDevices] = @VerifyDevices, + [SecurityState] = @SecurityState, + [SecurityVersion] = @SecurityVersion, + [SignedPublicKey] = @SignedPublicKey, + [MaxStorageGbIncreased] = @MaxStorageGb + WHERE + [Id] = @Id +END +GO From a92d7ac129612e25ac17cc5c3a447d8a14c41db0 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:43:03 +0100 Subject: [PATCH 17/68] [PM-27280] Support v2 encryption on key-connector signups (#6712) * account v2 registration for key connector * use new user repository functions * test coverage * integration test coverage * documentation * code review * missing test coverage * fix failing test * failing test * incorrect ticket number * moved back request model to Api, created dedicated data class in Core * sql stored procedure type mismatch, simplification * key connector authorization handler --- .../AccountsKeyManagementController.cs | 34 +- .../SetKeyConnectorKeyRequestModel.cs | 116 ++++-- src/Core/Constants.cs | 1 + .../KeyConnectorAuthorizationHandler.cs | 52 +++ .../Authorization/KeyConnectorOperations.cs | 16 + .../Interfaces/ISetKeyConnectorKeyCommand.cs | 13 + .../Commands/SetKeyConnectorKeyCommand.cs | 60 ++++ ...eyManagementServiceCollectionExtensions.cs | 11 +- .../Models/Data/KeyConnectorKeysData.cs | 12 + src/Core/Repositories/IUserRepository.cs | 2 + src/Core/Services/IUserService.cs | 2 + .../Services/Implementations/UserService.cs | 1 + .../Repositories/UserRepository.cs | 27 ++ .../Repositories/UserRepository.cs | 31 ++ .../User_UpdateKeyConnectorUserKey.sql | 28 ++ .../AccountsKeyManagementControllerTests.cs | 115 +++++- .../AccountsKeyManagementControllerTests.cs | 120 ++++++- .../SetKeyConnectorKeyRequestModelTests.cs | 333 ++++++++++++++++++ .../KeyConnectorAuthorizationHandlerTests.cs | 151 ++++++++ .../SetKeyConnectorKeyCommandTests.cs | 125 +++++++ .../Repositories/UserRepositoryTests.cs | 54 ++- ...2-17_00_User_UpdateKeyConnectorUserKey.sql | 29 ++ 22 files changed, 1283 insertions(+), 50 deletions(-) create mode 100644 src/Core/KeyManagement/Authorization/KeyConnectorAuthorizationHandler.cs create mode 100644 src/Core/KeyManagement/Authorization/KeyConnectorOperations.cs create mode 100644 src/Core/KeyManagement/Commands/Interfaces/ISetKeyConnectorKeyCommand.cs create mode 100644 src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs create mode 100644 src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs create mode 100644 src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateKeyConnectorUserKey.sql create mode 100644 test/Api.Test/KeyManagement/Models/Request/SetKeyConnectorKeyRequestModelTests.cs create mode 100644 test/Core.Test/KeyManagement/Authorization/KeyConnectorAuthorizationHandlerTests.cs create mode 100644 test/Core.Test/KeyManagement/Commands/SetKeyConnectorKeyCommandTests.cs rename test/Infrastructure.IntegrationTest/{Auth => }/Repositories/UserRepositoryTests.cs (91%) create mode 100644 util/Migrator/DbScripts/2025-12-17_00_User_UpdateKeyConnectorUserKey.sql diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index b944cdd052..a124616e30 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -47,6 +47,7 @@ public class AccountsKeyManagementController : Controller _webauthnKeyValidator; private readonly IRotationValidator, IEnumerable> _deviceValidator; private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery; + private readonly ISetKeyConnectorKeyCommand _setKeyConnectorKeyCommand; public AccountsKeyManagementController(IUserService userService, IFeatureService featureService, @@ -62,8 +63,10 @@ public class AccountsKeyManagementController : Controller emergencyAccessValidator, IRotationValidator, IReadOnlyList> organizationUserValidator, - IRotationValidator, IEnumerable> webAuthnKeyValidator, - IRotationValidator, IEnumerable> deviceValidator) + IRotationValidator, IEnumerable> + webAuthnKeyValidator, + IRotationValidator, IEnumerable> deviceValidator, + ISetKeyConnectorKeyCommand setKeyConnectorKeyCommand) { _userService = userService; _featureService = featureService; @@ -79,6 +82,7 @@ public class AccountsKeyManagementController : Controller _webauthnKeyValidator = webAuthnKeyValidator; _deviceValidator = deviceValidator; _keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery; + _setKeyConnectorKeyCommand = setKeyConnectorKeyCommand; } [HttpPost("key-management/regenerate-keys")] @@ -146,18 +150,28 @@ public class AccountsKeyManagementController : Controller throw new UnauthorizedAccessException(); } - var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier); - if (result.Succeeded) + if (model.IsV2Request()) { - return; + // V2 account registration + await _setKeyConnectorKeyCommand.SetKeyConnectorKeyForUserAsync(user, model.ToKeyConnectorKeysData()); } - - foreach (var error in result.Errors) + else { - ModelState.AddModelError(string.Empty, error.Description); - } + // V1 account registration + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328 + var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier); + if (result.Succeeded) + { + return; + } - throw new BadRequestException(ModelState); + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + + throw new BadRequestException(ModelState); + } } [HttpPost("convert-to-key-connector")] diff --git a/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs b/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs index 9f52a97383..6cd13fdf83 100644 --- a/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs @@ -1,36 +1,112 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Api.Request; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; namespace Bit.Api.KeyManagement.Models.Requests; -public class SetKeyConnectorKeyRequestModel +public class SetKeyConnectorKeyRequestModel : IValidatableObject { - [Required] - public string Key { get; set; } - [Required] - public KeysRequestModel Keys { get; set; } - [Required] - public KdfType Kdf { get; set; } - [Required] - public int KdfIterations { get; set; } - public int? KdfMemory { get; set; } - public int? KdfParallelism { get; set; } - [Required] - public string OrgIdentifier { get; set; } + // TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27328 + [Obsolete("Use KeyConnectorKeyWrappedUserKey instead")] + public string? Key { get; set; } + [Obsolete("Use AccountKeys instead")] + public KeysRequestModel? Keys { get; set; } + [Obsolete("Not used anymore")] + public KdfType? Kdf { get; set; } + [Obsolete("Not used anymore")] + public int? KdfIterations { get; set; } + [Obsolete("Not used anymore")] + public int? KdfMemory { get; set; } + [Obsolete("Not used anymore")] + public int? KdfParallelism { get; set; } + + [EncryptedString] + public string? KeyConnectorKeyWrappedUserKey { get; set; } + public AccountKeysRequestModel? AccountKeys { get; set; } + + [Required] + public required string OrgIdentifier { get; init; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (IsV2Request()) + { + // V2 registration + yield break; + } + + // V1 registration + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328 + if (string.IsNullOrEmpty(Key)) + { + yield return new ValidationResult("Key must be supplied."); + } + + if (Keys == null) + { + yield return new ValidationResult("Keys must be supplied."); + } + + if (Kdf == null) + { + yield return new ValidationResult("Kdf must be supplied."); + } + + if (KdfIterations == null) + { + yield return new ValidationResult("KdfIterations must be supplied."); + } + + if (Kdf == KdfType.Argon2id) + { + if (KdfMemory == null) + { + yield return new ValidationResult("KdfMemory must be supplied when Kdf is Argon2id."); + } + + if (KdfParallelism == null) + { + yield return new ValidationResult("KdfParallelism must be supplied when Kdf is Argon2id."); + } + } + } + + public bool IsV2Request() + { + return !string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) && AccountKeys != null; + } + + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328 public User ToUser(User existingUser) { - existingUser.Kdf = Kdf; - existingUser.KdfIterations = KdfIterations; + existingUser.Kdf = Kdf!.Value; + existingUser.KdfIterations = KdfIterations!.Value; existingUser.KdfMemory = KdfMemory; existingUser.KdfParallelism = KdfParallelism; existingUser.Key = Key; - Keys.ToUser(existingUser); + Keys!.ToUser(existingUser); return existingUser; } + + public KeyConnectorKeysData ToKeyConnectorKeysData() + { + // TODO remove validation with https://bitwarden.atlassian.net/browse/PM-27328 + if (string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) || AccountKeys == null) + { + throw new BadRequestException("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied."); + } + + return new KeyConnectorKeysData + { + KeyConnectorKeyWrappedUserKey = KeyConnectorKeyWrappedUserKey, + AccountKeys = AccountKeys, + OrgIdentifier = OrgIdentifier + }; + } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 12c144c142..94f599a2a6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -212,6 +212,7 @@ public static class FeatureFlagKeys public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component"; public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit"; public const string DataRecoveryTool = "pm-28813-data-recovery-tool"; + public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration"; /* Mobile Team */ public const string AndroidImportLoginsFlow = "import-logins-flow"; diff --git a/src/Core/KeyManagement/Authorization/KeyConnectorAuthorizationHandler.cs b/src/Core/KeyManagement/Authorization/KeyConnectorAuthorizationHandler.cs new file mode 100644 index 0000000000..7937390a8c --- /dev/null +++ b/src/Core/KeyManagement/Authorization/KeyConnectorAuthorizationHandler.cs @@ -0,0 +1,52 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.KeyManagement.Authorization; + +public class KeyConnectorAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public KeyConnectorAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + KeyConnectorOperationsRequirement requirement, + User user) + { + var authorized = requirement switch + { + not null when requirement == KeyConnectorOperations.Use => CanUse(user), + _ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement)) + }; + + if (authorized) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + + private bool CanUse(User user) + { + // User cannot use Key Connector if they already use it + if (user.UsesKeyConnector) + { + return false; + } + + // User cannot use Key Connector if they are an owner or admin of any organization + if (_currentContext.Organizations.Any(u => + u.Type is OrganizationUserType.Owner or OrganizationUserType.Admin)) + { + return false; + } + + return true; + } +} diff --git a/src/Core/KeyManagement/Authorization/KeyConnectorOperations.cs b/src/Core/KeyManagement/Authorization/KeyConnectorOperations.cs new file mode 100644 index 0000000000..a8d09a6ac7 --- /dev/null +++ b/src/Core/KeyManagement/Authorization/KeyConnectorOperations.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.KeyManagement.Authorization; + +public class KeyConnectorOperationsRequirement : OperationAuthorizationRequirement +{ + public KeyConnectorOperationsRequirement(string name) + { + Name = name; + } +} + +public static class KeyConnectorOperations +{ + public static readonly KeyConnectorOperationsRequirement Use = new(nameof(Use)); +} diff --git a/src/Core/KeyManagement/Commands/Interfaces/ISetKeyConnectorKeyCommand.cs b/src/Core/KeyManagement/Commands/Interfaces/ISetKeyConnectorKeyCommand.cs new file mode 100644 index 0000000000..65f6cddeb5 --- /dev/null +++ b/src/Core/KeyManagement/Commands/Interfaces/ISetKeyConnectorKeyCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Commands.Interfaces; + +/// +/// Creates the user key and account cryptographic state for a new user registering +/// with Key Connector SSO configuration. +/// +public interface ISetKeyConnectorKeyCommand +{ + Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData); +} diff --git a/src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs b/src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs new file mode 100644 index 0000000000..a96042de30 --- /dev/null +++ b/src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs @@ -0,0 +1,60 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Authorization; +using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.KeyManagement.Commands; + +public class SetKeyConnectorKeyCommand : ISetKeyConnectorKeyCommand +{ + private readonly IAuthorizationService _authorizationService; + private readonly ICurrentContext _currentContext; + private readonly IEventService _eventService; + private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; + private readonly IUserService _userService; + private readonly IUserRepository _userRepository; + + public SetKeyConnectorKeyCommand( + IAuthorizationService authorizationService, + ICurrentContext currentContext, + IEventService eventService, + IAcceptOrgUserCommand acceptOrgUserCommand, + IUserService userService, + IUserRepository userRepository) + { + _authorizationService = authorizationService; + _currentContext = currentContext; + _eventService = eventService; + _acceptOrgUserCommand = acceptOrgUserCommand; + _userService = userService; + _userRepository = userRepository; + } + + public async Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData) + { + var authorizationResult = await _authorizationService.AuthorizeAsync(_currentContext.HttpContext.User, user, + KeyConnectorOperations.Use); + if (!authorizationResult.Succeeded) + { + throw new BadRequestException("Cannot use Key Connector"); + } + + var setKeyConnectorUserKeyTask = + _userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorKeysData.KeyConnectorKeyWrappedUserKey); + + await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, + keyConnectorKeysData.AccountKeys.ToAccountKeysData(), [setKeyConnectorUserKeyTask]); + + await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector); + + await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(keyConnectorKeysData.OrgIdentifier, user, + _userService); + } +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs index abaf9406ba..96f990c299 100644 --- a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -1,9 +1,11 @@ -using Bit.Core.KeyManagement.Commands; +using Bit.Core.KeyManagement.Authorization; +using Bit.Core.KeyManagement.Commands; using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Kdf; using Bit.Core.KeyManagement.Kdf.Implementations; using Bit.Core.KeyManagement.Queries; using Bit.Core.KeyManagement.Queries.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.KeyManagement; @@ -12,15 +14,22 @@ public static class KeyManagementServiceCollectionExtensions { public static void AddKeyManagementServices(this IServiceCollection services) { + services.AddKeyManagementAuthorizationHandlers(); services.AddKeyManagementCommands(); services.AddKeyManagementQueries(); services.AddSendPasswordServices(); } + private static void AddKeyManagementAuthorizationHandlers(this IServiceCollection services) + { + services.AddScoped(); + } + private static void AddKeyManagementCommands(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void AddKeyManagementQueries(this IServiceCollection services) diff --git a/src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs b/src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs new file mode 100644 index 0000000000..5675c6bc96 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs @@ -0,0 +1,12 @@ +using Bit.Core.KeyManagement.Models.Api.Request; + +namespace Bit.Core.KeyManagement.Models.Data; + +public class KeyConnectorKeysData +{ + public required string KeyConnectorKeyWrappedUserKey { get; set; } + + public required AccountKeysRequestModel AccountKeys { get; set; } + + public required string OrgIdentifier { get; init; } +} diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 47ddb86f8e..93316d78bd 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -72,6 +72,8 @@ public interface IUserRepository : IRepository UserAccountKeysData accountKeysData, IEnumerable? updateUserDataActions = null); Task DeleteManyAsync(IEnumerable users); + + UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey); } public delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null, diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index fade63de51..a531883db1 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -33,6 +33,8 @@ public interface IUserService Task ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, string key); Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint, string key); + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328 + [Obsolete("Use ISetKeyConnectorKeyCommand instead. This method will be removed in a future version.")] Task SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier); Task ConvertToKeyConnectorAsync(User user); Task AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 8db66211b1..4e65e88767 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -621,6 +621,7 @@ public class UserService : UserManager, IUserService return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328 public async Task SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier) { var identityResult = CheckCanUseKeyConnector(user); diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 224351f034..571319e4c7 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Bit.Core; using Bit.Core.Billing.Premium.Models; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; @@ -401,6 +402,32 @@ public class UserRepository : Repository, IUserRepository return result.SingleOrDefault(); } + public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey) + { + return async (connection, transaction) => + { + var timestamp = DateTime.UtcNow; + + await connection!.ExecuteAsync( + "[dbo].[User_UpdateKeyConnectorUserKey]", + new + { + Id = userId, + Key = keyConnectorWrappedUserKey, + // Key Connector does not use KDF, so we set some defaults + Kdf = KdfType.Argon2id, + KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default, + KdfMemory = AuthConstants.ARGON2_MEMORY.Default, + KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default, + UsesKeyConnector = true, + RevisionDate = timestamp, + AccountRevisionDate = timestamp + }, + transaction: transaction, + commandType: CommandType.StoredProcedure); + }; + } + private async Task ProtectDataAndSaveAsync(User user, Func saveTask) { if (user == null) diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 9bf093e506..56d64094d0 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -1,5 +1,7 @@ using AutoMapper; +using Bit.Core; using Bit.Core.Billing.Premium.Models; +using Bit.Core.Enums; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; @@ -479,6 +481,35 @@ public class UserRepository : Repository, IUserR } } + public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey) + { + return async (_, _) => + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var userEntity = await dbContext.Users.FindAsync(userId); + if (userEntity == null) + { + throw new ArgumentException("User not found", nameof(userId)); + } + + var timestamp = DateTime.UtcNow; + + userEntity.Key = keyConnectorWrappedUserKey; + // Key Connector does not use KDF, so we set some defaults + userEntity.Kdf = KdfType.Argon2id; + userEntity.KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default; + userEntity.KdfMemory = AuthConstants.ARGON2_MEMORY.Default; + userEntity.KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default; + userEntity.UsesKeyConnector = true; + userEntity.RevisionDate = timestamp; + userEntity.AccountRevisionDate = timestamp; + + await dbContext.SaveChangesAsync(); + }; + } + private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable userIds) { var defaultCollections = (from c in dbContext.Collections diff --git a/src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateKeyConnectorUserKey.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateKeyConnectorUserKey.sql new file mode 100644 index 0000000000..7ab20a42af --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateKeyConnectorUserKey.sql @@ -0,0 +1,28 @@ +CREATE PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey] + @Id UNIQUEIDENTIFIER, + @Key VARCHAR(MAX), + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT, + @KdfParallelism INT, + @UsesKeyConnector BIT, + @RevisionDate DATETIME2(7), + @AccountRevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[User] + SET + [Key] = @Key, + [Kdf] = @Kdf, + [KdfIterations] = @KdfIterations, + [KdfMemory] = @KdfMemory, + [KdfParallelism] = @KdfParallelism, + [UsesKeyConnector] = @UsesKeyConnector, + [RevisionDate] = @RevisionDate, + [AccountRevisionDate] = @AccountRevisionDate + WHERE + [Id] = @Id +END diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index 1c456df106..eddffb6b36 100644 --- a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Api.KeyManagement.Models.Responses; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models; using Bit.Api.Vault.Models.Request; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; @@ -19,9 +20,11 @@ using Bit.Core.KeyManagement.Enums; using Bit.Core.KeyManagement.Models.Api.Request; using Bit.Core.KeyManagement.Repositories; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Vault.Enums; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; +using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.KeyManagement.Controllers; @@ -31,6 +34,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture(featureService => + { + featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any()) + .Returns(true); + }); _client = factory.CreateClient(); _loginHelper = new LoginHelper(_factory, _client); _userRepository = _factory.GetService(); @@ -78,8 +85,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture(featureService => + { + featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any()) + .Returns(false); + }); var localClient = localFactory.CreateClient(); var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; var localLoginHelper = new LoginHelper(localFactory, localClient); @@ -285,21 +295,21 @@ public class AccountsKeyManagementControllerTests : IClassFixture sutProvider, SetKeyConnectorKeyRequestModel data) { + data.KeyConnectorKeyWrappedUserKey = null; + data.AccountKeys = null; + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); await Assert.ThrowsAsync(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data)); @@ -252,10 +255,13 @@ public class AccountsKeyManagementControllerTests [Theory] [BitAutoData] - public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse( + public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse( SutProvider sutProvider, SetKeyConnectorKeyRequestModel data, User expectedUser) { + data.KeyConnectorKeyWrappedUserKey = null; + data.AccountKeys = null; + expectedUser.PublicKey = null; expectedUser.PrivateKey = null; sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()) @@ -278,17 +284,20 @@ public class AccountsKeyManagementControllerTests Assert.Equal(data.KdfIterations, user.KdfIterations); Assert.Equal(data.KdfMemory, user.KdfMemory); Assert.Equal(data.KdfParallelism, user.KdfParallelism); - Assert.Equal(data.Keys.PublicKey, user.PublicKey); - Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey); + Assert.Equal(data.Keys!.PublicKey, user.PublicKey); + Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey); }), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier)); } [Theory] [BitAutoData] - public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse( + public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeySucceeds_OkResponse( SutProvider sutProvider, SetKeyConnectorKeyRequestModel data, User expectedUser) { + data.KeyConnectorKeyWrappedUserKey = null; + data.AccountKeys = null; + expectedUser.PublicKey = null; expectedUser.PrivateKey = null; sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()) @@ -308,11 +317,108 @@ public class AccountsKeyManagementControllerTests Assert.Equal(data.KdfIterations, user.KdfIterations); Assert.Equal(data.KdfMemory, user.KdfMemory); Assert.Equal(data.KdfParallelism, user.KdfParallelism); - Assert.Equal(data.Keys.PublicKey, user.PublicKey); - Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey); + Assert.Equal(data.Keys!.PublicKey, user.PublicKey); + Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey); }), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier)); } + [Theory] + [BitAutoData] + public async Task PostSetKeyConnectorKeyAsync_V2_UserNull_Throws( + SutProvider sutProvider) + { + var request = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = "wrapped-user-key", + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = "public-key", + UserKeyEncryptedAccountPrivateKey = "encrypted-private-key" + }, + OrgIdentifier = "test-org" + }; + + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request)); + + await sutProvider.GetDependency().DidNotReceive() + .SetKeyConnectorKeyForUserAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task PostSetKeyConnectorKeyAsync_V2_Success( + SutProvider sutProvider, + User expectedUser) + { + var request = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = "wrapped-user-key", + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = "public-key", + UserKeyEncryptedAccountPrivateKey = "encrypted-private-key" + }, + OrgIdentifier = "test-org" + }; + + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()) + .Returns(expectedUser); + + await sutProvider.Sut.PostSetKeyConnectorKeyAsync(request); + + await sutProvider.GetDependency().Received(1) + .SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser), + Arg.Do(data => + { + Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey); + Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey); + Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey, + data.AccountKeys.UserKeyEncryptedAccountPrivateKey); + Assert.Equal(request.OrgIdentifier, data.OrgIdentifier); + })); + } + + [Theory] + [BitAutoData] + public async Task PostSetKeyConnectorKeyAsync_V2_CommandThrows_PropagatesException( + SutProvider sutProvider, + User expectedUser) + { + var request = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = "wrapped-user-key", + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = "public-key", + UserKeyEncryptedAccountPrivateKey = "encrypted-private-key" + }, + OrgIdentifier = "test-org" + }; + + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()) + .Returns(expectedUser); + sutProvider.GetDependency() + .When(x => x.SetKeyConnectorKeyForUserAsync(Arg.Any(), Arg.Any())) + .Do(_ => throw new BadRequestException("Command failed")); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request)); + + Assert.Equal("Command failed", exception.Message); + await sutProvider.GetDependency().Received(1) + .SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser), + Arg.Do(data => + { + Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey); + Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey); + Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey, + data.AccountKeys.UserKeyEncryptedAccountPrivateKey); + Assert.Equal(request.OrgIdentifier, data.OrgIdentifier); + })); + } + [Theory] [BitAutoData] public async Task PostConvertToKeyConnectorAsync_UserNull_Throws( diff --git a/test/Api.Test/KeyManagement/Models/Request/SetKeyConnectorKeyRequestModelTests.cs b/test/Api.Test/KeyManagement/Models/Request/SetKeyConnectorKeyRequestModelTests.cs new file mode 100644 index 0000000000..95ee743d02 --- /dev/null +++ b/test/Api.Test/KeyManagement/Models/Request/SetKeyConnectorKeyRequestModelTests.cs @@ -0,0 +1,333 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core; +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Api.Request; +using Xunit; + +namespace Bit.Api.Test.KeyManagement.Models.Request; + +public class SetKeyConnectorKeyRequestModelTests +{ + private const string _wrappedUserKey = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; + private const string _publicKey = "public-key"; + private const string _privateKey = "private-key"; + private const string _userKey = "user-key"; + private const string _orgIdentifier = "org-identifier"; + + [Fact] + public void Validate_V2Registration_Valid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = _wrappedUserKey, + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = _publicKey, + UserKeyEncryptedAccountPrivateKey = _privateKey + }, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Empty(results); + } + + [Fact] + public void Validate_V2Registration_WrappedUserKeyNotEncryptedString_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = "not-encrypted-string", + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = _publicKey, + UserKeyEncryptedAccountPrivateKey = _privateKey + }, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, + r => r.ErrorMessage == "KeyConnectorKeyWrappedUserKey is not a valid encrypted string."); + } + + [Fact] + public void Validate_V1Registration_Valid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = _userKey, + Keys = new KeysRequestModel + { + PublicKey = _publicKey, + EncryptedPrivateKey = _privateKey + }, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Empty(results); + } + + [Fact] + public void Validate_V1Registration_MissingKey_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = null, + Keys = new KeysRequestModel + { + PublicKey = _publicKey, + EncryptedPrivateKey = _privateKey + }, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, r => r.ErrorMessage == "Key must be supplied."); + } + + [Fact] + public void Validate_V1Registration_MissingKeys_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = _userKey, + Keys = null, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, r => r.ErrorMessage == "Keys must be supplied."); + } + + [Fact] + public void Validate_V1Registration_MissingKdf_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = _userKey, + Keys = new KeysRequestModel + { + PublicKey = _publicKey, + EncryptedPrivateKey = _privateKey + }, + Kdf = null, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, r => r.ErrorMessage == "Kdf must be supplied."); + } + + [Fact] + public void Validate_V1Registration_MissingKdfIterations_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = _userKey, + Keys = new KeysRequestModel + { + PublicKey = _publicKey, + EncryptedPrivateKey = _privateKey + }, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = null, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, r => r.ErrorMessage == "KdfIterations must be supplied."); + } + + [Fact] + public void Validate_V1Registration_Argon2id_MissingKdfMemory_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = _userKey, + Keys = new KeysRequestModel + { + PublicKey = _publicKey, + EncryptedPrivateKey = _privateKey + }, + Kdf = KdfType.Argon2id, + KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default, + KdfMemory = null, + KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, r => r.ErrorMessage == "KdfMemory must be supplied when Kdf is Argon2id."); + } + + [Fact] + public void Validate_V1Registration_Argon2id_MissingKdfParallelism_Invalid() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + Key = _userKey, + Keys = new KeysRequestModel + { + PublicKey = _publicKey, + EncryptedPrivateKey = _privateKey + }, + Kdf = KdfType.Argon2id, + KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default, + KdfMemory = AuthConstants.ARGON2_MEMORY.Default, + KdfParallelism = null, + OrgIdentifier = _orgIdentifier + }; + + // Act + var results = Validate(model); + + // Assert + Assert.Single(results); + Assert.Contains(results, r => r.ErrorMessage == "KdfParallelism must be supplied when Kdf is Argon2id."); + } + + [Fact] + public void ToKeyConnectorKeysData_EmptyKeyConnectorKeyWrappedUserKey_ThrowsException() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = "", + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = _publicKey, + UserKeyEncryptedAccountPrivateKey = _privateKey + }, + OrgIdentifier = _orgIdentifier + }; + + // Act + var exception = Assert.Throws(() => model.ToKeyConnectorKeysData()); + + // Assert + Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message); + } + + [Fact] + public void ToKeyConnectorKeysData_NullKeyConnectorKeyWrappedUserKey_ThrowsException() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = null, + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = _publicKey, + UserKeyEncryptedAccountPrivateKey = _privateKey + }, + OrgIdentifier = _orgIdentifier + }; + + // Act + var exception = Assert.Throws(() => model.ToKeyConnectorKeysData()); + + // Assert + Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message); + } + + [Fact] + public void ToKeyConnectorKeysData_NullAccountKeys_ThrowsException() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = _wrappedUserKey, + AccountKeys = null, + OrgIdentifier = _orgIdentifier + }; + + // Act + var exception = Assert.Throws(() => model.ToKeyConnectorKeysData()); + + // Assert + Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message); + } + + [Fact] + public void ToKeyConnectorKeysData_Valid_Success() + { + // Arrange + var model = new SetKeyConnectorKeyRequestModel + { + KeyConnectorKeyWrappedUserKey = _wrappedUserKey, + AccountKeys = new AccountKeysRequestModel + { + AccountPublicKey = _publicKey, + UserKeyEncryptedAccountPrivateKey = _privateKey + }, + OrgIdentifier = _orgIdentifier + }; + + // Act + var data = model.ToKeyConnectorKeysData(); + + // Assert + Assert.Equal(_wrappedUserKey, data.KeyConnectorKeyWrappedUserKey); + Assert.Equal(_publicKey, data.AccountKeys.AccountPublicKey); + Assert.Equal(_privateKey, data.AccountKeys.UserKeyEncryptedAccountPrivateKey); + Assert.Equal(_orgIdentifier, data.OrgIdentifier); + } + + private static List Validate(SetKeyConnectorKeyRequestModel model) + { + var results = new List(); + Validator.TryValidateObject(model, new ValidationContext(model), results, true); + return results; + } +} diff --git a/test/Core.Test/KeyManagement/Authorization/KeyConnectorAuthorizationHandlerTests.cs b/test/Core.Test/KeyManagement/Authorization/KeyConnectorAuthorizationHandlerTests.cs new file mode 100644 index 0000000000..fb774a78ac --- /dev/null +++ b/test/Core.Test/KeyManagement/Authorization/KeyConnectorAuthorizationHandlerTests.cs @@ -0,0 +1,151 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.KeyManagement.Authorization; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Authorization; + +[SutProviderCustomize] +public class KeyConnectorAuthorizationHandlerTests +{ + [Theory, BitAutoData] + public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success( + User user, + ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = false; + sutProvider.GetDependency().Organizations + .Returns(new List()); + + var requirement = KeyConnectorOperations.Use; + var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user); + + // Act + await sutProvider.Sut.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails( + User user, + ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = true; + sutProvider.GetDependency().Organizations + .Returns(new List()); + + var requirement = KeyConnectorOperations.Use; + var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user); + + // Act + await sutProvider.Sut.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_UserIsOwner_Fails( + User user, + Guid organizationId, + ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = false; + var organizations = new List + { + new() { Id = organizationId, Type = OrganizationUserType.Owner } + }; + sutProvider.GetDependency().Organizations.Returns(organizations); + + var requirement = KeyConnectorOperations.Use; + var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user); + + // Act + await sutProvider.Sut.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_UserIsAdmin_Fails( + User user, + Guid organizationId, + ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = false; + var organizations = new List + { + new() { Id = organizationId, Type = OrganizationUserType.Admin } + }; + sutProvider.GetDependency().Organizations.Returns(organizations); + + var requirement = KeyConnectorOperations.Use; + var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user); + + // Act + await sutProvider.Sut.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_UserIsRegularMember_Success( + User user, + Guid organizationId, + ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = false; + var organizations = new List + { + new() { Id = organizationId, Type = OrganizationUserType.User } + }; + sutProvider.GetDependency().Organizations.Returns(organizations); + + var requirement = KeyConnectorOperations.Use; + var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user); + + // Act + await sutProvider.Sut.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + } + + [Theory, BitAutoData] + public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException( + User user, + ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = false; + sutProvider.GetDependency().Organizations + .Returns(new List()); + + var unsupportedRequirement = new KeyConnectorOperationsRequirement("UnsupportedOperation"); + var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user); + + // Act & Assert + await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); + } +} diff --git a/test/Core.Test/KeyManagement/Commands/SetKeyConnectorKeyCommandTests.cs b/test/Core.Test/KeyManagement/Commands/SetKeyConnectorKeyCommandTests.cs new file mode 100644 index 0000000000..74f76f368b --- /dev/null +++ b/test/Core.Test/KeyManagement/Commands/SetKeyConnectorKeyCommandTests.cs @@ -0,0 +1,125 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Commands; + +[SutProviderCustomize] +public class SetKeyConnectorKeyCommandTests +{ + + [Theory, BitAutoData] + public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys( + User user, + KeyConnectorKeysData data, + SutProvider sutProvider) + { + // Set up valid V2 encryption data + if (data.AccountKeys!.SignatureKeyPair != null) + { + data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = "ed25519"; + } + + var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData(); + + // Arrange + user.UsesKeyConnector = false; + var currentContext = sutProvider.GetDependency(); + var httpContext = Substitute.For(); + httpContext.User.Returns(new ClaimsPrincipal()); + currentContext.HttpContext.Returns(httpContext); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), user, Arg.Any>()) + .Returns(AuthorizationResult.Success()); + + var userRepository = sutProvider.GetDependency(); + var mockUpdateUserData = Substitute.For(); + userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!) + .Returns(mockUpdateUserData); + + // Act + await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data); + + // Assert + + userRepository + .Received(1) + .SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey); + + await userRepository + .Received(1) + .SetV2AccountCryptographicStateAsync( + user.Id, + Arg.Is(data => + data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey && + data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey && + data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey && + data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm && + data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey && + data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey && + data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState && + data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion), + Arg.Is>(actions => + actions.Count() == 1 && actions.First() == mockUpdateUserData)); + + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector); + + await sutProvider.GetDependency() + .Received(1) + .AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency()); + } + + [Theory, BitAutoData] + public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException( + User user, + KeyConnectorKeysData data, + SutProvider sutProvider) + { + // Arrange + user.UsesKeyConnector = true; + var currentContext = sutProvider.GetDependency(); + var httpContext = Substitute.For(); + httpContext.User.Returns(new ClaimsPrincipal()); + currentContext.HttpContext.Returns(httpContext); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), user, Arg.Any>()) + .Returns(AuthorizationResult.Failed()); + + // Act & Assert + await Assert.ThrowsAsync( + () => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data)); + + sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetKeyConnectorUserKey(Arg.Any(), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetV2AccountCryptographicStateAsync(Arg.Any(), Arg.Any(), Arg.Any>()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogUserEventAsync(Arg.Any(), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .AcceptOrgUserByOrgSsoIdAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } +} diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs similarity index 91% rename from test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs rename to test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs index bbbd6d5cdb..98b2613ecb 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs @@ -1,9 +1,11 @@ -using Bit.Core.AdminConsole.Repositories; +using Bit.Core; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.IntegrationTest.AdminConsole; +using Microsoft.Data.SqlClient; using Xunit; namespace Bit.Infrastructure.IntegrationTest.Repositories; @@ -500,4 +502,54 @@ public class UserRepositoryTests // Assert Assert.Empty(results); } + + [Theory, DatabaseData] + public async Task SetKeyConnectorUserKey_UpdatesUserKey(IUserRepository userRepository, Database database) + { + var user = await userRepository.CreateTestUserAsync(); + + const string keyConnectorWrappedKey = "key-connector-wrapped-user-key"; + + var setKeyConnectorUserKeyDelegate = userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorWrappedKey); + + await RunUpdateUserDataAsync(setKeyConnectorUserKeyDelegate, database); + + var updatedUser = await userRepository.GetByIdAsync(user.Id); + + Assert.NotNull(updatedUser); + Assert.Equal(keyConnectorWrappedKey, updatedUser.Key); + Assert.True(updatedUser.UsesKeyConnector); + Assert.Equal(KdfType.Argon2id, updatedUser.Kdf); + Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, updatedUser.KdfIterations); + Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, updatedUser.KdfMemory); + Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, updatedUser.KdfParallelism); + Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1)); + Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1)); + } + + private static async Task RunUpdateUserDataAsync(UpdateUserData task, Database database) + { + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + await using var connection = new SqlConnection(database.ConnectionString); + connection.Open(); + + await using var transaction = connection.BeginTransaction(); + try + { + await task(connection, transaction); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + else + { + await task(); + } + } } diff --git a/util/Migrator/DbScripts/2025-12-17_00_User_UpdateKeyConnectorUserKey.sql b/util/Migrator/DbScripts/2025-12-17_00_User_UpdateKeyConnectorUserKey.sql new file mode 100644 index 0000000000..545bf830f6 --- /dev/null +++ b/util/Migrator/DbScripts/2025-12-17_00_User_UpdateKeyConnectorUserKey.sql @@ -0,0 +1,29 @@ +CREATE OR ALTER PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey] + @Id UNIQUEIDENTIFIER, + @Key VARCHAR(MAX), + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT, + @KdfParallelism INT, + @UsesKeyConnector BIT, + @RevisionDate DATETIME2(7), + @AccountRevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + +UPDATE + [dbo].[User] +SET + [Key] = @Key, + [Kdf] = @Kdf, + [KdfIterations] = @KdfIterations, + [KdfMemory] = @KdfMemory, + [KdfParallelism] = @KdfParallelism, + [UsesKeyConnector] = @UsesKeyConnector, + [RevisionDate] = @RevisionDate, + [AccountRevisionDate] = @AccountRevisionDate +WHERE + [Id] = @Id +END +GO From 1b41a06e32d819c92494a68f183f44f889d25a8b Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Thu, 18 Dec 2025 14:12:56 -0500 Subject: [PATCH 18/68] [PM-29780] Add feature flag for Send email OTP verification (#6742) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 94f599a2a6..e1ccbbd9b8 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -239,6 +239,7 @@ public static class FeatureFlagKeys public const string UseChromiumImporter = "pm-23982-chromium-importer"; public const string ChromiumImporterWithABE = "pm-25855-chromium-importer-abe"; public const string SendUIRefresh = "pm-28175-send-ui-refresh"; + public const string SendEmailOTP = "pm-19051-send-email-verification"; /* Vault Team */ public const string CipherKeyEncryption = "cipher-key-encryption"; From cc2d69e1fea685d3714299d1f6820e0003575d14 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 19 Dec 2025 06:05:18 +1000 Subject: [PATCH 19/68] [PM-28487] Move Events and EventsProcessor to DIRT ownership (#6678) * Move Events and EventsProcessor to DIRT ownership * include test projs * sort lines alphabetically within group * fix order --------- Co-authored-by: Graham Walker --- .github/CODEOWNERS | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 597085d97d..f0c85d98c1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -53,6 +53,11 @@ src/Core/IdentityServer @bitwarden/team-auth-dev # Dirt (Data Insights & Reporting) team **/Dirt @bitwarden/team-data-insights-and-reporting-dev +src/Events @bitwarden/team-data-insights-and-reporting-dev +src/EventsProcessor @bitwarden/team-data-insights-and-reporting-dev +test/Events.IntegrationTest @bitwarden/team-data-insights-and-reporting-dev +test/Events.Test @bitwarden/team-data-insights-and-reporting-dev +test/EventsProcessor.Test @bitwarden/team-data-insights-and-reporting-dev # Vault team **/Vault @bitwarden/team-vault-dev @@ -63,8 +68,6 @@ src/Core/IdentityServer @bitwarden/team-auth-dev bitwarden_license/src/Scim @bitwarden/team-admin-console-dev bitwarden_license/src/test/Scim.IntegrationTest @bitwarden/team-admin-console-dev bitwarden_license/src/test/Scim.ScimTest @bitwarden/team-admin-console-dev -src/Events @bitwarden/team-admin-console-dev -src/EventsProcessor @bitwarden/team-admin-console-dev # Billing team **/*billing* @bitwarden/team-billing-dev From e6c97bd8505d3a454763e41ebfdb05c92691ad1a Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:10:40 -0500 Subject: [PATCH 20/68] =?UTF-8?q?Revert=20"refactor(IdentityTokenResponse)?= =?UTF-8?q?:=20[Auth/PM-3287]=20Remove=20deprecated=20res=E2=80=A6"=20(#67?= =?UTF-8?q?55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bbe682dae92097fd14ebc78d7f23c80a0eea46ef. --- .../RequestValidators/BaseRequestValidator.cs | 1 + .../CustomTokenRequestValidator.cs | 17 +++++++++++++++++ .../Endpoints/IdentityServerTests.cs | 1 + 3 files changed, 19 insertions(+) diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index b0f3311b2c..0bdf1d89c2 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -671,6 +671,7 @@ public abstract class BaseRequestValidator where T : class customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); + customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); customResponse.Add("Kdf", (byte)user.Kdf); customResponse.Add("KdfIterations", user.KdfIterations); customResponse.Add("KdfMemory", user.KdfMemory); diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 5eee4199b2..38a4813ecd 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -4,6 +4,7 @@ using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.IdentityServer; +using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -154,7 +155,23 @@ public class CustomTokenRequestValidator : BaseRequestValidator var root = body.RootElement; AssertRefreshTokenExists(root); AssertHelper.AssertJsonProperty(root, "ForcePasswordReset", JsonValueKind.False); + AssertHelper.AssertJsonProperty(root, "ResetMasterPassword", JsonValueKind.False); var kdf = AssertHelper.AssertJsonProperty(root, "Kdf", JsonValueKind.Number).GetInt32(); Assert.Equal(0, kdf); var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32(); From 457e293fdc5a56569c5a4ac74ef786563553e0ce Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:35:01 -0800 Subject: [PATCH 21/68] [PM-29017] - improve logic for cipher SaveDetailsAsync validation (#6731) * improve logic for cipher SaveDetailsAsync validation. fix tests * revert change * fix test * remove duplicate semicolon --- .../Services/Implementations/CipherService.cs | 7 +- .../Vault/Services/CipherServiceTests.cs | 73 ++++++++++++++----- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 2085345b16..bb752b471f 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -1029,11 +1029,8 @@ public class CipherService : ICipherService var existingCipherData = DeserializeCipherData(existingCipher); var newCipherData = DeserializeCipherData(cipher); - // "hidden password" users may not add cipher key encryption - if (existingCipher.Key == null && cipher.Key != null) - { - throw new BadRequestException("You do not have permission to add cipher key encryption."); - } + // For hidden-password users, never allow Key to change at all. + cipher.Key = existingCipher.Key; // Keep only non-hidden fileds from the new cipher var nonHiddenFields = newCipherData.Fields?.Where(f => f.Type != FieldType.Hidden) ?? []; // Get hidden fields from the existing cipher diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index c5eecb8f34..fc84651951 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -1215,12 +1215,12 @@ public class CipherServiceTests private static SaveDetailsAsyncDependencies GetSaveDetailsAsyncDependencies( SutProvider sutProvider, string newPassword, - bool viewPassword, - bool editPermission, + bool permission, string? key = null, string? totp = null, CipherLoginFido2CredentialData[]? passkeys = null, - CipherFieldData[]? fields = null + CipherFieldData[]? fields = null, + string? existingKey = "OriginalKey" ) { var cipherDetails = new CipherDetails @@ -1233,13 +1233,22 @@ public class CipherServiceTests Key = key, }; - var newLoginData = new CipherLoginData { Username = "user", Password = newPassword, Totp = totp, Fido2Credentials = passkeys, Fields = fields }; + var newLoginData = new CipherLoginData + { + Username = "user", + Password = newPassword, + Totp = totp, + Fido2Credentials = passkeys, + Fields = fields + }; + cipherDetails.Data = JsonSerializer.Serialize(newLoginData); var existingCipher = new Cipher { Id = cipherDetails.Id, Type = CipherType.Login, + Key = existingKey, Data = JsonSerializer.Serialize( new CipherLoginData { @@ -1261,7 +1270,14 @@ public class CipherServiceTests var permissions = new Dictionary { - { cipherDetails.Id, new OrganizationCipherPermission { ViewPassword = viewPassword, Edit = editPermission } } + { + cipherDetails.Id, + new OrganizationCipherPermission + { + ViewPassword = permission, + Edit = permission + } + } }; sutProvider.GetDependency() @@ -1278,7 +1294,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task SaveDetailsAsync_PasswordNotChangedWithoutViewPasswordPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: true); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1294,7 +1310,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task SaveDetailsAsync_PasswordNotChangedWithoutEditPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1310,7 +1326,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task SaveDetailsAsync_PasswordChangedWithPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1326,7 +1342,11 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task SaveDetailsAsync_CipherKeyChangedWithPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, "NewKey"); + var deps = GetSaveDetailsAsyncDependencies( + sutProvider, + newPassword: "NewPassword", + permission: true, + key: "NewKey"); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1336,27 +1356,40 @@ public class CipherServiceTests true); Assert.Equal("NewKey", deps.CipherDetails.Key); + + await sutProvider.GetDependency() + .Received() + .ReplaceAsync(Arg.Is(c => c.Id == deps.CipherDetails.Id && c.Key == "NewKey")); } [Theory, BitAutoData] - public async Task SaveDetailsAsync_CipherKeyChangedWithoutPermission(string _, SutProvider sutProvider) + public async Task SaveDetailsAsync_CipherKeyNotChangedWithoutPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, "NewKey"); + var deps = GetSaveDetailsAsyncDependencies( + sutProvider, + newPassword: "NewPassword", + permission: false, + key: "NewKey" + ); - var exception = await Assert.ThrowsAsync(() => deps.SutProvider.Sut.SaveDetailsAsync( + await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, deps.CipherDetails.UserId.Value, deps.CipherDetails.RevisionDate, null, - true)); + true); - Assert.Contains("do not have permission", exception.Message); + Assert.Equal("OriginalKey", deps.CipherDetails.Key); + + await sutProvider.GetDependency() + .Received() + .ReplaceAsync(Arg.Is(c => c.Id == deps.CipherDetails.Id && c.Key == "OriginalKey")); } [Theory, BitAutoData] public async Task SaveDetailsAsync_TotpChangedWithoutPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, totp: "NewTotp"); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, totp: "NewTotp"); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1372,7 +1405,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task SaveDetailsAsync_TotpChangedWithPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, totp: "NewTotp"); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, totp: "NewTotp"); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1397,7 +1430,7 @@ public class CipherServiceTests } }; - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: false, passkeys: passkeys); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, passkeys: passkeys); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1422,7 +1455,7 @@ public class CipherServiceTests } }; - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, passkeys: passkeys); + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, passkeys: passkeys); await deps.SutProvider.Sut.SaveDetailsAsync( deps.CipherDetails, @@ -1439,7 +1472,7 @@ public class CipherServiceTests [BitAutoData] public async Task SaveDetailsAsync_HiddenFieldsChangedWithoutPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: false, editPermission: false, fields: + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: false, fields: [ new CipherFieldData { @@ -1464,7 +1497,7 @@ public class CipherServiceTests [BitAutoData] public async Task SaveDetailsAsync_HiddenFieldsChangedWithPermission(string _, SutProvider sutProvider) { - var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", viewPassword: true, editPermission: true, fields: + var deps = GetSaveDetailsAsyncDependencies(sutProvider, "NewPassword", permission: true, fields: [ new CipherFieldData { From bc800a788e9fb4e67a05af112ab00c4b02a8d5ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:06:33 -0500 Subject: [PATCH 22/68] [deps]: Update actions/checkout action to v6 (#6706) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/_move_edd_db_scripts.yml | 4 ++-- .github/workflows/build.yml | 8 ++++---- .github/workflows/cleanup-rc-branch.yml | 2 +- .github/workflows/code-references.yml | 2 +- .github/workflows/load-test.yml | 2 +- .github/workflows/protect-files.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/repository-management.yml | 4 ++-- .github/workflows/test-database.yml | 6 +++--- .github/workflows/test.yml | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/_move_edd_db_scripts.yml b/.github/workflows/_move_edd_db_scripts.yml index 7e97fa2a07..742e7b897e 100644 --- a/.github/workflows/_move_edd_db_scripts.yml +++ b/.github/workflows/_move_edd_db_scripts.yml @@ -38,7 +38,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Check out branch - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} persist-credentials: false @@ -68,7 +68,7 @@ jobs: if: ${{ needs.setup.outputs.copy_edd_scripts == 'true' }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b457b9d56..1afaab0882 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -102,7 +102,7 @@ jobs: echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT" - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -289,7 +289,7 @@ jobs: actions: read steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -416,7 +416,7 @@ jobs: - win-x64 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index 63079826c7..ae482ef4e6 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -31,7 +31,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Checkout main - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: main token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 35e6cfdd40..98f5288ec8 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index cdb53109f5..dd3cef9d83 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -87,7 +87,7 @@ jobs: datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479 - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index a939be6fdb..4b137eb221 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -31,7 +31,7 @@ jobs: label: "DB-migrations-changed" steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 2 persist-credentials: false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2272387d84..6f00d4f85f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -106,7 +106,7 @@ jobs: echo "Github Release Option: $RELEASE_OPTION" - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75b4df4e5c..887f78f5df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: fi - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 74823c34b5..a0f7ea73b1 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -91,7 +91,7 @@ jobs: permission-contents: write - name: Check out branch - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: main token: ${{ steps.app-token.outputs.token }} @@ -215,7 +215,7 @@ jobs: permission-contents: write - name: Check out target ref - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ inputs.target_ref }} token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index b0d0c076a1..5ce13b25c6 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -44,7 +44,7 @@ jobs: checks: write steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false @@ -178,7 +178,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false @@ -269,7 +269,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36ab8785d5..72dd17d7d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false From 69d72c2ad3a209b06bce301897fe124fe0c0794a Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Sat, 20 Dec 2025 07:32:51 +1000 Subject: [PATCH 23/68] [PM-28485] Move organization events domain to DIRT code ownership (#6685) --- .../{AdminConsole => Dirt}/Controllers/EventsController.cs | 3 ++- .../Models/Response/EventResponseModel.cs | 2 +- .../Public/Controllers/EventsController.cs | 7 +++---- .../Public/Models}/EventFilterRequestModel.cs | 2 +- .../Response => Dirt/Public/Models}/EventResponseModel.cs | 3 ++- .../Controllers/SecretsManagerEventsController.cs | 1 + src/Api/Utilities/DiagnosticTools/EventDiagnosticLogger.cs | 4 ++-- src/Core/{AdminConsole => Dirt}/Entities/Event.cs | 0 src/Core/{AdminConsole => Dirt}/Enums/EventSystemUser.cs | 0 src/Core/{AdminConsole => Dirt}/Enums/EventType.cs | 0 .../{AdminConsole => Dirt}/Models/Data/EventMessage.cs | 0 .../{AdminConsole => Dirt}/Models/Data/EventTableEntity.cs | 0 src/Core/{AdminConsole => Dirt}/Models/Data/IEvent.cs | 0 .../Repositories/IEventRepository.cs | 0 .../Repositories/TableStorage/EventRepository.cs | 0 .../{AdminConsole => Dirt}/Services/IEventWriteService.cs | 0 .../Implementations/AzureQueueEventWriteService.cs | 0 .../Implementations}/EventIntegrationEventWriteService.cs | 0 .../Services/Implementations/EventService.cs | 0 .../Implementations/RepositoryEventWriteService.cs | 0 .../Services/NoopImplementations/NoopEventService.cs | 0 .../Services/NoopImplementations/NoopEventWriteService.cs | 0 .../{AdminConsole => Dirt}/Repositories/EventRepository.cs | 0 .../Configurations/EventEntityTypeConfiguration.cs | 0 .../{AdminConsole => Dirt}/Models/Event.cs | 0 .../{AdminConsole => Dirt}/Repositories/EventRepository.cs | 0 .../Repositories/Queries/EventReadPageByCipherIdQuery.cs | 0 .../EventReadPageByOrganizationIdActingUserIdQuery.cs | 0 .../Queries/EventReadPageByOrganizationIdQuery.cs | 0 .../EventReadPageByOrganizationIdServiceAccountIdQuery.cs | 0 .../Repositories/Queries/EventReadPageByProjectIdQuery.cs | 0 .../Queries/EventReadPageByProviderIdActingUserIdQuery.cs | 0 .../Repositories/Queries/EventReadPageByProviderIdQuery.cs | 0 .../Repositories/Queries/EventReadPageBySecretIdQuery.cs | 0 .../Queries/EventReadPageByServiceAccountIdQuery.cs | 0 .../Repositories/Queries/EventReadPageByUserIdQuery.cs | 0 src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_Create.sql | 0 .../dbo/{ => Dirt}/Stored Procedures/Event_ReadById.sql | 0 .../Stored Procedures/Event_ReadPageByCipherId.sql | 0 .../Stored Procedures/Event_ReadPageByOrganizationId.sql | 0 .../Event_ReadPageByOrganizationIdActingUserId.sql | 0 .../Stored Procedures/Event_ReadPageByProviderId.sql | 0 .../Event_ReadPageByProviderIdActingUserId.sql | 0 .../Stored Procedures/Event_ReadPageByUserId.sql | 0 src/Sql/dbo/{ => Dirt}/Tables/Event.sql | 0 src/Sql/dbo/{ => Dirt}/Views/EventView.sql | 0 .../DiagnosticTools/EventDiagnosticLoggerTests.cs | 6 +++--- .../Services/AzureQueueEventWriteServiceTests.cs | 0 .../{AdminConsole => Dirt}/Services/EventServiceTests.cs | 0 .../Services/RepositoryEventWriteServiceTests.cs | 0 .../{AdminConsole => Dirt}/Autofixture/EventFixtures.cs | 0 .../Repositories/EqualityComparers/EventCompare.cs | 0 52 files changed, 15 insertions(+), 13 deletions(-) rename src/Api/{AdminConsole => Dirt}/Controllers/EventsController.cs (99%) rename src/Api/{AdminConsole => Dirt}/Models/Response/EventResponseModel.cs (98%) rename src/Api/{AdminConsole => Dirt}/Public/Controllers/EventsController.cs (98%) rename src/Api/{AdminConsole/Public/Models/Request => Dirt/Public/Models}/EventFilterRequestModel.cs (97%) rename src/Api/{AdminConsole/Public/Models/Response => Dirt/Public/Models}/EventResponseModel.cs (98%) rename src/Core/{AdminConsole => Dirt}/Entities/Event.cs (100%) rename src/Core/{AdminConsole => Dirt}/Enums/EventSystemUser.cs (100%) rename src/Core/{AdminConsole => Dirt}/Enums/EventType.cs (100%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventMessage.cs (100%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventTableEntity.cs (100%) rename src/Core/{AdminConsole => Dirt}/Models/Data/IEvent.cs (100%) rename src/Core/{AdminConsole => Dirt}/Repositories/IEventRepository.cs (100%) rename src/Core/{AdminConsole => Dirt}/Repositories/TableStorage/EventRepository.cs (100%) rename src/Core/{AdminConsole => Dirt}/Services/IEventWriteService.cs (100%) rename src/Core/{AdminConsole => Dirt}/Services/Implementations/AzureQueueEventWriteService.cs (100%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/EventIntegrationEventWriteService.cs (100%) rename src/Core/{AdminConsole => Dirt}/Services/Implementations/EventService.cs (100%) rename src/Core/{ => Dirt}/Services/Implementations/RepositoryEventWriteService.cs (100%) rename src/Core/{AdminConsole => Dirt}/Services/NoopImplementations/NoopEventService.cs (100%) rename src/Core/{AdminConsole => Dirt}/Services/NoopImplementations/NoopEventWriteService.cs (100%) rename src/Infrastructure.Dapper/{AdminConsole => Dirt}/Repositories/EventRepository.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Configurations/EventEntityTypeConfiguration.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Models/Event.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/EventRepository.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByCipherIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByOrganizationIdActingUserIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByOrganizationIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByProjectIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByProviderIdActingUserIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByProviderIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageBySecretIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs (100%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/EventReadPageByUserIdQuery.cs (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_Create.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadById.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadPageByCipherId.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadPageByOrganizationId.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadPageByOrganizationIdActingUserId.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadPageByProviderId.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadPageByProviderIdActingUserId.sql (100%) rename src/Sql/dbo/{ => Dirt}/Stored Procedures/Event_ReadPageByUserId.sql (100%) rename src/Sql/dbo/{ => Dirt}/Tables/Event.sql (100%) rename src/Sql/dbo/{ => Dirt}/Views/EventView.sql (100%) rename test/Core.Test/{AdminConsole => Dirt}/Services/AzureQueueEventWriteServiceTests.cs (100%) rename test/Core.Test/{AdminConsole => Dirt}/Services/EventServiceTests.cs (100%) rename test/Core.Test/{AdminConsole => Dirt}/Services/RepositoryEventWriteServiceTests.cs (100%) rename test/Infrastructure.EFIntegration.Test/{AdminConsole => Dirt}/Autofixture/EventFixtures.cs (100%) rename test/Infrastructure.EFIntegration.Test/{AdminConsole => Dirt}/Repositories/EqualityComparers/EventCompare.cs (100%) diff --git a/src/Api/AdminConsole/Controllers/EventsController.cs b/src/Api/Dirt/Controllers/EventsController.cs similarity index 99% rename from src/Api/AdminConsole/Controllers/EventsController.cs rename to src/Api/Dirt/Controllers/EventsController.cs index 7e058a7870..1ac83c1316 100644 --- a/src/Api/AdminConsole/Controllers/EventsController.cs +++ b/src/Api/Dirt/Controllers/EventsController.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Api.Dirt.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Api.Utilities.DiagnosticTools; @@ -17,7 +18,7 @@ using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("events")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs b/src/Api/Dirt/Models/Response/EventResponseModel.cs similarity index 98% rename from src/Api/AdminConsole/Models/Response/EventResponseModel.cs rename to src/Api/Dirt/Models/Response/EventResponseModel.cs index c259bc3bc4..bfcc50c84e 100644 --- a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs +++ b/src/Api/Dirt/Models/Response/EventResponseModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Models.Api; using Bit.Core.Models.Data; -namespace Bit.Api.Models.Response; +namespace Bit.Api.Dirt.Models.Response; public class EventResponseModel : ResponseModel { diff --git a/src/Api/AdminConsole/Public/Controllers/EventsController.cs b/src/Api/Dirt/Public/Controllers/EventsController.cs similarity index 98% rename from src/Api/AdminConsole/Public/Controllers/EventsController.cs rename to src/Api/Dirt/Public/Controllers/EventsController.cs index b92e576ef9..8c76137489 100644 --- a/src/Api/AdminConsole/Public/Controllers/EventsController.cs +++ b/src/Api/Dirt/Public/Controllers/EventsController.cs @@ -1,6 +1,5 @@ - -using System.Net; -using Bit.Api.Models.Public.Request; +using System.Net; +using Bit.Api.Dirt.Public.Models; using Bit.Api.Models.Public.Response; using Bit.Api.Utilities.DiagnosticTools; using Bit.Core.Context; @@ -12,7 +11,7 @@ using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.Public.Controllers; +namespace Bit.Api.Dirt.Public.Controllers; [Route("public/events")] [Authorize("Organization")] diff --git a/src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs b/src/Api/Dirt/Public/Models/EventFilterRequestModel.cs similarity index 97% rename from src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs rename to src/Api/Dirt/Public/Models/EventFilterRequestModel.cs index a007349f26..20984c2cb0 100644 --- a/src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs +++ b/src/Api/Dirt/Public/Models/EventFilterRequestModel.cs @@ -3,7 +3,7 @@ using Bit.Core.Exceptions; -namespace Bit.Api.Models.Public.Request; +namespace Bit.Api.Dirt.Public.Models; public class EventFilterRequestModel { diff --git a/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs b/src/Api/Dirt/Public/Models/EventResponseModel.cs similarity index 98% rename from src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs rename to src/Api/Dirt/Public/Models/EventResponseModel.cs index 3e1de2747a..77c0b5a275 100644 --- a/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs +++ b/src/Api/Dirt/Public/Models/EventResponseModel.cs @@ -1,8 +1,9 @@ using System.ComponentModel.DataAnnotations; +using Bit.Api.Models.Public.Response; using Bit.Core.Enums; using Bit.Core.Models.Data; -namespace Bit.Api.Models.Public.Response; +namespace Bit.Api.Dirt.Public.Models; /// /// An event log. diff --git a/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs b/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs index af162fe399..0f467a4c78 100644 --- a/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Api.Dirt.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core.Exceptions; diff --git a/src/Api/Utilities/DiagnosticTools/EventDiagnosticLogger.cs b/src/Api/Utilities/DiagnosticTools/EventDiagnosticLogger.cs index 9f6a8d2639..af34931181 100644 --- a/src/Api/Utilities/DiagnosticTools/EventDiagnosticLogger.cs +++ b/src/Api/Utilities/DiagnosticTools/EventDiagnosticLogger.cs @@ -1,4 +1,4 @@ -using Bit.Api.Models.Public.Request; +using Bit.Api.Dirt.Public.Models; using Bit.Api.Models.Public.Response; using Bit.Core; using Bit.Core.Services; @@ -49,7 +49,7 @@ public static class EventDiagnosticLogger this ILogger logger, IFeatureService featureService, Guid organizationId, - IEnumerable data, + IEnumerable data, string? continuationToken, DateTime? queryStart = null, DateTime? queryEnd = null) diff --git a/src/Core/AdminConsole/Entities/Event.cs b/src/Core/Dirt/Entities/Event.cs similarity index 100% rename from src/Core/AdminConsole/Entities/Event.cs rename to src/Core/Dirt/Entities/Event.cs diff --git a/src/Core/AdminConsole/Enums/EventSystemUser.cs b/src/Core/Dirt/Enums/EventSystemUser.cs similarity index 100% rename from src/Core/AdminConsole/Enums/EventSystemUser.cs rename to src/Core/Dirt/Enums/EventSystemUser.cs diff --git a/src/Core/AdminConsole/Enums/EventType.cs b/src/Core/Dirt/Enums/EventType.cs similarity index 100% rename from src/Core/AdminConsole/Enums/EventType.cs rename to src/Core/Dirt/Enums/EventType.cs diff --git a/src/Core/AdminConsole/Models/Data/EventMessage.cs b/src/Core/Dirt/Models/Data/EventMessage.cs similarity index 100% rename from src/Core/AdminConsole/Models/Data/EventMessage.cs rename to src/Core/Dirt/Models/Data/EventMessage.cs diff --git a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs b/src/Core/Dirt/Models/Data/EventTableEntity.cs similarity index 100% rename from src/Core/AdminConsole/Models/Data/EventTableEntity.cs rename to src/Core/Dirt/Models/Data/EventTableEntity.cs diff --git a/src/Core/AdminConsole/Models/Data/IEvent.cs b/src/Core/Dirt/Models/Data/IEvent.cs similarity index 100% rename from src/Core/AdminConsole/Models/Data/IEvent.cs rename to src/Core/Dirt/Models/Data/IEvent.cs diff --git a/src/Core/AdminConsole/Repositories/IEventRepository.cs b/src/Core/Dirt/Repositories/IEventRepository.cs similarity index 100% rename from src/Core/AdminConsole/Repositories/IEventRepository.cs rename to src/Core/Dirt/Repositories/IEventRepository.cs diff --git a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs b/src/Core/Dirt/Repositories/TableStorage/EventRepository.cs similarity index 100% rename from src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs rename to src/Core/Dirt/Repositories/TableStorage/EventRepository.cs diff --git a/src/Core/AdminConsole/Services/IEventWriteService.cs b/src/Core/Dirt/Services/IEventWriteService.cs similarity index 100% rename from src/Core/AdminConsole/Services/IEventWriteService.cs rename to src/Core/Dirt/Services/IEventWriteService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/AzureQueueEventWriteService.cs b/src/Core/Dirt/Services/Implementations/AzureQueueEventWriteService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/AzureQueueEventWriteService.cs rename to src/Core/Dirt/Services/Implementations/AzureQueueEventWriteService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs b/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs rename to src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/Dirt/Services/Implementations/EventService.cs similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventService.cs rename to src/Core/Dirt/Services/Implementations/EventService.cs diff --git a/src/Core/Services/Implementations/RepositoryEventWriteService.cs b/src/Core/Dirt/Services/Implementations/RepositoryEventWriteService.cs similarity index 100% rename from src/Core/Services/Implementations/RepositoryEventWriteService.cs rename to src/Core/Dirt/Services/Implementations/RepositoryEventWriteService.cs diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs b/src/Core/Dirt/Services/NoopImplementations/NoopEventService.cs similarity index 100% rename from src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs rename to src/Core/Dirt/Services/NoopImplementations/NoopEventService.cs diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventWriteService.cs b/src/Core/Dirt/Services/NoopImplementations/NoopEventWriteService.cs similarity index 100% rename from src/Core/AdminConsole/Services/NoopImplementations/NoopEventWriteService.cs rename to src/Core/Dirt/Services/NoopImplementations/NoopEventWriteService.cs diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/EventRepository.cs b/src/Infrastructure.Dapper/Dirt/Repositories/EventRepository.cs similarity index 100% rename from src/Infrastructure.Dapper/AdminConsole/Repositories/EventRepository.cs rename to src/Infrastructure.Dapper/Dirt/Repositories/EventRepository.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/EventEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Dirt/Configurations/EventEntityTypeConfiguration.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Configurations/EventEntityTypeConfiguration.cs rename to src/Infrastructure.EntityFramework/Dirt/Configurations/EventEntityTypeConfiguration.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/Event.cs b/src/Infrastructure.EntityFramework/Dirt/Models/Event.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Models/Event.cs rename to src/Infrastructure.EntityFramework/Dirt/Models/Event.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/EventRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/EventRepository.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByCipherIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByCipherIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByCipherIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByCipherIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdActingUserIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdActingUserIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdActingUserIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdActingUserIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProjectIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProjectIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProjectIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProjectIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProviderIdActingUserIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProviderIdActingUserIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProviderIdActingUserIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProviderIdActingUserIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProviderIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProviderIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProviderIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProviderIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageBySecretIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageBySecretIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageBySecretIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageBySecretIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByUserIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByUserIdQuery.cs similarity index 100% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByUserIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByUserIdQuery.cs diff --git a/src/Sql/dbo/Stored Procedures/Event_Create.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_Create.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_Create.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_Create.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadById.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadById.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadById.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadById.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadPageByCipherId.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByCipherId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadPageByCipherId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByCipherId.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadPageByOrganizationId.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByOrganizationId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadPageByOrganizationId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByOrganizationId.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadPageByOrganizationIdActingUserId.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByOrganizationIdActingUserId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadPageByOrganizationIdActingUserId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByOrganizationIdActingUserId.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadPageByProviderId.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByProviderId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadPageByProviderId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByProviderId.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadPageByProviderIdActingUserId.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByProviderIdActingUserId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadPageByProviderIdActingUserId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByProviderIdActingUserId.sql diff --git a/src/Sql/dbo/Stored Procedures/Event_ReadPageByUserId.sql b/src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByUserId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/Event_ReadPageByUserId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByUserId.sql diff --git a/src/Sql/dbo/Tables/Event.sql b/src/Sql/dbo/Dirt/Tables/Event.sql similarity index 100% rename from src/Sql/dbo/Tables/Event.sql rename to src/Sql/dbo/Dirt/Tables/Event.sql diff --git a/src/Sql/dbo/Views/EventView.sql b/src/Sql/dbo/Dirt/Views/EventView.sql similarity index 100% rename from src/Sql/dbo/Views/EventView.sql rename to src/Sql/dbo/Dirt/Views/EventView.sql diff --git a/test/Api.Test/Utilities/DiagnosticTools/EventDiagnosticLoggerTests.cs b/test/Api.Test/Utilities/DiagnosticTools/EventDiagnosticLoggerTests.cs index ada75b148b..95fa949bc7 100644 --- a/test/Api.Test/Utilities/DiagnosticTools/EventDiagnosticLoggerTests.cs +++ b/test/Api.Test/Utilities/DiagnosticTools/EventDiagnosticLoggerTests.cs @@ -1,4 +1,4 @@ -using Bit.Api.Models.Public.Request; +using Bit.Api.Dirt.Public.Models; using Bit.Api.Models.Public.Response; using Bit.Api.Utilities.DiagnosticTools; using Bit.Core; @@ -155,7 +155,7 @@ public class EventDiagnosticLoggerTests var featureService = Substitute.For(); featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true); - Bit.Api.Models.Response.EventResponseModel[] emptyEvents = []; + Api.Dirt.Models.Response.EventResponseModel[] emptyEvents = []; // Act logger.LogAggregateData(featureService, organizationId, emptyEvents, null, null, null); @@ -188,7 +188,7 @@ public class EventDiagnosticLoggerTests var oldestEvent = Substitute.For(); oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-2)); - var events = new List + var events = new List { new (newestEvent), new (middleEvent), diff --git a/test/Core.Test/AdminConsole/Services/AzureQueueEventWriteServiceTests.cs b/test/Core.Test/Dirt/Services/AzureQueueEventWriteServiceTests.cs similarity index 100% rename from test/Core.Test/AdminConsole/Services/AzureQueueEventWriteServiceTests.cs rename to test/Core.Test/Dirt/Services/AzureQueueEventWriteServiceTests.cs diff --git a/test/Core.Test/AdminConsole/Services/EventServiceTests.cs b/test/Core.Test/Dirt/Services/EventServiceTests.cs similarity index 100% rename from test/Core.Test/AdminConsole/Services/EventServiceTests.cs rename to test/Core.Test/Dirt/Services/EventServiceTests.cs diff --git a/test/Core.Test/AdminConsole/Services/RepositoryEventWriteServiceTests.cs b/test/Core.Test/Dirt/Services/RepositoryEventWriteServiceTests.cs similarity index 100% rename from test/Core.Test/AdminConsole/Services/RepositoryEventWriteServiceTests.cs rename to test/Core.Test/Dirt/Services/RepositoryEventWriteServiceTests.cs diff --git a/test/Infrastructure.EFIntegration.Test/AdminConsole/Autofixture/EventFixtures.cs b/test/Infrastructure.EFIntegration.Test/Dirt/Autofixture/EventFixtures.cs similarity index 100% rename from test/Infrastructure.EFIntegration.Test/AdminConsole/Autofixture/EventFixtures.cs rename to test/Infrastructure.EFIntegration.Test/Dirt/Autofixture/EventFixtures.cs diff --git a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/EqualityComparers/EventCompare.cs b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/EqualityComparers/EventCompare.cs similarity index 100% rename from test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/EqualityComparers/EventCompare.cs rename to test/Infrastructure.EFIntegration.Test/Dirt/Repositories/EqualityComparers/EventCompare.cs From eb360ffec194bbaad45286bc5ab02b18ab340807 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:28:27 +0100 Subject: [PATCH 24/68] [PM-29930]Fix [Defect] Automatic Sync - Sync License throws error on Self Host (#6770) * Restore the mistakenly remove controller * Fix the lint build error --- .../Billing/Controllers/LicensesController.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/Api/Billing/Controllers/LicensesController.cs diff --git a/src/Api/Billing/Controllers/LicensesController.cs b/src/Api/Billing/Controllers/LicensesController.cs new file mode 100644 index 0000000000..29313bd4d8 --- /dev/null +++ b/src/Api/Billing/Controllers/LicensesController.cs @@ -0,0 +1,91 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api.OrganizationLicenses; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Billing.Controllers; + +[Route("licenses")] +[Authorize("Licensing")] +[SelfHosted(NotSelfHostedOnly = true)] +public class LicensesController : Controller +{ + private readonly IUserRepository _userRepository; + private readonly IUserService _userService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery; + private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand; + private readonly ICurrentContext _currentContext; + + public LicensesController( + IUserRepository userRepository, + IUserService userService, + IOrganizationRepository organizationRepository, + IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery, + IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand, + ICurrentContext currentContext) + { + _userRepository = userRepository; + _userService = userService; + _organizationRepository = organizationRepository; + _getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery; + _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand; + _currentContext = currentContext; + } + + [HttpGet("user/{id}")] + public async Task GetUser(string id, [FromQuery] string key) + { + var user = await _userRepository.GetByIdAsync(new Guid(id)); + if (user == null) + { + return null; + } + else if (!user.LicenseKey.Equals(key)) + { + await Task.Delay(2000); + throw new BadRequestException("Invalid license key."); + } + + var license = await _userService.GenerateLicenseAsync(user, null); + return license; + } + + /// + /// Used by self-hosted installations to get an updated license file + /// + [HttpGet("organization/{id}")] + public async Task OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model) + { + var organization = await _organizationRepository.GetByIdAsync(new Guid(id)); + if (organization == null) + { + throw new NotFoundException("Organization not found."); + } + + if (!organization.LicenseKey.Equals(model.LicenseKey)) + { + await Task.Delay(2000); + throw new BadRequestException("Invalid license key."); + } + + if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey)) + { + throw new BadRequestException("Invalid Billing Sync Key"); + } + + var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value); + return license; + } +} From ae3c8317e3937c0ea19c057d3e8277441e317221 Mon Sep 17 00:00:00 2001 From: sneakernuts <671942+sneakernuts@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:09:23 -0700 Subject: [PATCH 25/68] SRE-3582 billing cleanup (#6772) --- src/Billing/BillingSettings.cs | 34 -- .../Controllers/FreshdeskController.cs | 395 ------------------ .../Controllers/FreshsalesController.cs | 248 ----------- .../Models/FreshdeskReplyRequestModel.cs | 9 - src/Billing/Models/FreshdeskWebhookModel.cs | 24 -- .../OnyxAnswerWithCitationRequestModel.cs | 75 ---- src/Billing/Models/OnyxResponseModel.cs | 15 - src/Billing/Startup.cs | 8 - src/Billing/appsettings.Development.json | 5 - src/Billing/appsettings.Production.json | 5 +- src/Billing/appsettings.json | 21 - .../Controllers/FreshdeskControllerTests.cs | 251 ----------- .../Controllers/FreshsalesControllerTests.cs | 82 ---- 13 files changed, 1 insertion(+), 1171 deletions(-) delete mode 100644 src/Billing/Controllers/FreshdeskController.cs delete mode 100644 src/Billing/Controllers/FreshsalesController.cs delete mode 100644 src/Billing/Models/FreshdeskReplyRequestModel.cs delete mode 100644 src/Billing/Models/FreshdeskWebhookModel.cs delete mode 100644 src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs delete mode 100644 src/Billing/Models/OnyxResponseModel.cs delete mode 100644 test/Billing.Test/Controllers/FreshdeskControllerTests.cs delete mode 100644 test/Billing.Test/Controllers/FreshsalesControllerTests.cs diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 64a52ed290..2830f603ac 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -9,10 +9,7 @@ public class BillingSettings public virtual string StripeWebhookKey { get; set; } public virtual string StripeWebhookSecret20250827Basil { get; set; } public virtual string AppleWebhookKey { get; set; } - public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings(); - public virtual string FreshsalesApiKey { get; set; } public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings(); - public virtual OnyxSettings Onyx { get; set; } = new OnyxSettings(); public class PayPalSettings { @@ -21,35 +18,4 @@ public class BillingSettings public virtual string WebhookKey { get; set; } } - public class FreshDeskSettings - { - public virtual string ApiKey { get; set; } - public virtual string WebhookKey { get; set; } - /// - /// Indicates the data center region. Valid values are "US" and "EU" - /// - public virtual string Region { get; set; } - public virtual string UserFieldName { get; set; } - public virtual string OrgFieldName { get; set; } - - public virtual bool RemoveNewlinesInReplies { get; set; } = false; - public virtual string AutoReplyGreeting { get; set; } = string.Empty; - public virtual string AutoReplySalutation { get; set; } = string.Empty; - } - - public class OnyxSettings - { - public virtual string ApiKey { get; set; } - public virtual string BaseUrl { get; set; } - public virtual string Path { get; set; } - public virtual int PersonaId { get; set; } - public virtual bool UseAnswerWithCitationModels { get; set; } = true; - - public virtual SearchSettings SearchSettings { get; set; } = new SearchSettings(); - } - public class SearchSettings - { - public virtual string RunSearch { get; set; } = "auto"; // "always", "never", "auto" - public virtual bool RealTime { get; set; } = true; - } } diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs deleted file mode 100644 index 38ed05cfdf..0000000000 --- a/src/Billing/Controllers/FreshdeskController.cs +++ /dev/null @@ -1,395 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Net.Http.Headers; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Web; -using Bit.Billing.Models; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Bit.Core.Utilities; -using Markdig; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Bit.Billing.Controllers; - -[Route("freshdesk")] -public class FreshdeskController : Controller -{ - private readonly BillingSettings _billingSettings; - private readonly IUserRepository _userRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly ILogger _logger; - private readonly GlobalSettings _globalSettings; - private readonly IHttpClientFactory _httpClientFactory; - - public FreshdeskController( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOptions billingSettings, - ILogger logger, - GlobalSettings globalSettings, - IHttpClientFactory httpClientFactory) - { - _billingSettings = billingSettings?.Value ?? throw new ArgumentNullException(nameof(billingSettings)); - _userRepository = userRepository; - _organizationRepository = organizationRepository; - _logger = logger; - _globalSettings = globalSettings; - _httpClientFactory = httpClientFactory; - } - - [HttpPost("webhook")] - public async Task PostWebhook([FromQuery, Required] string key, - [FromBody, Required] FreshdeskWebhookModel model) - { - if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshDesk.WebhookKey)) - { - return new BadRequestResult(); - } - - try - { - var ticketId = model.TicketId; - var ticketContactEmail = model.TicketContactEmail; - var ticketTags = model.TicketTags; - if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail)) - { - return new BadRequestResult(); - } - - var updateBody = new Dictionary(); - var note = string.Empty; - note += $"
  • Region: {_billingSettings.FreshDesk.Region}
  • "; - var customFields = new Dictionary(); - var user = await _userRepository.GetByEmailAsync(ticketContactEmail); - if (user == null) - { - note += $"
  • No user found: {ticketContactEmail}
  • "; - await CreateNote(ticketId, note); - } - - if (user != null) - { - var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"; - note += $"
  • User, {user.Email}: {userLink}
  • "; - customFields.Add(_billingSettings.FreshDesk.UserFieldName, userLink); - var tags = new HashSet(); - if (user.Premium) - { - tags.Add("Premium"); - } - var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); - - foreach (var org in orgs) - { - // Prevent org names from injecting any additional HTML - var orgName = HttpUtility.HtmlEncode(org.Name); - var orgNote = $"{orgName} ({org.Seats.GetValueOrDefault()}): " + - $"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}"; - note += $"
  • Org, {orgNote}
  • "; - if (!customFields.Any(kvp => kvp.Key == _billingSettings.FreshDesk.OrgFieldName)) - { - customFields.Add(_billingSettings.FreshDesk.OrgFieldName, orgNote); - } - else - { - customFields[_billingSettings.FreshDesk.OrgFieldName] += $"\n{orgNote}"; - } - - var displayAttribute = GetAttribute(org.PlanType); - var planName = displayAttribute?.Name?.Split(" ").FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(planName)) - { - tags.Add(string.Format("Org: {0}", planName)); - } - } - if (tags.Any()) - { - var tagsToUpdate = tags.ToList(); - if (!string.IsNullOrWhiteSpace(ticketTags)) - { - var splitTicketTags = ticketTags.Split(','); - for (var i = 0; i < splitTicketTags.Length; i++) - { - tagsToUpdate.Insert(i, splitTicketTags[i]); - } - } - updateBody.Add("tags", tagsToUpdate); - } - - if (customFields.Any()) - { - updateBody.Add("custom_fields", customFields); - } - var updateRequest = new HttpRequestMessage(HttpMethod.Put, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId)) - { - Content = JsonContent.Create(updateBody), - }; - await CallFreshdeskApiAsync(updateRequest); - await CreateNote(ticketId, note); - } - - return new OkResult(); - } - catch (Exception e) - { - _logger.LogError(e, "Error processing freshdesk webhook."); - return new BadRequestResult(); - } - } - - [HttpPost("webhook-onyx-ai")] - public async Task PostWebhookOnyxAi([FromQuery, Required] string key, - [FromBody, Required] FreshdeskOnyxAiWebhookModel model) - { - // ensure that the key is from Freshdesk - if (!IsValidRequestFromFreshdesk(key)) - { - return new BadRequestResult(); - } - - // if there is no description, then we don't send anything to onyx - if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim())) - { - return Ok(); - } - - // Get response from Onyx AI - var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model); - - // the CallOnyxApi will return a null if we have an error response - if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg)) - { - _logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ", - JsonSerializer.Serialize(model), - JsonSerializer.Serialize(onyxRequest), - JsonSerializer.Serialize(onyxResponse)); - - return Ok(); // return ok so we don't retry - } - - // add the answer as a note to the ticket - await AddAnswerNoteToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId); - - return Ok(); - } - - [HttpPost("webhook-onyx-ai-reply")] - public async Task PostWebhookOnyxAiReply([FromQuery, Required] string key, - [FromBody, Required] FreshdeskOnyxAiWebhookModel model) - { - // NOTE: - // at this time, this endpoint is a duplicate of `webhook-onyx-ai` - // eventually, we will merge both endpoints into one webhook for Freshdesk - - // ensure that the key is from Freshdesk - if (!IsValidRequestFromFreshdesk(key) || !ModelState.IsValid) - { - return new BadRequestResult(); - } - - // if there is no description, then we don't send anything to onyx - if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim())) - { - return Ok(); - } - - // create the onyx `answer-with-citation` request - var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model); - - // the CallOnyxApi will return a null if we have an error response - if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg)) - { - _logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ", - JsonSerializer.Serialize(model), - JsonSerializer.Serialize(onyxRequest), - JsonSerializer.Serialize(onyxResponse)); - - return Ok(); // return ok so we don't retry - } - - // add the reply to the ticket - await AddReplyToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId); - - return Ok(); - } - - private bool IsValidRequestFromFreshdesk(string key) - { - if (string.IsNullOrWhiteSpace(key) - || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshDesk.WebhookKey)) - { - return false; - } - - return true; - } - - private async Task CreateNote(string ticketId, string note) - { - var noteBody = new Dictionary - { - { "body", $"
      {note}
    " }, - { "private", true } - }; - var noteRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) - { - Content = JsonContent.Create(noteBody), - }; - await CallFreshdeskApiAsync(noteRequest); - } - - private async Task AddAnswerNoteToTicketAsync(string note, string ticketId) - { - // if there is no content, then we don't need to add a note - if (string.IsNullOrWhiteSpace(note)) - { - return; - } - - var noteBody = new Dictionary - { - { "body", $"Onyx AI:
      {note}
    " }, - { "private", true } - }; - - var noteRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) - { - Content = JsonContent.Create(noteBody), - }; - - var addNoteResponse = await CallFreshdeskApiAsync(noteRequest); - if (addNoteResponse.StatusCode != System.Net.HttpStatusCode.Created) - { - _logger.LogError("Error adding note to Freshdesk ticket. Ticket Id: {0}. Status: {1}", - ticketId, addNoteResponse.ToString()); - } - } - - private async Task AddReplyToTicketAsync(string note, string ticketId) - { - // if there is no content, then we don't need to add a note - if (string.IsNullOrWhiteSpace(note)) - { - return; - } - - // convert note from markdown to html - var htmlNote = note; - try - { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); - htmlNote = Markdig.Markdown.ToHtml(note, pipeline); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error converting markdown to HTML for Freshdesk reply. Ticket Id: {0}. Note: {1}", - ticketId, note); - htmlNote = note; // fallback to the original note - } - - // clear out any new lines that Freshdesk doesn't like - if (_billingSettings.FreshDesk.RemoveNewlinesInReplies) - { - htmlNote = htmlNote.Replace(Environment.NewLine, string.Empty); - } - - var replyBody = new FreshdeskReplyRequestModel - { - Body = $"{_billingSettings.FreshDesk.AutoReplyGreeting}{htmlNote}{_billingSettings.FreshDesk.AutoReplySalutation}", - }; - - var replyRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/reply", ticketId)) - { - Content = JsonContent.Create(replyBody), - }; - - var addReplyResponse = await CallFreshdeskApiAsync(replyRequest); - if (addReplyResponse.StatusCode != System.Net.HttpStatusCode.Created) - { - _logger.LogError("Error adding reply to Freshdesk ticket. Ticket Id: {0}. Status: {1}", - ticketId, addReplyResponse.ToString()); - } - } - - private async Task CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0) - { - try - { - var freshdeskAuthkey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshDesk.ApiKey}:X")); - var httpClient = _httpClientFactory.CreateClient("FreshdeskApi"); - request.Headers.Add("Authorization", $"Basic {freshdeskAuthkey}"); - var response = await httpClient.SendAsync(request); - if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3) - { - return response; - } - } - catch - { - if (retriedCount > 3) - { - throw; - } - } - await Task.Delay(30000 * (retriedCount + 1)); - return await CallFreshdeskApiAsync(request, retriedCount++); - } - - async Task<(OnyxRequestModel onyxRequest, OnyxResponseModel onyxResponse)> GetAnswerFromOnyx(FreshdeskOnyxAiWebhookModel model) - { - // TODO: remove the use of the deprecated answer-with-citation models after we are sure - if (_billingSettings.Onyx.UseAnswerWithCitationModels) - { - var onyxRequest = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx); - var onyxAnswerWithCitationRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) - { - Content = JsonContent.Create(onyxRequest, mediaType: new MediaTypeHeaderValue("application/json")), - }; - var onyxResponse = await CallOnyxApi(onyxAnswerWithCitationRequest); - return (onyxRequest, onyxResponse); - } - - var request = new OnyxSendMessageSimpleApiRequestModel(model.TicketDescriptionText, _billingSettings.Onyx); - var onyxSimpleRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("{0}{1}", _billingSettings.Onyx.BaseUrl, _billingSettings.Onyx.Path)) - { - Content = JsonContent.Create(request, mediaType: new MediaTypeHeaderValue("application/json")), - }; - var onyxSimpleResponse = await CallOnyxApi(onyxSimpleRequest); - return (request, onyxSimpleResponse); - } - - private async Task CallOnyxApi(HttpRequestMessage request) where T : class, new() - { - var httpClient = _httpClientFactory.CreateClient("OnyxApi"); - var response = await httpClient.SendAsync(request); - - if (response.StatusCode != System.Net.HttpStatusCode.OK) - { - _logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}", - response.StatusCode, JsonSerializer.Serialize(response)); - return new T(); - } - var responseStr = await response.Content.ReadAsStringAsync(); - var responseJson = JsonSerializer.Deserialize(responseStr, options: new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }); - - return responseJson ?? new T(); - } - - private TAttribute? GetAttribute(Enum enumValue) where TAttribute : Attribute - { - var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault(); - return memberInfo != null ? memberInfo.GetCustomAttribute() : null; - } -} diff --git a/src/Billing/Controllers/FreshsalesController.cs b/src/Billing/Controllers/FreshsalesController.cs deleted file mode 100644 index 68382fbd5d..0000000000 --- a/src/Billing/Controllers/FreshsalesController.cs +++ /dev/null @@ -1,248 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Net.Http.Headers; -using System.Text.Json.Serialization; -using Bit.Core.Billing.Enums; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Bit.Core.Utilities; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; - -namespace Bit.Billing.Controllers; - -[Route("freshsales")] -public class FreshsalesController : Controller -{ - private readonly IUserRepository _userRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly ILogger _logger; - private readonly GlobalSettings _globalSettings; - - private readonly string _freshsalesApiKey; - - private readonly HttpClient _httpClient; - - public FreshsalesController(IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOptions billingSettings, - ILogger logger, - GlobalSettings globalSettings) - { - _userRepository = userRepository; - _organizationRepository = organizationRepository; - _logger = logger; - _globalSettings = globalSettings; - - _httpClient = new HttpClient - { - BaseAddress = new Uri("https://bitwarden.freshsales.io/api/") - }; - - _freshsalesApiKey = billingSettings.Value.FreshsalesApiKey; - - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( - "Token", - $"token={_freshsalesApiKey}"); - } - - - [HttpPost("webhook")] - public async Task PostWebhook([FromHeader(Name = "Authorization")] string key, - [FromBody] CustomWebhookRequestModel request, - CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(_freshsalesApiKey, key)) - { - return Unauthorized(); - } - - try - { - var leadResponse = await _httpClient.GetFromJsonAsync>( - $"leads/{request.LeadId}", - cancellationToken); - - var lead = leadResponse.Lead; - - var primaryEmail = lead.Emails - .Where(e => e.IsPrimary) - .FirstOrDefault(); - - if (primaryEmail == null) - { - return BadRequest(new { Message = "Lead has not primary email." }); - } - - var user = await _userRepository.GetByEmailAsync(primaryEmail.Value); - - if (user == null) - { - return NoContent(); - } - - var newTags = new HashSet(); - - if (user.Premium) - { - newTags.Add("Premium"); - } - - var noteItems = new List - { - $"User, {user.Email}: {_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}" - }; - - var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); - - foreach (var org in orgs) - { - noteItems.Add($"Org, {org.DisplayName()}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}"); - if (TryGetPlanName(org.PlanType, out var planName)) - { - newTags.Add($"Org: {planName}"); - } - } - - if (newTags.Any()) - { - var allTags = newTags.Concat(lead.Tags); - var updateLeadResponse = await _httpClient.PutAsJsonAsync( - $"leads/{request.LeadId}", - CreateWrapper(new { tags = allTags }), - cancellationToken); - updateLeadResponse.EnsureSuccessStatusCode(); - } - - var createNoteResponse = await _httpClient.PostAsJsonAsync( - "notes", - CreateNoteRequestModel(request.LeadId, string.Join('\n', noteItems)), cancellationToken); - createNoteResponse.EnsureSuccessStatusCode(); - return NoContent(); - } - catch (Exception ex) - { - Console.WriteLine(ex); - _logger.LogError(ex, "Error processing freshsales webhook"); - return BadRequest(new { ex.Message }); - } - } - - private static LeadWrapper CreateWrapper(T lead) - { - return new LeadWrapper - { - Lead = lead, - }; - } - - private static CreateNoteRequestModel CreateNoteRequestModel(long leadId, string content) - { - return new CreateNoteRequestModel - { - Note = new EditNoteModel - { - Description = content, - TargetableType = "Lead", - TargetableId = leadId, - }, - }; - } - - private static bool TryGetPlanName(PlanType planType, out string planName) - { - switch (planType) - { - case PlanType.Free: - planName = "Free"; - return true; - case PlanType.FamiliesAnnually: - case PlanType.FamiliesAnnually2025: - case PlanType.FamiliesAnnually2019: - planName = "Families"; - return true; - case PlanType.TeamsAnnually: - case PlanType.TeamsAnnually2023: - case PlanType.TeamsAnnually2020: - case PlanType.TeamsAnnually2019: - case PlanType.TeamsMonthly: - case PlanType.TeamsMonthly2023: - case PlanType.TeamsMonthly2020: - case PlanType.TeamsMonthly2019: - case PlanType.TeamsStarter: - case PlanType.TeamsStarter2023: - planName = "Teams"; - return true; - case PlanType.EnterpriseAnnually: - case PlanType.EnterpriseAnnually2023: - case PlanType.EnterpriseAnnually2020: - case PlanType.EnterpriseAnnually2019: - case PlanType.EnterpriseMonthly: - case PlanType.EnterpriseMonthly2023: - case PlanType.EnterpriseMonthly2020: - case PlanType.EnterpriseMonthly2019: - planName = "Enterprise"; - return true; - case PlanType.Custom: - planName = "Custom"; - return true; - default: - planName = null; - return false; - } - } -} - -public class CustomWebhookRequestModel -{ - [JsonPropertyName("leadId")] - public long LeadId { get; set; } -} - -public class LeadWrapper -{ - [JsonPropertyName("lead")] - public T Lead { get; set; } - - public static LeadWrapper Create(TItem lead) - { - return new LeadWrapper - { - Lead = lead, - }; - } -} - -public class FreshsalesLeadModel -{ - public string[] Tags { get; set; } - public FreshsalesEmailModel[] Emails { get; set; } -} - -public class FreshsalesEmailModel -{ - [JsonPropertyName("value")] - public string Value { get; set; } - - [JsonPropertyName("is_primary")] - public bool IsPrimary { get; set; } -} - -public class CreateNoteRequestModel -{ - [JsonPropertyName("note")] - public EditNoteModel Note { get; set; } -} - -public class EditNoteModel -{ - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("targetable_type")] - public string TargetableType { get; set; } - - [JsonPropertyName("targetable_id")] - public long TargetableId { get; set; } -} diff --git a/src/Billing/Models/FreshdeskReplyRequestModel.cs b/src/Billing/Models/FreshdeskReplyRequestModel.cs deleted file mode 100644 index 3927039769..0000000000 --- a/src/Billing/Models/FreshdeskReplyRequestModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Bit.Billing.Models; - -public class FreshdeskReplyRequestModel -{ - [JsonPropertyName("body")] - public required string Body { get; set; } -} diff --git a/src/Billing/Models/FreshdeskWebhookModel.cs b/src/Billing/Models/FreshdeskWebhookModel.cs deleted file mode 100644 index aac0e9339d..0000000000 --- a/src/Billing/Models/FreshdeskWebhookModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Text.Json.Serialization; - -namespace Bit.Billing.Models; - -public class FreshdeskWebhookModel -{ - [JsonPropertyName("ticket_id")] - public string TicketId { get; set; } - - [JsonPropertyName("ticket_contact_email")] - public string TicketContactEmail { get; set; } - - [JsonPropertyName("ticket_tags")] - public string TicketTags { get; set; } -} - -public class FreshdeskOnyxAiWebhookModel : FreshdeskWebhookModel -{ - [JsonPropertyName("ticket_description_text")] - public string TicketDescriptionText { get; set; } -} diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs deleted file mode 100644 index 9a753be4bc..0000000000 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Text.Json.Serialization; -using static Bit.Billing.BillingSettings; - -namespace Bit.Billing.Models; - -public class OnyxRequestModel -{ - [JsonPropertyName("persona_id")] - public int PersonaId { get; set; } = 1; - - [JsonPropertyName("retrieval_options")] - public RetrievalOptions RetrievalOptions { get; set; } = new RetrievalOptions(); - - public OnyxRequestModel(OnyxSettings onyxSettings) - { - PersonaId = onyxSettings.PersonaId; - RetrievalOptions.RunSearch = onyxSettings.SearchSettings.RunSearch; - RetrievalOptions.RealTime = onyxSettings.SearchSettings.RealTime; - } -} - -/// -/// This is used with the onyx endpoint /query/answer-with-citation -/// which has been deprecated. This can be removed once later -/// -public class OnyxAnswerWithCitationRequestModel : OnyxRequestModel -{ - [JsonPropertyName("messages")] - public List Messages { get; set; } = new List(); - - public OnyxAnswerWithCitationRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) - { - message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); - Messages = new List() { new Message() { MessageText = message } }; - } -} - -/// -/// This is used with the onyx endpoint /chat/send-message-simple-api -/// -public class OnyxSendMessageSimpleApiRequestModel : OnyxRequestModel -{ - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; - - public OnyxSendMessageSimpleApiRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) - { - Message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); - } -} - -public class Message -{ - [JsonPropertyName("message")] - public string MessageText { get; set; } = string.Empty; - - [JsonPropertyName("sender")] - public string Sender { get; set; } = "user"; -} - -public class RetrievalOptions -{ - [JsonPropertyName("run_search")] - public string RunSearch { get; set; } = RetrievalOptionsRunSearch.Auto; - - [JsonPropertyName("real_time")] - public bool RealTime { get; set; } = true; -} - -public class RetrievalOptionsRunSearch -{ - public const string Always = "always"; - public const string Never = "never"; - public const string Auto = "auto"; -} diff --git a/src/Billing/Models/OnyxResponseModel.cs b/src/Billing/Models/OnyxResponseModel.cs deleted file mode 100644 index 96fa134c40..0000000000 --- a/src/Billing/Models/OnyxResponseModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Bit.Billing.Models; - -public class OnyxResponseModel -{ - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; - - [JsonPropertyName("answer_citationless")] - public string AnswerCitationless { get; set; } = string.Empty; - - [JsonPropertyName("error_msg")] - public string ErrorMsg { get; set; } = string.Empty; -} diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 1343dc0895..30f4f5f562 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -2,7 +2,6 @@ #nullable disable using System.Globalization; -using System.Net.Http.Headers; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; using Bit.Commercial.Core.Utilities; @@ -98,13 +97,6 @@ public class Startup // Authentication services.AddAuthentication(); - // Set up HttpClients - services.AddHttpClient("FreshdeskApi"); - services.AddHttpClient("OnyxApi", client => - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", billingSettings.Onyx.ApiKey); - }); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Billing/appsettings.Development.json b/src/Billing/appsettings.Development.json index fe8e47b2f6..77057fde7f 100644 --- a/src/Billing/appsettings.Development.json +++ b/src/Billing/appsettings.Development.json @@ -32,10 +32,5 @@ "connectionString": "UseDevelopmentStorage=true" } }, - "billingSettings": { - "onyx": { - "personaId": 68 - } - }, "pricingUri": "https://billingpricing.qa.bitwarden.pw" } diff --git a/src/Billing/appsettings.Production.json b/src/Billing/appsettings.Production.json index 4be5d51a52..819986181f 100644 --- a/src/Billing/appsettings.Production.json +++ b/src/Billing/appsettings.Production.json @@ -26,10 +26,7 @@ "payPal": { "production": true, "businessId": "4ZDA7DLUUJGMN" - }, - "onyx": { - "personaId": 7 - } + } }, "Logging": { "IncludeScopes": false, diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index aa14f1d377..7093b6a923 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -61,27 +61,6 @@ "production": false, "businessId": "AD3LAUZSNVPJY", "webhookKey": "SECRET" - }, - "freshdesk": { - "apiKey": "SECRET", - "webhookKey": "SECRET", - "region": "US", - "userFieldName": "cf_user", - "orgFieldName": "cf_org", - "removeNewlinesInReplies": true, - "autoReplyGreeting": "Greetings,

    Thank you for contacting Bitwarden. The reply below was generated by our AI agent based on your message:

    ", - "autoReplySalutation": "

    If this response doesn’t fully address your question, simply reply to this email and a member of our Customer Success team will be happy to assist you further.

    Best Regards,
    The Bitwarden Customer Success Team

    " - }, - "onyx": { - "apiKey": "SECRET", - "baseUrl": "https://cloud.onyx.app/api", - "path": "/chat/send-message-simple-api", - "useAnswerWithCitationModels": true, - "personaId": 7, - "searchSettings": { - "runSearch": "always", - "realTime": true - } } } } diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs deleted file mode 100644 index 5c9199d29a..0000000000 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System.Text.Json; -using Bit.Billing.Controllers; -using Bit.Billing.Models; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; -using Bit.Core.Repositories; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NSubstitute; -using NSubstitute.ReceivedExtensions; -using Xunit; - -namespace Bit.Billing.Test.Controllers; - -[ControllerCustomize(typeof(FreshdeskController))] -[SutProviderCustomize] -public class FreshdeskControllerTests -{ - private const string ApiKey = "TESTFRESHDESKAPIKEY"; - private const string WebhookKey = "TESTKEY"; - - private const string UserFieldName = "cf_user"; - private const string OrgFieldName = "cf_org"; - - [Theory] - [BitAutoData((string)null, null)] - [BitAutoData((string)null)] - [BitAutoData(WebhookKey, null)] - public async Task PostWebhook_NullRequiredParameters_BadRequest(string freshdeskWebhookKey, FreshdeskWebhookModel model, - BillingSettings billingSettings, SutProvider sutProvider) - { - sutProvider.GetDependency>().Value.FreshDesk.WebhookKey.Returns(billingSettings.FreshDesk.WebhookKey); - - var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model); - - var statusCodeResult = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode); - } - - [Theory] - [BitAutoData] - public async Task PostWebhook_Success(User user, FreshdeskWebhookModel model, - List organizations, SutProvider sutProvider) - { - model.TicketContactEmail = user.Email; - - sutProvider.GetDependency().GetByEmailAsync(user.Email).Returns(user); - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(organizations); - - var mockHttpMessageHandler = Substitute.ForPartsOf(); - var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockResponse); - var httpClient = new HttpClient(mockHttpMessageHandler); - - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); - - sutProvider.GetDependency>().Value.FreshDesk.WebhookKey.Returns(WebhookKey); - sutProvider.GetDependency>().Value.FreshDesk.ApiKey.Returns(ApiKey); - sutProvider.GetDependency>().Value.FreshDesk.UserFieldName.Returns(UserFieldName); - sutProvider.GetDependency>().Value.FreshDesk.OrgFieldName.Returns(OrgFieldName); - - var response = await sutProvider.Sut.PostWebhook(WebhookKey, model); - - var statusCodeResult = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); - - _ = mockHttpMessageHandler.Received(1).Send(Arg.Is(m => m.Method == HttpMethod.Put && m.RequestUri.ToString().EndsWith(model.TicketId)), Arg.Any()); - _ = mockHttpMessageHandler.Received(1).Send(Arg.Is(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any()); - } - - [Theory] - [BitAutoData(WebhookKey)] - public async Task PostWebhook_add_note_when_user_is_invalid( - string freshdeskWebhookKey, FreshdeskWebhookModel model, - SutProvider sutProvider) - { - // Arrange - for an invalid user - model.TicketContactEmail = "invalid@user"; - sutProvider.GetDependency().GetByEmailAsync(model.TicketContactEmail).Returns((User)null); - sutProvider.GetDependency>().Value.FreshDesk.WebhookKey.Returns(WebhookKey); - - var mockHttpMessageHandler = Substitute.ForPartsOf(); - var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockResponse); - var httpClient = new HttpClient(mockHttpMessageHandler); - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); - - // Act - var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model); - - // Assert - var statusCodeResult = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); - - await mockHttpMessageHandler - .Received(1).Send( - Arg.Is( - m => m.Method == HttpMethod.Post - && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes") - && m.Content.ReadAsStringAsync().Result.Contains("No user found")), - Arg.Any()); - } - - - [Theory] - [BitAutoData((string)null, null)] - [BitAutoData((string)null)] - [BitAutoData(WebhookKey, null)] - public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest( - string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - BillingSettings billingSettings, SutProvider sutProvider) - { - sutProvider.GetDependency>() - .Value.FreshDesk.WebhookKey.Returns(billingSettings.FreshDesk.WebhookKey); - - var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - - var statusCodeResult = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode); - } - - [Theory] - [BitAutoData(WebhookKey)] - public async Task PostWebhookOnyxAi_invalid_onyx_response_results_is_logged( - string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - SutProvider sutProvider) - { - var billingSettings = sutProvider.GetDependency>().Value; - billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); - billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); - - // mocking freshdesk Api request for ticket info - var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); - var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); - - // mocking Onyx api response given a ticket description - var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); - var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); - mockOnyxHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockOnyxResponse); - var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler); - - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient); - sutProvider.GetDependency().CreateClient("OnyxApi").Returns(onyxHttpClient); - - var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - - var statusCodeResult = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); - - var _logger = sutProvider.GetDependency>(); - - // workaround because _logger.Received(1).LogWarning(...) does not work - _logger.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Log" && c.GetArguments()[1].ToString().Contains("Error getting answer from Onyx AI")); - - // sent call to Onyx API - but we got an error response - _ = mockOnyxHttpMessageHandler.Received(1).Send(Arg.Any(), Arg.Any()); - // did not call freshdesk to add a note since onyx failed - _ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData(WebhookKey)] - public async Task PostWebhookOnyxAi_success( - string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - OnyxResponseModel onyxResponse, - SutProvider sutProvider) - { - var billingSettings = sutProvider.GetDependency>().Value; - billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); - billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); - - // mocking freshdesk api add note request (POST) - var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); - var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); - mockFreshdeskHttpMessageHandler.Send( - Arg.Is(_ => _.Method == HttpMethod.Post), - Arg.Any()) - .Returns(mockFreshdeskAddNoteResponse); - var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); - - // mocking Onyx api response given a ticket description - var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); - onyxResponse.ErrorMsg = "string.Empty"; - var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(JsonSerializer.Serialize(onyxResponse)) - }; - mockOnyxHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockOnyxResponse); - var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler); - - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient); - sutProvider.GetDependency().CreateClient("OnyxApi").Returns(onyxHttpClient); - - var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - - var result = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - } - - [Theory] - [BitAutoData(WebhookKey)] - public async Task PostWebhookOnyxAi_ticket_description_is_empty_return_success( - string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - SutProvider sutProvider) - { - var billingSettings = sutProvider.GetDependency>().Value; - billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); - billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); - - model.TicketDescriptionText = " "; // empty description - - // mocking freshdesk api add note request (POST) - var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); - var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); - - // mocking Onyx api response given a ticket description - var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); - var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler); - - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient); - sutProvider.GetDependency().CreateClient("OnyxApi").Returns(onyxHttpClient); - - var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - - var result = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - _ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any(), Arg.Any()); - _ = mockOnyxHttpMessageHandler.DidNotReceive().Send(Arg.Any(), Arg.Any()); - } - - public class MockHttpMessageHandler : HttpMessageHandler - { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return Send(request, cancellationToken); - } - - public new virtual Task Send(HttpRequestMessage request, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} diff --git a/test/Billing.Test/Controllers/FreshsalesControllerTests.cs b/test/Billing.Test/Controllers/FreshsalesControllerTests.cs deleted file mode 100644 index c9ae6efb1a..0000000000 --- a/test/Billing.Test/Controllers/FreshsalesControllerTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Bit.Billing.Controllers; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NSubstitute; -using Xunit; - -namespace Bit.Billing.Test.Controllers; - -public class FreshsalesControllerTests -{ - private const string ApiKey = "TEST_FRESHSALES_APIKEY"; - private const string TestLead = "TEST_FRESHSALES_TESTLEAD"; - - private static (FreshsalesController, IUserRepository, IOrganizationRepository) CreateSut( - string freshsalesApiKey) - { - var userRepository = Substitute.For(); - var organizationRepository = Substitute.For(); - - var billingSettings = Options.Create(new BillingSettings - { - FreshsalesApiKey = freshsalesApiKey, - }); - var globalSettings = new GlobalSettings(); - globalSettings.BaseServiceUri.Admin = "https://test.com"; - - var sut = new FreshsalesController( - userRepository, - organizationRepository, - billingSettings, - Substitute.For>(), - globalSettings - ); - - return (sut, userRepository, organizationRepository); - } - - [RequiredEnvironmentTheory(ApiKey, TestLead), EnvironmentData(ApiKey, TestLead)] - public async Task PostWebhook_Success(string freshsalesApiKey, long leadId) - { - // This test is only for development to use: - // `export TEST_FRESHSALES_APIKEY=[apikey]` - // `export TEST_FRESHSALES_TESTLEAD=[lead id]` - // `dotnet test --filter "FullyQualifiedName~FreshsalesControllerTests.PostWebhook_Success"` - var (sut, userRepository, organizationRepository) = CreateSut(freshsalesApiKey); - - var user = new User - { - Id = Guid.NewGuid(), - Email = "test@email.com", - Premium = true, - }; - - userRepository.GetByEmailAsync(user.Email) - .Returns(user); - - organizationRepository.GetManyByUserIdAsync(user.Id) - .Returns(new List - { - new Organization - { - Id = Guid.NewGuid(), - Name = "Test Org", - } - }); - - var response = await sut.PostWebhook(freshsalesApiKey, new CustomWebhookRequestModel - { - LeadId = leadId, - }, new CancellationToken(false)); - - var statusCodeResult = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status204NoContent, statusCodeResult.StatusCode); - } -} From 2dce8722d6d3b5df69e21ca6f929e6b8b68bfd74 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:14:18 -0600 Subject: [PATCH 26/68] Remove unused FF (#6709) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index e1ccbbd9b8..b732420e82 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -187,7 +187,6 @@ public static class FeatureFlagKeys /* Billing Team */ public const string TrialPayment = "PM-8163-trial-payment"; - 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"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; From a82365b5dfb2caa4fee703b9136843c0ade9f5e9 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:29:28 -0500 Subject: [PATCH 27/68] PM-30125 - IdentityTokenResponse - mark fields as deprecated (#6773) --- .../IdentityServer/RequestValidators/BaseRequestValidator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 0bdf1d89c2..e07446d49f 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -659,6 +659,7 @@ public abstract class BaseRequestValidator where T : class var customResponse = new Dictionary(); if (!string.IsNullOrWhiteSpace(user.PrivateKey)) { + // PrivateKey usage is now deprecated in favor of AccountKeys customResponse.Add("PrivateKey", user.PrivateKey); var accountKeys = await _accountKeysQuery.Run(user); customResponse.Add("AccountKeys", new PrivateKeysResponseModel(accountKeys)); @@ -666,6 +667,7 @@ public abstract class BaseRequestValidator where T : class if (!string.IsNullOrWhiteSpace(user.Key)) { + // Key is deprecated in favor of UserDecryptionOptions.MasterPasswordUnlock.MasterKeyEncryptedUserKey customResponse.Add("Key", user.Key); } From fafc61d7b9a50570c7bab0445fee180ad8a61a95 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:00:17 -0500 Subject: [PATCH 28/68] [BRE-1439] Removing obsolete Server image from publish workflow (#6774) --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6f00d4f85f..7983bef2bc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -91,7 +91,6 @@ jobs: - project_name: Nginx - project_name: Notifications - project_name: Scim - - project_name: Server - project_name: Setup - project_name: Sso steps: From c632a9490af20df7351fc1db541d6ac56a91747f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:51:54 -0600 Subject: [PATCH 29/68] [deps] Platform: Update Azure.Messaging.EventGrid to v5 (#6215) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Api/Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 48fedfc8c1..dd27de2e63 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -33,7 +33,7 @@ - + From 3486d293300784c02f3d0235c3f1e3d2f049e570 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:12:14 -0800 Subject: [PATCH 30/68] remove RemoveCardItemTypePolicy flag (#6760) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b732420e82..eb42754475 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -244,7 +244,6 @@ public static class FeatureFlagKeys public const string CipherKeyEncryption = "cipher-key-encryption"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string PhishingDetection = "phishing-detection"; - public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy"; public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view"; public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption"; public const string PM23904_RiskInsightsForPremium = "pm-23904-risk-insights-for-premium"; From f80a5696a1dd130708d615b4ace17533d78c189a Mon Sep 17 00:00:00 2001 From: Tyler <71953103+fntyler@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:02:17 -0500 Subject: [PATCH 31/68] BRE-1005 docs(README): add dynamic badges for container image digests (#6769) BRE-1005 docs(README): add dynamic badges for container image digests * remove links to packages --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index c817931c67..6aa609bc8c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,42 @@ Invoke-RestMethod -OutFile bitwarden.ps1 ` .\bitwarden.ps1 -start ``` +## Production Container Images + +
    +View Current Production Image Hashes (click to expand) +
    + +### US Production Cluster + +| Service | Image Hash | +|---------|------------| +| **Admin** | ![admin](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.admin&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **API** | ![api](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.api&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Billing** | ![billing](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.billing&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Events** | ![events](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.events&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **EventsProcessor** | ![eventsprocessor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.eventsprocessor&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Identity** | ![identity](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.identity&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Notifications** | ![notifications](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.notifications&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **SCIM** | ![scim](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.scim&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **SSO** | ![sso](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.sso&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | + +### EU Production Cluster + +| Service | Image Hash | +|---------|------------| +| **Admin** | ![admin](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.admin&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **API** | ![api](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.api&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Billing** | ![billing](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.billing&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Events** | ![events](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.events&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **EventsProcessor** | ![eventsprocessor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.eventsprocessor&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Identity** | ![identity](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.identity&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **Notifications** | ![notifications](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.notifications&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **SCIM** | ![scim](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.scim&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | +| **SSO** | ![sso](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.sso&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) | + +
    + ## We're Hiring! Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden. From 96622d7928a1907c124fd27f55258a45af57c70e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:34:19 -0500 Subject: [PATCH 32/68] [deps]: Update github-action minor (#6327) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 14 +++++++------- .github/workflows/code-references.yml | 2 +- .github/workflows/load-test.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/repository-management.yml | 4 ++-- .github/workflows/test-database.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1afaab0882..1e7b95cc75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,7 +123,7 @@ jobs: uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Set up Node - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: cache: "npm" cache-dependency-path: "**/package-lock.json" @@ -169,10 +169,10 @@ jobs: ########## Set up Docker ########## - name: Set up QEMU emulators - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 ########## ACRs ########## - name: Log in to Azure @@ -246,7 +246,7 @@ jobs: - name: Install Cosign if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 + uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 - name: Sign image with Cosign if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' @@ -264,7 +264,7 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@f6601287cdb1efc985d6b765bbf99cb4c0ac29d8 # v7.0.0 + uses: anchore/scan-action@3c9a191a0fbab285ca6b8530b5de5a642cba332f # v7.2.2 with: image: ${{ steps.image-tags.outputs.primary_tag }} fail-build: false @@ -481,7 +481,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Generate GH App token - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} @@ -531,7 +531,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Generate GH App token - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 98f5288ec8..cb7ca9e200 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -59,7 +59,7 @@ jobs: - name: Collect id: collect - uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0 + uses: launchdarkly/find-code-references@89a7d362d1d4b3725fe0fe0ccd0dc69e3bdcba58 # v2.14.0 with: accessToken: ${{ steps.get-kv-secrets.outputs.LD-ACCESS-TOKEN }} projKey: default diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index dd3cef9d83..10bfe50d10 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -95,7 +95,7 @@ jobs: uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0 - name: Run k6 tests - uses: grafana/run-k6-action@c6b79182b9b666aa4f630f4a6be9158ead62536e # v1.2.0 + uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d # v1.3.1 continue-on-error: false env: K6_OTEL_METRIC_PREFIX: k6_ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 887f78f5df..a3c4fb1ffd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,7 +89,7 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 + uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 with: artifacts: "docker-stub-US.zip, docker-stub-EU.zip, diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index a0f7ea73b1..c98faed340 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -83,7 +83,7 @@ jobs: version: ${{ inputs.version_number_override }} - name: Generate GH App token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} @@ -207,7 +207,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Generate GH App token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 5ce13b25c6..54ecd7962f 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -156,7 +156,7 @@ jobs: run: 'docker logs "$(docker ps --quiet --filter "name=mssql")"' - name: Report test results - uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0 + uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results @@ -165,7 +165,7 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - name: Docker Compose down if: always() diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72dd17d7d0..550d943dbc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: toolchain: stable - name: Cache cargo registry - uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7 + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - name: Print environment run: | @@ -59,7 +59,7 @@ jobs: run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" - name: Report test results - uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0 + uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results @@ -68,4 +68,4 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 From 67534e2cda7d466b6854425e082ad2b823d2c19f Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 26 Dec 2025 10:13:12 +1000 Subject: [PATCH 33/68] [PM-29556] Fix: changing organization plan nulls out public and private keys (#6738) Main fix: only assign new key value where old keys are not set and new keys have been provided. Refactors: - use consistent DTO model for keypairs - delete duplicate property assignment for new orgs --- .../Controllers/ProviderClientsController.cs | 3 +- .../OrganizationCreateRequestModel.cs | 5 +- .../OrganizationKeysRequestModel.cs | 49 +------ .../OrganizationNoPaymentCreateRequest.cs | 3 +- .../OrganizationUpdateRequestModel.cs | 3 +- .../OrganizationUpgradeRequestModel.cs | 5 +- .../Models/Requests/KeyPairRequestBody.cs | 8 ++ .../CloudOrganizationSignUpCommand.cs | 4 +- .../Organizations/OrganizationExtensions.cs | 28 ++++ ...ProviderClientOrganizationSignUpCommand.cs | 4 +- .../Update/OrganizationUpdateCommand.cs | 18 ++- .../Update/OrganizationUpdateExtensions.cs | 43 ------ .../Update/OrganizationUpdateRequest.cs | 13 +- .../Models/Business/OrganizationUpgrade.cs | 4 +- .../UpgradeOrganizationPlanCommand.cs | 12 +- .../ProviderClientsControllerTests.cs | 4 +- .../OrganizationUpdateCommandTests.cs | 16 ++- .../UpgradeOrganizationPlanCommandTests.cs | 131 ++++++++++++++++++ 18 files changed, 220 insertions(+), 133 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index caf2651e16..dfa6984826 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -57,8 +57,7 @@ public class ProviderClientsController( Owner = user, BillingEmail = provider.BillingEmail, OwnerKey = requestBody.Key, - PublicKey = requestBody.KeyPair.PublicKey, - PrivateKey = requestBody.KeyPair.EncryptedPrivateKey, + Keys = requestBody.KeyPair.ToPublicKeyEncryptionKeyPairData(), CollectionName = requestBody.CollectionName, IsFromProvider = true }; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 7754c44c8c..464ba0c2fd 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -113,11 +113,10 @@ public class OrganizationCreateRequestModel : IValidatableObject BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, - SkipTrial = SkipTrial + SkipTrial = SkipTrial, + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; - Keys?.ToOrganizationSignup(orgSignup); - return orgSignup; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs index 22b225a689..ef2fb0f07b 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs @@ -2,8 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Models.Business; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Api.AdminConsole.Models.Request.Organizations; @@ -14,48 +13,10 @@ public class OrganizationKeysRequestModel [Required] public string EncryptedPrivateKey { get; set; } - public OrganizationSignup ToOrganizationSignup(OrganizationSignup existingSignup) + public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData() { - if (string.IsNullOrWhiteSpace(existingSignup.PublicKey)) - { - existingSignup.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingSignup.PrivateKey)) - { - existingSignup.PrivateKey = EncryptedPrivateKey; - } - - return existingSignup; - } - - public OrganizationUpgrade ToOrganizationUpgrade(OrganizationUpgrade existingUpgrade) - { - if (string.IsNullOrWhiteSpace(existingUpgrade.PublicKey)) - { - existingUpgrade.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingUpgrade.PrivateKey)) - { - existingUpgrade.PrivateKey = EncryptedPrivateKey; - } - - return existingUpgrade; - } - - public Organization ToOrganization(Organization existingOrg) - { - if (string.IsNullOrWhiteSpace(existingOrg.PublicKey)) - { - existingOrg.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingOrg.PrivateKey)) - { - existingOrg.PrivateKey = EncryptedPrivateKey; - } - - return existingOrg; + return new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: EncryptedPrivateKey, + publicKey: PublicKey); } } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs index 0c62b23518..81d7c413eb 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs @@ -110,10 +110,9 @@ public class OrganizationNoPaymentCreateRequest BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; - Keys?.ToOrganizationSignup(orgSignup); - return orgSignup; } } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs index 6c3867fe09..a0b1247ae1 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -22,7 +22,6 @@ public class OrganizationUpdateRequestModel OrganizationId = organizationId, Name = Name, BillingEmail = BillingEmail, - PublicKey = Keys?.PublicKey, - EncryptedPrivateKey = Keys?.EncryptedPrivateKey + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs index a5dec192b9..7d5a9e56c7 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -43,11 +43,10 @@ public class OrganizationUpgradeRequestModel { BillingAddressCountry = BillingAddressCountry, BillingAddressPostalCode = BillingAddressPostalCode - } + }, + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; - Keys?.ToOrganizationUpgrade(orgUpgrade); - return orgUpgrade; } } diff --git a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs index 2fec3bd61d..9979141b6d 100644 --- a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs +++ b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Api.Billing.Models.Requests; @@ -12,4 +13,11 @@ public class KeyPairRequestBody public string PublicKey { get; set; } [Required(ErrorMessage = "'encryptedPrivateKey' must be provided")] public string EncryptedPrivateKey { get; set; } + + public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData() + { + return new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: EncryptedPrivateKey, + publicKey: PublicKey); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 7f24c4acd7..2aa09a5250 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -99,8 +99,8 @@ public class CloudOrganizationSignUpCommand( ReferenceData = signup.Owner.ReferenceData, Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, + PublicKey = signup.Keys?.PublicKey, + PrivateKey = signup.Keys?.WrappedPrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs new file mode 100644 index 0000000000..bb8f985495 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs @@ -0,0 +1,28 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public static class OrganizationExtensions +{ + /// + /// Updates the organization public and private keys if provided and not already set. + /// This is legacy code for old organizations that were not created with a public/private keypair. + /// It is a soft migration that will silently migrate organizations when they perform certain actions, + /// e.g. change their details or upgrade their plan. + /// + public static void BackfillPublicPrivateKeys(this Organization organization, PublicKeyEncryptionKeyPairData? keyPair) + { + // Only backfill if both new keys are provided and both old keys are missing. + if (string.IsNullOrWhiteSpace(keyPair?.PublicKey) || + string.IsNullOrWhiteSpace(keyPair.WrappedPrivateKey) || + !string.IsNullOrWhiteSpace(organization.PublicKey) || + !string.IsNullOrWhiteSpace(organization.PrivateKey)) + { + return; + } + + organization.PublicKey = keyPair.PublicKey; + organization.PrivateKey = keyPair.WrappedPrivateKey; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs index 4a8f08a4f7..c51ab2a5e0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -93,8 +93,8 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati ReferenceData = signup.Owner.ReferenceData, Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, + PublicKey = signup.Keys?.PublicKey, + PrivateKey = signup.Keys?.WrappedPrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs index 83318fd1e6..5cfd2191b3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs @@ -39,8 +39,20 @@ public class OrganizationUpdateCommand( var originalBillingEmail = organization.BillingEmail; // Apply updates to organization - organization.UpdateDetails(request); - organization.BackfillPublicPrivateKeys(request); + // These values may or may not be sent by the client depending on the operation being performed. + // Skip any values not provided. + if (request.Name is not null) + { + organization.Name = request.Name; + } + + if (request.BillingEmail is not null) + { + organization.BillingEmail = request.BillingEmail.ToLowerInvariant().Trim(); + } + + organization.BackfillPublicPrivateKeys(request.Keys); + await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); // Update billing information in Stripe if required @@ -56,7 +68,7 @@ public class OrganizationUpdateCommand( ///
    private async Task UpdateSelfHostedAsync(Organization organization, OrganizationUpdateRequest request) { - organization.BackfillPublicPrivateKeys(request); + organization.BackfillPublicPrivateKeys(request.Keys); await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); return organization; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs deleted file mode 100644 index e90c39bc54..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Bit.Core.AdminConsole.Entities; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; - -public static class OrganizationUpdateExtensions -{ - /// - /// Updates the organization name and/or billing email. - /// Any null property on the request object will be skipped. - /// - public static void UpdateDetails(this Organization organization, OrganizationUpdateRequest request) - { - // These values may or may not be sent by the client depending on the operation being performed. - // Skip any values not provided. - if (request.Name is not null) - { - organization.Name = request.Name; - } - - if (request.BillingEmail is not null) - { - organization.BillingEmail = request.BillingEmail.ToLowerInvariant().Trim(); - } - } - - /// - /// Updates the organization public and private keys if provided and not already set. - /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft - /// migration that will silently migrate organizations when they change their details. - /// - public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationUpdateRequest request) - { - if (!string.IsNullOrWhiteSpace(request.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) - { - organization.PublicKey = request.PublicKey; - } - - if (!string.IsNullOrWhiteSpace(request.EncryptedPrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) - { - organization.PrivateKey = request.EncryptedPrivateKey; - } - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs index 21d4948678..4695ee0ba7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; /// /// Request model for updating the name, billing email, and/or public-private keys for an organization (legacy migration code). @@ -22,12 +24,7 @@ public record OrganizationUpdateRequest public string? BillingEmail { get; init; } /// - /// The organization's public key to set (optional, only set if not already present on the organization). + /// The organization's public/private key pair to set (optional, only set if not already present on the organization). /// - public string? PublicKey { get; init; } - - /// - /// The organization's encrypted private key to set (optional, only set if not already present on the organization). - /// - public string? EncryptedPrivateKey { get; init; } + public PublicKeyEncryptionKeyPairData? Keys { get; init; } } diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index 89b9a5e6f2..d165a96d0a 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -2,6 +2,7 @@ #nullable disable using Bit.Core.Billing.Enums; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Core.Models.Business; @@ -13,8 +14,7 @@ public class OrganizationUpgrade public short AdditionalStorageGb { get; set; } public bool PremiumAccessAddon { get; set; } public TaxInfo TaxInfo { get; set; } - public string PublicKey { get; set; } - public string PrivateKey { get; set; } + public PublicKeyEncryptionKeyPairData Keys { get; set; } public int? AdditionalSmSeats { get; set; } public int? AdditionalServiceAccounts { get; set; } public bool UseSecretsManager { get; set; } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 092ee0f46e..4ad63bd8d7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -256,27 +257,20 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand organization.SelfHost = newPlan.HasSelfHost; organization.UsePolicies = newPlan.HasPolicies; organization.MaxStorageGb = (short)(newPlan.PasswordManager.BaseStorageGb + upgrade.AdditionalStorageGb); - organization.UseGroups = newPlan.HasGroups; - organization.UseDirectory = newPlan.HasDirectory; - organization.UseEvents = newPlan.HasEvents; - organization.UseTotp = newPlan.HasTotp; - organization.Use2fa = newPlan.Has2fa; - organization.UseApi = newPlan.HasApi; organization.UseSso = newPlan.HasSso; organization.UseOrganizationDomains = newPlan.HasOrganizationDomains; organization.UseKeyConnector = newPlan.HasKeyConnector ? organization.UseKeyConnector : false; organization.UseScim = newPlan.HasScim; organization.UseResetPassword = newPlan.HasResetPassword; - organization.SelfHost = newPlan.HasSelfHost; organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon; organization.UseCustomPermissions = newPlan.HasCustomPermissions; organization.Plan = newPlan.Name; organization.Enabled = success; - organization.PublicKey = upgrade.PublicKey; - organization.PrivateKey = upgrade.PrivateKey; organization.UsePasswordManager = true; organization.UseSecretsManager = upgrade.UseSecretsManager; + organization.BackfillPublicPrivateKeys(upgrade.Keys); + if (upgrade.UseSecretsManager) { organization.SmSeats = newPlan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault(); diff --git a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index c7c749effd..259797dfb3 100644 --- a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -66,8 +66,8 @@ public class ProviderClientsControllerTests signup.Plan == requestBody.PlanType && signup.AdditionalSeats == requestBody.Seats && signup.OwnerKey == requestBody.Key && - signup.PublicKey == requestBody.KeyPair.PublicKey && - signup.PrivateKey == requestBody.KeyPair.EncryptedPrivateKey && + signup.Keys.PublicKey == requestBody.KeyPair.PublicKey && + signup.Keys.WrappedPrivateKey == requestBody.KeyPair.EncryptedPrivateKey && signup.CollectionName == requestBody.CollectionName), requestBody.OwnerEmail, user) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs index d547d80aed..997076e7ef 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -162,8 +163,9 @@ public class OrganizationUpdateCommandTests OrganizationId = organizationId, Name = organization.Name, BillingEmail = organization.BillingEmail, - PublicKey = publicKey, - EncryptedPrivateKey = encryptedPrivateKey + Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: encryptedPrivateKey, + publicKey: publicKey) }; // Act @@ -207,8 +209,9 @@ public class OrganizationUpdateCommandTests OrganizationId = organizationId, Name = organization.Name, BillingEmail = organization.BillingEmail, - PublicKey = newPublicKey, - EncryptedPrivateKey = newEncryptedPrivateKey + Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: newEncryptedPrivateKey, + publicKey: newPublicKey) }; // Act @@ -394,8 +397,9 @@ public class OrganizationUpdateCommandTests OrganizationId = organizationId, Name = newName, // Should be ignored BillingEmail = newBillingEmail, // Should be ignored - PublicKey = publicKey, - EncryptedPrivateKey = encryptedPrivateKey + Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: encryptedPrivateKey, + publicKey: publicKey) }; // Act diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 8a00604bb0..223047ee07 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -242,4 +243,134 @@ public class UpgradeOrganizationPlanCommandTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default); } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_WhenOrganizationIsMissingPublicAndPrivateKeys_Backfills( + Organization organization, + OrganizationUpgrade upgrade, + string newPublicKey, + string newPrivateKey, + SutProvider sutProvider) + { + organization.PublicKey = null; + organization.PrivateKey = null; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: newPrivateKey, + publicKey: newPublicKey); + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Equal(newPublicKey, organization.PublicKey); + Assert.Equal(newPrivateKey, organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull( + Organization organization, + OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + // Arrange + const string existingPublicKey = "existing-public-key"; + const string existingPrivateKey = "existing-private-key"; + + organization.PublicKey = existingPublicKey; + organization.PrivateKey = existingPrivateKey; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = null; + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Equal(existingPublicKey, organization.PublicKey); + Assert.Equal(existingPrivateKey, organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys( + Organization organization, + OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + // Arrange + const string existingPublicKey = "existing-public-key"; + const string existingPrivateKey = "existing-private-key"; + const string newPublicKey = "new-public-key"; + const string newPrivateKey = "new-private-key"; + + organization.PublicKey = existingPublicKey; + organization.PrivateKey = existingPrivateKey; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: newPrivateKey, + publicKey: newPublicKey); + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Equal(existingPublicKey, organization.PublicKey); + Assert.Equal(existingPrivateKey, organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } } From 0cfb68336b18d201bd160e59dc93a9765147a7e6 Mon Sep 17 00:00:00 2001 From: Derek Nance Date: Fri, 26 Dec 2025 16:44:34 -0600 Subject: [PATCH 34/68] [PM-28025] Revert "chore(feature-flag): [PM-19665] Remove web-push feature flag" (#6779) This reverts commit 1c60b805bf80c190332f954e0922d7544eb77284. --- src/Api/Models/Response/ConfigResponseModel.cs | 8 +++++--- src/Core/Constants.cs | 1 + .../Factories/WebApplicationFactoryBase.cs | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Api/Models/Response/ConfigResponseModel.cs b/src/Api/Models/Response/ConfigResponseModel.cs index 20bc3f9e10..d748254206 100644 --- a/src/Api/Models/Response/ConfigResponseModel.cs +++ b/src/Api/Models/Response/ConfigResponseModel.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Services; @@ -45,7 +46,8 @@ public class ConfigResponseModel : ResponseModel Sso = globalSettings.BaseServiceUri.Sso }; FeatureStates = featureService.GetAll(); - Push = PushSettings.Build(globalSettings); + var webPushEnabled = FeatureStates.TryGetValue(FeatureFlagKeys.WebPush, out var webPushEnabledValue) ? (bool)webPushEnabledValue : false; + Push = PushSettings.Build(webPushEnabled, globalSettings); Settings = new ServerSettingsResponseModel { DisableUserRegistration = globalSettings.DisableUserRegistration @@ -74,9 +76,9 @@ public class PushSettings public PushTechnologyType PushTechnology { get; private init; } public string VapidPublicKey { get; private init; } - public static PushSettings Build(IGlobalSettings globalSettings) + public static PushSettings Build(bool webPushEnabled, IGlobalSettings globalSettings) { - var vapidPublicKey = globalSettings.WebPush.VapidPublicKey; + var vapidPublicKey = webPushEnabled ? globalSettings.WebPush.VapidPublicKey : null; var pushTechnology = vapidPublicKey != null ? PushTechnologyType.WebPush : PushTechnologyType.SignalR; return new() { diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index eb42754475..c3c009a2d5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -228,6 +228,7 @@ public static class FeatureFlagKeys public const string CxpExportMobile = "cxp-export-mobile"; /* Platform Team */ + public const string WebPush = "web-push"; public const string IpcChannelFramework = "ipc-channel-framework"; public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked"; public const string PushNotificationsWhenInactive = "pm-25130-receive-push-notifications-for-inactive-users"; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index a9b3e6f7f0..4b42f575a1 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -154,6 +154,7 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // Web push notifications { "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" }, + { "globalSettings:launchDarkly:flagValues:web-push", "true" }, }; // Some database drivers modify the connection string From bf5cacdfc56451ef14d9aadbbd6cbac76b7b37b8 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Sun, 28 Dec 2025 13:26:07 -0500 Subject: [PATCH 35/68] chore(dependencies): Ignore minor updates for Platform deps --- .github/renovate.json5 | 115 ++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 074b4dde2b..2ca17c5b5f 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -10,42 +10,7 @@ "nuget", ], packageRules: [ - { - groupName: "cargo minor", - matchManagers: ["cargo"], - matchUpdateTypes: ["minor"], - }, - { - groupName: "dockerfile minor", - matchManagers: ["dockerfile"], - matchUpdateTypes: ["minor"], - }, - { - groupName: "docker-compose minor", - matchManagers: ["docker-compose"], - matchUpdateTypes: ["minor"], - }, - { - groupName: "github-action minor", - matchManagers: ["github-actions"], - matchUpdateTypes: ["minor"], - addLabels: ["hold"], - }, - { - // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates. - // This overrides the default that ignores patch updates for nuget dependencies. - matchPackageNames: [ - "/^Microsoft\\.Extensions\\./", - "/^Microsoft\\.AspNetCore\\./", - ], - matchUpdateTypes: ["patch"], - dependencyDashboardApproval: false, - }, - { - matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"], - groupName: "sdk-internal", - dependencyDashboardApproval: true - }, + // ==================== Team Ownership Rules ==================== { matchManagers: ["dockerfile", "docker-compose"], commitMessagePrefix: "[deps] BRE:", @@ -101,11 +66,6 @@ commitMessagePrefix: "[deps] Billing:", reviewers: ["team:team-billing-dev"], }, - { - matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"], - groupName: "EntityFrameworkCore", - description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", - }, { matchPackageNames: [ "Dapper", @@ -162,6 +122,12 @@ commitMessagePrefix: "[deps] Platform:", reviewers: ["team:team-platform-dev"], }, + { + matchUpdateTypes: ["lockFileMaintenance"], + description: "Platform owns lock file maintenance", + commitMessagePrefix: "[deps] Platform:", + reviewers: ["team:team-platform-dev"], + }, { matchPackageNames: [ "AutoMapper.Extensions.Microsoft.DependencyInjection", @@ -191,6 +157,73 @@ commitMessagePrefix: "[deps] Vault:", reviewers: ["team:team-vault-dev"], }, + + // ==================== Grouping Rules ==================== + // These come after any specific team assignment rules to ensure + // that grouping is not overridden by subsequent rule definitions. + { + groupName: "cargo minor", + matchManagers: ["cargo"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "dockerfile minor", + matchManagers: ["dockerfile"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "docker-compose minor", + matchManagers: ["docker-compose"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "github-action minor", + matchManagers: ["github-actions"], + matchUpdateTypes: ["minor"], + addLabels: ["hold"], + }, + { + matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"], + groupName: "EntityFrameworkCore", + description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"], + groupName: "sdk-internal", + dependencyDashboardApproval: true + }, + + // ==================== Dashboard Rules ==================== + { + // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates. + // This overrides the default that ignores patch updates for nuget dependencies. + matchPackageNames: [ + "/^Microsoft\\.Extensions\\./", + "/^Microsoft\\.AspNetCore\\./", + ], + matchUpdateTypes: ["patch"], + dependencyDashboardApproval: false, + }, + { + // For the Platform-owned dependencies below, we have decided we will only be creating PRs + // for major updates, and sending minor (as well as patch, inherited from base config) to the dashboard. + // This rule comes AFTER grouping rules so that groups are respected while still + // sending minor/patch updates to the dependency dashboard for approval. + matchPackageNames: [ + "AspNetCoreRateLimit", + "AspNetCoreRateLimit.Redis", + "Azure.Data.Tables", + "Azure.Extensions.AspNetCore.DataProtection.Blobs", + "Azure.Messaging.EventGrid", + "Azure.Messaging.ServiceBus", + "Azure.Storage.Blobs", + "Azure.Storage.Queues", + "LaunchDarkly.ServerSdk", + "Quartz", + ], + matchUpdateTypes: ["minor"], + dependencyDashboardApproval: true, + }, ], ignoreDeps: ["dotnet-sdk"], } From 8a79bfa6737915db46f9c3d5c3745bd374abaf58 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:59:46 +0100 Subject: [PATCH 36/68] [deps]: Update actions/upload-artifact action to v6 (#6766) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 16 ++++++++-------- .github/workflows/test-database.yml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e7b95cc75..694e9048a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,7 +160,7 @@ jobs: ls -atlh ../../../ - name: Upload project artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: ${{ matrix.dotnet }} with: name: ${{ matrix.project_name }}.zip @@ -356,7 +356,7 @@ jobs: if: | github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: docker-stub-US.zip path: docker-stub-US.zip @@ -366,7 +366,7 @@ jobs: if: | github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: docker-stub-EU.zip path: docker-stub-EU.zip @@ -378,21 +378,21 @@ jobs: pwsh ./generate_openapi_files.ps1 - name: Upload Public API Swagger artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: swagger.json path: api.public.json if-no-files-found: error - name: Upload Internal API Swagger artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: internal.json path: api.json if-no-files-found: error - name: Upload Identity Swagger artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: identity.json path: identity.json @@ -438,7 +438,7 @@ jobs: - name: Upload project artifact for Windows if: ${{ contains(matrix.target, 'win') == true }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe @@ -446,7 +446,7 @@ jobs: - name: Upload project artifact if: ${{ contains(matrix.target, 'win') == false }} - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: MsSqlMigratorUtility-${{ matrix.target }} path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 54ecd7962f..0fbdb5d069 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -197,7 +197,7 @@ jobs: shell: pwsh - name: Upload DACPAC - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: sql.dacpac path: Sql.dacpac @@ -223,7 +223,7 @@ jobs: shell: pwsh - name: Report validation results - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: report.xml path: | From 0f104af9210507d7d17bd4d73fb6059b4f332ca5 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 29 Dec 2025 10:00:05 -0500 Subject: [PATCH 37/68] chore(deps): Move Cosmos cache to Auth ownership --- .github/renovate.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 2ca17c5b5f..77539ef839 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -33,6 +33,7 @@ "Fido2.AspNet", "Duende.IdentityServer", "Microsoft.AspNetCore.Authentication.JwtBearer", + "Microsoft.Extensions.Caching.Cosmos", "Microsoft.Extensions.Identity.Stores", "Otp.NET", "Sustainsys.Saml2.AspNetCore2", @@ -113,7 +114,6 @@ "Microsoft.Extensions.DependencyInjection", "Microsoft.Extensions.Logging", "Microsoft.Extensions.Logging.Console", - "Microsoft.Extensions.Caching.Cosmos", "Microsoft.Extensions.Caching.SqlServer", "Microsoft.Extensions.Caching.StackExchangeRedis", "Quartz", From 2dc4e9a420120aec0b0edbe2ef571304b0383068 Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Mon, 29 Dec 2025 11:55:05 -0500 Subject: [PATCH 38/68] feat(2fa-webauthn) [PM-20109]: Increase 2FA WebAuthn Security Key Limit (#6751) * feat(global-settings) [PM-20109]: Add WebAuthN global settings. * feat(webauthn) [PM-20109]: Update maximum allowed WebAuthN credentials to use new settings. * test(webauthn) [PM-20109]: Update command tests to use global configs. * feat(global-settings) [PM-20109]: Set defaults for maximum allowed credentials. * feat(two-factor-request-model) [PM-20109]: Remove hard-coded 5 limit on ID validation. * Revert "test(webauthn) [PM-20109]: Update command tests to use global configs." This reverts commit ba9f0d5fb6cfc8ad1bb8812d150172df6a617a3f. * Revert "feat(webauthn) [PM-20109]: Update maximum allowed WebAuthN credentials to use new settings." This reverts commit d2faef0c1366b420d5ef04038c4fd05f391f73e2. * feat(global-settings) [PM-20109]: Add WebAuthNSettings to interface for User Service consumption. * feat(user-service) [PM-20109]: Add boundary and persistence-time validation for maximum allowed WebAuthN 2FA credentials. * test(user-service) [PM-20109]: Update tests for WebAuthN limit scenarios. * refactor(user-service) [PM-20109]: Typo in variable name. * refactor(user-service) [PM-20109]: Remove unnecessary pending check. * refactor(user-service) [PM-20109]: Pending check is necessary. * refactor(webauthn) [PM-20109]: Re-spell WebAuthN => WebAuthn. * refactor(user-service) [PM-20109]: Re-format pending checks for consistency. * refactor(user-service) [PM-20109]: Fix type spelling in comments. * test(user-service) [PM-20109]: Combine premium and non-premium test cases with AutoData. * refactor(user-service) [PM-20109]: Swap HasPremiumAccessQuery in for CanAccessPremium. * refactor(user-service) [PM-20109]: Convert limit check to positive, edit comments. --- .../Models/Request/TwoFactorRequestModels.cs | 2 +- .../Services/Implementations/UserService.cs | 28 +++ src/Core/Settings/GlobalSettings.cs | 7 + src/Core/Settings/IGlobalSettings.cs | 1 + test/Core.Test/Services/UserServiceTests.cs | 207 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index 79df29c928..6173de81d9 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -273,7 +273,7 @@ public class TwoFactorWebAuthnDeleteRequestModel : SecretVerificationRequestMode yield return validationResult; } - if (!Id.HasValue || Id < 0 || Id > 5) + if (!Id.HasValue) { yield return new ValidationResult("Invalid Key Id", new string[] { nameof(Id) }); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 4e65e88767..498721238b 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -344,6 +344,12 @@ public class UserService : UserManager, IUserService await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint); } + /// + /// Initiates WebAuthn 2FA credential registration and generates a challenge for adding a new security key. + /// + /// The current user. + /// + /// Maximum allowed number of credentials already registered. public async Task StartWebAuthnRegistrationAsync(User user) { var providers = user.GetTwoFactorProviders(); @@ -364,6 +370,17 @@ public class UserService : UserManager, IUserService provider.MetaData = new Dictionary(); } + // Boundary validation to provide a better UX. There is also second-level enforcement at persistence time. + var maximumAllowedCredentialCount = await _hasPremiumAccessQuery.HasPremiumAccessAsync(user.Id) + ? _globalSettings.WebAuthn.PremiumMaximumAllowedCredentials + : _globalSettings.WebAuthn.NonPremiumMaximumAllowedCredentials; + // Count only saved credentials ("Key{id}") toward the limit. + if (provider.MetaData.Count(k => k.Key.StartsWith("Key")) >= + maximumAllowedCredentialCount) + { + throw new BadRequestException("Maximum allowed WebAuthn credential count exceeded."); + } + var fidoUser = new Fido2User { DisplayName = user.Name, @@ -402,6 +419,17 @@ public class UserService : UserManager, IUserService return false; } + // Persistence-time validation for comprehensive enforcement. There is also boundary validation for best-possible UX. + var maximumAllowedCredentialCount = await _hasPremiumAccessQuery.HasPremiumAccessAsync(user.Id) + ? _globalSettings.WebAuthn.PremiumMaximumAllowedCredentials + : _globalSettings.WebAuthn.NonPremiumMaximumAllowedCredentials; + // Count only saved credentials ("Key{id}") toward the limit. + if (provider.MetaData.Count(k => k.Key.StartsWith("Key")) >= + maximumAllowedCredentialCount) + { + throw new BadRequestException("Maximum allowed WebAuthn credential count exceeded."); + } + var options = CredentialCreateOptions.FromJson((string)pendingValue); // Callback to ensure credential ID is unique. Always return true since we don't care if another diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index f030c73809..60a1fda19f 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -66,6 +66,7 @@ public class GlobalSettings : IGlobalSettings public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new(); public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings(); + public virtual WebAuthnSettings WebAuthn { get; set; } = new WebAuthnSettings(); public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings(); public virtual ImportCiphersLimitationSettings ImportCiphersLimitation { get; set; } = new ImportCiphersLimitationSettings(); public virtual BitPaySettings BitPay { get; set; } = new BitPaySettings(); @@ -613,6 +614,12 @@ public class GlobalSettings : IGlobalSettings public string AKey { get; set; } } + public class WebAuthnSettings + { + public int PremiumMaximumAllowedCredentials { get; set; } = 10; + public int NonPremiumMaximumAllowedCredentials { get; set; } = 5; + } + public class BraintreeSettings { public bool Production { get; set; } diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index 06dece3394..c316836d09 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -28,4 +28,5 @@ public interface IGlobalSettings string DevelopmentDirectory { get; set; } IWebPushSettings WebPush { get; set; } GlobalSettings.EventLoggingSettings EventLogging { get; set; } + GlobalSettings.WebAuthnSettings WebAuthn { get; set; } } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 9d83674f44..073379820e 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -25,11 +25,15 @@ using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Fido2NetLib; +using Fido2NetLib.Objects; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; using NSubstitute; using Xunit; +using static Fido2NetLib.Fido2; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.Services; @@ -594,6 +598,209 @@ public class UserServiceTests user.MasterPassword = null; } } + + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds( + bool hasPremium, SutProvider sutProvider, User user) + { + // Arrange - Non-premium user with 4 credentials (below limit of 5) + SetupWebAuthnProvider(user, credentialCount: 4); + + sutProvider.GetDependency().WebAuthn = new GlobalSettings.WebAuthnSettings + { + PremiumMaximumAllowedCredentials = 10, + NonPremiumMaximumAllowedCredentials = 5 + }; + + user.Premium = hasPremium; + user.Id = Guid.NewGuid(); + user.Email = "test@example.com"; + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns(new List()); + + var mockFido2 = sutProvider.GetDependency(); + mockFido2.RequestNewCredential( + Arg.Any(), + Arg.Any>(), + Arg.Any(), + Arg.Any()) + .Returns(new CredentialCreateOptions + { + Challenge = new byte[] { 1, 2, 3 }, + Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""), + User = new Fido2User + { + Id = user.Id.ToByteArray(), + Name = user.Email, + DisplayName = user.Name + }, + PubKeyCredParams = new List() + }); + + // Act + var result = await sutProvider.Sut.StartWebAuthnRegistrationAsync(user); + + // Assert + Assert.NotNull(result); + await sutProvider.GetDependency().Received(1).ReplaceAsync(user); + } + + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium, + SutProvider sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse) + { + // Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit) + SetupWebAuthnProviderWithPending(user, credentialCount: 10); + + sutProvider.GetDependency().WebAuthn = new GlobalSettings.WebAuthnSettings + { + PremiumMaximumAllowedCredentials = 10, + NonPremiumMaximumAllowedCredentials = 5 + }; + + user.Premium = hasPremium; + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 11, "NewKey", deviceResponse)); + + Assert.Equal("Maximum allowed WebAuthn credential count exceeded.", exception.Message); + } + + [Theory] + [BitAutoData(true)] + [BitAutoData(false)] + public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium, + SutProvider sutProvider, User user, AuthenticatorAttestationRawResponse deviceResponse) + { + // Arrange - User has 4 credentials (below limit of 5) + SetupWebAuthnProviderWithPending(user, credentialCount: 4); + + sutProvider.GetDependency().WebAuthn = new GlobalSettings.WebAuthnSettings + { + PremiumMaximumAllowedCredentials = 10, + NonPremiumMaximumAllowedCredentials = 5 + }; + + user.Premium = hasPremium; + user.Id = Guid.NewGuid(); + + sutProvider.GetDependency() + .GetManyByUserAsync(user.Id) + .Returns(new List()); + + var mockFido2 = sutProvider.GetDependency(); + mockFido2.MakeNewCredentialAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new CredentialMakeResult("ok", "", new AttestationVerificationSuccess + { + Aaguid = Guid.NewGuid(), + Counter = 0, + CredentialId = new byte[] { 1, 2, 3 }, + CredType = "public-key", + PublicKey = new byte[] { 4, 5, 6 }, + Status = "ok", + User = new Fido2User + { + Id = user.Id.ToByteArray(), + Name = user.Email ?? "test@example.com", + DisplayName = user.Name ?? "Test User" + } + })); + + // Act + var result = await sutProvider.Sut.CompleteWebAuthRegistrationAsync(user, 5, "NewKey", deviceResponse); + + // Assert + Assert.True(result); + await sutProvider.GetDependency().Received(1).ReplaceAsync(user); + } + + private static void SetupWebAuthnProvider(User user, int credentialCount) + { + var providers = new Dictionary(); + var metadata = new Dictionary(); + + // Add credentials as Key1, Key2, Key3, etc. + for (int i = 1; i <= credentialCount; i++) + { + metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData + { + Name = $"Key {i}", + Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }), + PublicKey = new byte[] { (byte)i }, + UserHandle = new byte[] { (byte)i }, + SignatureCounter = 0, + CredType = "public-key", + RegDate = DateTime.UtcNow, + AaGuid = Guid.NewGuid() + }; + } + + providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider + { + Enabled = true, + MetaData = metadata + }; + + user.SetTwoFactorProviders(providers); + } + + private static void SetupWebAuthnProviderWithPending(User user, int credentialCount) + { + var providers = new Dictionary(); + var metadata = new Dictionary(); + + // Add existing credentials + for (int i = 1; i <= credentialCount; i++) + { + metadata[$"Key{i}"] = new TwoFactorProvider.WebAuthnData + { + Name = $"Key {i}", + Descriptor = new PublicKeyCredentialDescriptor(new byte[] { (byte)i }), + PublicKey = new byte[] { (byte)i }, + UserHandle = new byte[] { (byte)i }, + SignatureCounter = 0, + CredType = "public-key", + RegDate = DateTime.UtcNow, + AaGuid = Guid.NewGuid() + }; + } + + // Add pending registration + var pendingOptions = new CredentialCreateOptions + { + Challenge = new byte[] { 1, 2, 3 }, + Rp = new PublicKeyCredentialRpEntity("example.com", "example.com", ""), + User = new Fido2User + { + Id = user.Id.ToByteArray(), + Name = user.Email ?? "test@example.com", + DisplayName = user.Name ?? "Test User" + }, + PubKeyCredParams = new List() + }; + metadata["pending"] = pendingOptions.ToJson(); + + providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider + { + Enabled = true, + MetaData = metadata + }; + + user.SetTwoFactorProviders(providers); + } } public static class UserServiceSutProviderExtensions From 3b5bb76800e33222cb7581ccb9a4c36056ccd304 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 29 Dec 2025 09:30:22 -0800 Subject: [PATCH 39/68] [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 --- ...anizationDataOwnershipPolicyRequirement.cs | 11 ++ .../Services/Implementations/CipherService.cs | 38 ++++- .../Vault/Services/CipherServiceTests.cs | 135 ++++++++++++++++++ 3 files changed, 177 insertions(+), 7 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs index 28d6614dcb..c9653053ea 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs @@ -72,6 +72,17 @@ public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement { return _policyDetails.Any(p => p.OrganizationId == organizationId); } + + /// + /// 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. + /// + public bool IgnoreStorageLimitsOnMigration(Guid organizationId) + { + return _policyDetails.Any(p => p.OrganizationId == organizationId && + p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed); + } } public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection) diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index bb752b471f..797b595cbe 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -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); } + /// + /// Checks if the storage limit for the org should be ignored due to the Organization Data Ownership Policy + /// + private async Task IgnoreStorageLimitsOnMigrationAsync(Guid userId, Organization organization) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems)) + { + return false; + } + + if (!organization.UsePolicies) + { + return false; + } + + var requirement = await _policyRequirementQuery.GetAsync(userId); + + return requirement.IgnoreStorageLimitsOnMigration(organization.Id); + } + private async Task ValidateViewPasswordUserAsync(Cipher cipher) { if (cipher.Data == null || !cipher.OrganizationId.HasValue) diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index fc84651951..058c6f68ab 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -1190,6 +1190,7 @@ public class CipherServiceTests sutProvider.GetDependency().GetByIdAsync(organizationId) .Returns(new Organization { + UsePolicies = true, PlanType = PlanType.EnterpriseAnnually, MaxStorageGb = 100 }); @@ -1206,6 +1207,140 @@ public class CipherServiceTests Arg.Is>(arg => !arg.Except(ciphers).Any())); } + [Theory, BitAutoData] + public async Task ShareManyAsync_StorageLimitBypass_Passes(SutProvider sutProvider, + IEnumerable ciphers, Guid organizationId, List collectionIds) + { + sutProvider.GetDependency().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().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true); + + sutProvider.GetDependency() + .GetAsync(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().Received(1).UpdateCiphersAsync(sharingUserId, + Arg.Is>(arg => !arg.Except(ciphers).Any())); + } + + [Theory, BitAutoData] + public async Task ShareManyAsync_StorageLimit_Enforced(SutProvider sutProvider, + IEnumerable ciphers, Guid organizationId, List collectionIds) + { + sutProvider.GetDependency().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() + .GetAsync(sharingUserId) + .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [])); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId) + ); + Assert.Contains("Not enough storage available for this organization.", exception.Message); + await sutProvider.GetDependency().DidNotReceive().UpdateCiphersAsync(sharingUserId, + Arg.Is>(arg => !arg.Except(ciphers).Any())); + } + + [Theory, BitAutoData] + public async Task ShareManyAsync_StorageLimit_Enforced_WhenFeatureFlagDisabled(SutProvider sutProvider, + IEnumerable ciphers, Guid organizationId, List collectionIds) + { + sutProvider.GetDependency().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().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(false); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId) + ); + Assert.Contains("Not enough storage available for this organization.", exception.Message); + await sutProvider.GetDependency().DidNotReceive().UpdateCiphersAsync(sharingUserId, + Arg.Is>(arg => !arg.Except(ciphers).Any())); + } + + [Theory, BitAutoData] + public async Task ShareManyAsync_StorageLimit_Enforced_WhenUsePoliciesDisabled(SutProvider sutProvider, + IEnumerable ciphers, Guid organizationId, List collectionIds) + { + sutProvider.GetDependency().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().IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems).Returns(true); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId) + ); + Assert.Contains("Not enough storage available for this organization.", exception.Message); + await sutProvider.GetDependency().DidNotReceive().UpdateCiphersAsync(sharingUserId, + Arg.Is>(arg => !arg.Except(ciphers).Any())); + } + private class SaveDetailsAsyncDependencies { public CipherDetails CipherDetails { get; set; } From 34b4dc3985dae407db03437a65c2a8a1851d157a Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Mon, 29 Dec 2025 13:30:57 -0500 Subject: [PATCH 40/68] [PM-29650] retain item archive date on softdelete (#6771) --- src/Core/Vault/Services/Implementations/CipherService.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 797b595cbe..fa2cfbb209 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -719,13 +719,6 @@ public class CipherService : ICipherService cipherDetails.DeletedDate = cipherDetails.RevisionDate = DateTime.UtcNow; - if (cipherDetails.ArchivedDate.HasValue) - { - // If the cipher was archived, clear the archived date when soft deleting - // If a user were to restore an archived cipher, it should go back to the vault not the archive vault - cipherDetails.ArchivedDate = null; - } - await _securityTaskRepository.MarkAsCompleteByCipherIds([cipherDetails.Id]); await _cipherRepository.UpsertAsync(cipherDetails); await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted); From 9a340c0fdd1fcaf2859c52e2d1e9012ec9da8e9c Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Tue, 30 Dec 2025 07:31:26 -0600 Subject: [PATCH 41/68] Allow mobile clients to create passkeys (#6383) [PM-26177] * Allow mobile clients to create vault passkeys * Document uses for authorization policies --- .../Auth/Controllers/WebAuthnController.cs | 7 +- src/Core/Auth/Identity/Policies.cs | 96 +++++++++++++++++-- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index 60b8621c5e..833087e99c 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -21,7 +21,6 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("webauthn")] -[Authorize(Policies.Web)] public class WebAuthnController : Controller { private readonly IUserService _userService; @@ -62,6 +61,7 @@ public class WebAuthnController : Controller _featureService = featureService; } + [Authorize(Policies.Web)] [HttpGet("")] public async Task> Get() { @@ -71,6 +71,7 @@ public class WebAuthnController : Controller return new ListResponseModel(credentials.Select(c => new WebAuthnCredentialResponseModel(c))); } + [Authorize(Policies.Application)] [HttpPost("attestation-options")] public async Task AttestationOptions([FromBody] SecretVerificationRequestModel model) { @@ -88,6 +89,7 @@ public class WebAuthnController : Controller }; } + [Authorize(Policies.Web)] [HttpPost("assertion-options")] public async Task AssertionOptions([FromBody] SecretVerificationRequestModel model) { @@ -104,6 +106,7 @@ public class WebAuthnController : Controller }; } + [Authorize(Policies.Application)] [HttpPost("")] public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model) { @@ -149,6 +152,7 @@ public class WebAuthnController : Controller } } + [Authorize(Policies.Application)] [HttpPut()] public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model) { @@ -172,6 +176,7 @@ public class WebAuthnController : Controller await _credentialRepository.UpdateAsync(credential); } + [Authorize(Policies.Web)] [HttpPost("{id}/delete")] public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model) { diff --git a/src/Core/Auth/Identity/Policies.cs b/src/Core/Auth/Identity/Policies.cs index b2d94b0a6e..698a890006 100644 --- a/src/Core/Auth/Identity/Policies.cs +++ b/src/Core/Auth/Identity/Policies.cs @@ -5,12 +5,94 @@ public static class Policies /// /// Policy for managing access to the Send feature. /// - public const string Send = "Send"; // [Authorize(Policy = Policies.Send)] - public const string Application = "Application"; // [Authorize(Policy = Policies.Application)] - public const string Web = "Web"; // [Authorize(Policy = Policies.Web)] - public const string Push = "Push"; // [Authorize(Policy = Policies.Push)] + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Send)] + /// + /// + /// + public const string Send = "Send"; + + /// + /// Policy to manage access to general API endpoints. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Application)] + /// + /// + /// + public const string Application = "Application"; + + /// + /// Policy to manage access to API endpoints intended for use by the Web Vault and browser extension only. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Web)] + /// + /// + /// + public const string Web = "Web"; + + /// + /// Policy to restrict access to API endpoints for the Push feature. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Push)] + /// + /// + /// + public const string Push = "Push"; + + // TODO: This is unused public const string Licensing = "Licensing"; // [Authorize(Policy = Policies.Licensing)] - public const string Organization = "Organization"; // [Authorize(Policy = Policies.Organization)] - public const string Installation = "Installation"; // [Authorize(Policy = Policies.Installation)] - public const string Secrets = "Secrets"; // [Authorize(Policy = Policies.Secrets)] + + /// + /// Policy to restrict access to API endpoints related to the Organization features. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Licensing)] + /// + /// + /// + public const string Organization = "Organization"; + + /// + /// Policy to restrict access to API endpoints related to the setting up new installations. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Installation)] + /// + /// + /// + public const string Installation = "Installation"; + + /// + /// Policy to restrict access to API endpoints for Secrets Manager features. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Secrets)] + /// + /// + /// + public const string Secrets = "Secrets"; } From 86a68ab6376783403df5be11dd03467e63d2decd Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:59:19 -0500 Subject: [PATCH 42/68] Move all event integration code to Dirt (#6757) * Move all event integration code to Dirt * Format to fix lint --- ...zationIntegrationConfigurationController.cs | 8 ++++---- .../OrganizationIntegrationController.cs | 8 ++++---- .../Controllers/SlackIntegrationController.cs | 14 +++++++------- .../Controllers/TeamsIntegrationController.cs | 14 +++++++------- ...tionIntegrationConfigurationRequestModel.cs | 5 ++--- .../OrganizationIntegrationRequestModel.cs} | 8 ++++---- ...ionIntegrationConfigurationResponseModel.cs | 4 ++-- .../OrganizationIntegrationResponseModel.cs | 8 ++++---- .../EventIntegrations/DatadogIntegration.cs | 3 --- .../Data/EventIntegrations/SlackIntegration.cs | 3 --- .../SlackIntegrationConfiguration.cs | 3 --- .../IIntegrationConfigurationDetailsCache.cs | 14 -------------- .../Entities/OrganizationIntegration.cs | 6 +++--- .../OrganizationIntegrationConfiguration.cs | 2 +- .../Enums/IntegrationType.cs | 2 +- .../Enums/OrganizationIntegrationStatus.cs | 2 +- ...tIntegrationsServiceCollectionExtensions.cs | 18 ++++++++++-------- ...anizationIntegrationConfigurationCommand.cs | 10 +++++----- ...anizationIntegrationConfigurationCommand.cs | 6 +++--- ...ganizationIntegrationConfigurationsQuery.cs | 8 ++++---- ...anizationIntegrationConfigurationCommand.cs | 4 ++-- ...anizationIntegrationConfigurationCommand.cs | 2 +- ...ganizationIntegrationConfigurationsQuery.cs | 4 ++-- ...anizationIntegrationConfigurationCommand.cs | 4 ++-- ...anizationIntegrationConfigurationCommand.cs | 10 +++++----- .../CreateOrganizationIntegrationCommand.cs | 8 ++++---- .../DeleteOrganizationIntegrationCommand.cs | 6 +++--- .../GetOrganizationIntegrationsQuery.cs | 8 ++++---- .../ICreateOrganizationIntegrationCommand.cs | 4 ++-- .../IDeleteOrganizationIntegrationCommand.cs | 2 +- .../IGetOrganizationIntegrationsQuery.cs | 4 ++-- .../IUpdateOrganizationIntegrationCommand.cs | 4 ++-- .../UpdateOrganizationIntegrationCommand.cs | 8 ++++---- .../EventIntegrations/README.md | 0 .../EventIntegrations/DatadogIntegration.cs | 3 +++ .../DatadogIntegrationConfigurationDetails.cs | 2 +- .../DatadogListenerConfiguration.cs | 4 ++-- .../Data/EventIntegrations/HecIntegration.cs | 2 +- .../HecListenerConfiguration.cs | 4 ++-- .../IEventListenerConfiguration.cs | 2 +- .../IIntegrationListenerConfiguration.cs | 4 ++-- .../EventIntegrations/IIntegrationMessage.cs | 4 ++-- .../IntegrationFailureCategory.cs | 2 +- .../IntegrationFilterGroup.cs | 2 +- .../IntegrationFilterOperation.cs | 2 +- .../EventIntegrations/IntegrationFilterRule.cs | 2 +- .../IntegrationHandlerResult.cs | 2 +- .../EventIntegrations/IntegrationMessage.cs | 4 ++-- .../EventIntegrations/IntegrationOAuthState.cs | 4 ++-- .../IntegrationTemplateContext.cs | 2 +- .../EventIntegrations/ListenerConfiguration.cs | 2 +- ...anizationIntegrationConfigurationDetails.cs | 5 ++--- .../RepositoryListenerConfiguration.cs | 2 +- .../Data/EventIntegrations/SlackIntegration.cs | 3 +++ .../SlackIntegrationConfiguration.cs | 3 +++ .../SlackIntegrationConfigurationDetails.cs | 2 +- .../SlackListenerConfiguration.cs | 4 ++-- .../Data/EventIntegrations/TeamsIntegration.cs | 4 ++-- .../TeamsIntegrationConfigurationDetails.cs | 2 +- .../TeamsListenerConfiguration.cs | 4 ++-- .../EventIntegrations/WebhookIntegration.cs | 2 +- .../WebhookIntegrationConfiguration.cs | 2 +- .../WebhookIntegrationConfigurationDetails.cs | 2 +- .../WebhookListenerConfiguration.cs | 4 ++-- .../Models/Data}/Slack/SlackApiResponse.cs | 2 +- .../Models/Data}/Teams/TeamsApiResponse.cs | 2 +- .../Data}/Teams/TeamsBotCredentialProvider.cs | 2 +- ...zationIntegrationConfigurationRepository.cs | 8 +++++--- .../IOrganizationIntegrationRepository.cs | 5 +++-- .../Services/IAzureServiceBusService.cs | 4 ++-- .../Services/IEventIntegrationPublisher.cs | 4 ++-- .../Services/IEventMessageHandler.cs | 2 +- .../Services/IIntegrationFilterService.cs | 4 ++-- .../Services/IIntegrationHandler.cs | 4 ++-- ...izationIntegrationConfigurationValidator.cs | 6 +++--- .../Services/IRabbitMqService.cs | 4 ++-- .../Services/ISlackService.cs | 5 +++-- .../Services/ITeamsService.cs | 5 +++-- .../AzureServiceBusEventListenerService.cs | 6 +++--- ...zureServiceBusIntegrationListenerService.cs | 6 +++--- .../Implementations}/AzureServiceBusService.cs | 6 +++--- .../AzureTableStorageEventHandler.cs | 7 +++---- .../DatadogIntegrationHandler.cs | 4 ++-- .../EventIntegrationEventWriteService.cs | 3 ++- .../EventIntegrationHandler.cs | 8 ++++---- .../EventLoggingListenerService.cs | 6 ++---- .../Implementations}/EventRepositoryHandler.cs | 3 ++- .../IntegrationFilterFactory.cs | 2 +- .../IntegrationFilterService.cs | 4 ++-- ...izationIntegrationConfigurationValidator.cs | 8 ++++---- .../RabbitMqEventListenerService.cs | 6 +++--- .../RabbitMqIntegrationListenerService.cs | 6 +++--- .../Implementations}/RabbitMqService.cs | 6 +++--- .../SlackIntegrationHandler.cs | 4 ++-- .../Services/Implementations}/SlackService.cs | 4 ++-- .../TeamsIntegrationHandler.cs | 4 ++-- .../Services/Implementations}/TeamsService.cs | 10 +++++----- .../WebhookIntegrationHandler.cs | 4 ++-- .../NoopImplementations/NoopSlackService.cs | 5 ++--- .../NoopImplementations/NoopTeamsService.cs | 5 ++--- .../EventIntegrationsCacheConstants.cs | 3 ++- .../DapperServiceCollectionExtensions.cs | 1 + ...zationIntegrationConfigurationRepository.cs | 9 +++++---- .../OrganizationIntegrationRepository.cs | 7 ++++--- ...tionConfigurationEntityTypeConfiguration.cs | 2 +- ...zationIntegrationEntityTypeConfiguration.cs | 2 +- .../Models/OrganizationIntegration.cs | 16 ---------------- .../OrganizationIntegrationConfiguration.cs | 16 ---------------- .../Dirt/Models/OrganizationIntegration.cs | 17 +++++++++++++++++ .../OrganizationIntegrationConfiguration.cs | 16 ++++++++++++++++ ...zationIntegrationConfigurationRepository.cs | 16 ++++++++-------- .../OrganizationIntegrationRepository.cs | 14 +++++++------- ...ntTypeOrganizationIdIntegrationTypeQuery.cs | 9 ++++++--- ...grationConfigurationDetailsReadManyQuery.cs | 8 ++++---- ...ReadManyByOrganizationIntegrationIdQuery.cs | 4 ++-- ...dByTeamsConfigurationTenantIdTeamIdQuery.cs | 6 +++--- ...IntegrationReadManyByOrganizationIdQuery.cs | 4 ++-- .../OrganizationIntegrationControllerTests.cs | 14 +++++++------- ...IntegrationsConfigurationControllerTests.cs | 12 ++++++------ .../SlackIntegrationControllerTests.cs | 14 +++++++------- .../TeamsIntegrationControllerTests.cs | 16 ++++++++-------- ...OrganizationIntegrationRequestModelTests.cs | 10 +++++----- ...rganizationIntegrationResponseModelTests.cs | 12 ++++++------ .../Services/IntegrationTypeTests.cs | 2 +- ...egrationServiceCollectionExtensionsTests.cs | 17 ++++++++++------- ...tionIntegrationConfigurationCommandTests.cs | 11 ++++++----- ...tionIntegrationConfigurationCommandTests.cs | 9 +++++---- ...ationIntegrationConfigurationsQueryTests.cs | 8 ++++---- ...tionIntegrationConfigurationCommandTests.cs | 11 ++++++----- ...reateOrganizationIntegrationCommandTests.cs | 10 +++++----- ...eleteOrganizationIntegrationCommandTests.cs | 10 +++++----- .../GetOrganizationIntegrationsQueryTests.cs | 8 ++++---- ...pdateOrganizationIntegrationCommandTests.cs | 10 +++++----- .../IntegrationHandlerResultTests.cs | 4 ++-- .../IntegrationMessageTests.cs | 6 +++--- .../IntegrationOAuthStateTests.cs | 6 +++--- .../IntegrationTemplateContextTests.cs | 4 ++-- ...tionIntegrationConfigurationDetailsTests.cs | 4 ++-- .../TestListenerConfiguration.cs | 5 +++-- .../Teams/TeamsBotCredentialProviderTests.cs | 4 ++-- ...AzureServiceBusEventListenerServiceTests.cs | 7 ++++--- ...erviceBusIntegrationListenerServiceTests.cs | 8 +++++--- .../Services/DatadogIntegrationHandlerTests.cs | 6 +++--- .../EventIntegrationEventWriteServiceTests.cs | 5 +++-- .../Services/EventIntegrationHandlerTests.cs | 11 ++++++----- .../Services/EventRepositoryHandlerTests.cs | 5 +++-- .../Services/IntegrationFilterFactoryTests.cs | 6 +++--- .../Services/IntegrationFilterServiceTests.cs | 6 +++--- .../Services/IntegrationHandlerTests.cs | 8 ++++---- ...onIntegrationConfigurationValidatorTests.cs | 10 +++++----- .../RabbitMqEventListenerServiceTests.cs | 7 ++++--- .../RabbitMqIntegrationListenerServiceTests.cs | 8 +++++--- .../Services/SlackIntegrationHandlerTests.cs | 9 +++++---- .../Services/SlackServiceTests.cs | 4 ++-- .../Services/TeamsIntegrationHandlerTests.cs | 7 ++++--- .../Services/TeamsServiceTests.cs | 12 ++++++------ .../Services/WebhookIntegrationHandlerTests.cs | 6 +++--- .../EventIntegrationsCacheConstantsTests.cs | 3 ++- 158 files changed, 487 insertions(+), 472 deletions(-) rename src/Api/{AdminConsole => Dirt}/Controllers/OrganizationIntegrationConfigurationController.cs (92%) rename src/Api/{AdminConsole => Dirt}/Controllers/OrganizationIntegrationController.cs (91%) rename src/Api/{AdminConsole => Dirt}/Controllers/SlackIntegrationController.cs (94%) rename src/Api/{AdminConsole => Dirt}/Controllers/TeamsIntegrationController.cs (94%) rename src/Api/{AdminConsole/Models/Request/Organizations => Dirt/Models/Request}/OrganizationIntegrationConfigurationRequestModel.cs (86%) rename src/Api/{AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs => Dirt/Models/Request/OrganizationIntegrationRequestModel.cs} (94%) rename src/Api/{AdminConsole/Models/Response/Organizations => Dirt/Models/Response}/OrganizationIntegrationConfigurationResponseModel.cs (90%) rename src/Api/{AdminConsole/Models/Response/Organizations => Dirt/Models/Response}/OrganizationIntegrationResponseModel.cs (93%) delete mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs delete mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs delete mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs delete mode 100644 src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs rename src/Core/{AdminConsole => Dirt}/Entities/OrganizationIntegration.cs (83%) rename src/Core/{AdminConsole => Dirt}/Entities/OrganizationIntegrationConfiguration.cs (93%) rename src/Core/{AdminConsole => Dirt}/Enums/IntegrationType.cs (96%) rename src/Core/{AdminConsole => Dirt}/Enums/OrganizationIntegrationStatus.cs (66%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs (98%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs (89%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs (90%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs (78%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs (88%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs (89%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs (85%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs (90%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs (92%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs (85%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs (85%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs (68%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs (83%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs (87%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs (80%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs (87%) rename src/Core/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs (86%) rename src/Core/{AdminConsole/Services/Implementations => Dirt}/EventIntegrations/README.md (100%) create mode 100644 src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs (54%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs (91%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/HecIntegration.cs (58%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/HecListenerConfiguration.cs (91%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IEventListenerConfiguration.cs (80%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs (86%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IIntegrationMessage.cs (77%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationFailureCategory.cs (93%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationFilterGroup.cs (76%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationFilterOperation.cs (61%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationFilterRule.cs (76%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationHandlerResult.cs (97%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationMessage.cs (93%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationOAuthState.cs (95%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationTemplateContext.cs (97%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/ListenerConfiguration.cs (94%) rename src/Core/{AdminConsole/Models/Data/Organizations => Dirt/Models/Data/EventIntegrations}/OrganizationIntegrationConfigurationDetails.cs (95%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs (87%) create mode 100644 src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs create mode 100644 src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs (56%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/SlackListenerConfiguration.cs (91%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/TeamsIntegration.cs (71%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs (56%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs (91%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/WebhookIntegration.cs (57%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs (60%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs (62%) rename src/Core/{AdminConsole => Dirt}/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs (91%) rename src/Core/{AdminConsole/Models => Dirt/Models/Data}/Slack/SlackApiResponse.cs (97%) rename src/Core/{AdminConsole/Models => Dirt/Models/Data}/Teams/TeamsApiResponse.cs (97%) rename src/Core/{AdminConsole/Models => Dirt/Models/Data}/Teams/TeamsBotCredentialProvider.cs (94%) rename src/Core/{AdminConsole => Dirt}/Repositories/IOrganizationIntegrationConfigurationRepository.cs (88%) rename src/Core/{AdminConsole => Dirt}/Repositories/IOrganizationIntegrationRepository.cs (74%) rename src/Core/{AdminConsole => Dirt}/Services/IAzureServiceBusService.cs (77%) rename src/Core/{AdminConsole => Dirt}/Services/IEventIntegrationPublisher.cs (67%) rename src/Core/{AdminConsole => Dirt}/Services/IEventMessageHandler.cs (85%) rename src/Core/{AdminConsole => Dirt}/Services/IIntegrationFilterService.cs (67%) rename src/Core/{AdminConsole => Dirt}/Services/IIntegrationHandler.cs (98%) rename src/Core/{AdminConsole => Dirt}/Services/IOrganizationIntegrationConfigurationValidator.cs (86%) rename src/Core/{AdminConsole => Dirt}/Services/IRabbitMqService.cs (89%) rename src/Core/{AdminConsole => Dirt}/Services/ISlackService.cs (97%) rename src/Core/{AdminConsole => Dirt}/Services/ITeamsService.cs (95%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/AzureServiceBusEventListenerService.cs (89%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/AzureServiceBusIntegrationListenerService.cs (94%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/AzureServiceBusService.cs (94%) rename src/Core/{AdminConsole => Dirt}/Services/Implementations/AzureTableStorageEventHandler.cs (84%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/DatadogIntegrationHandler.cs (90%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/EventIntegrationHandler.cs (97%) rename src/Core/{AdminConsole/Services => Dirt/Services/Implementations}/EventLoggingListenerService.cs (97%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/EventRepositoryHandler.cs (87%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/IntegrationFilterFactory.cs (97%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/IntegrationFilterService.cs (97%) rename src/Core/{AdminConsole/Services => Dirt/Services/Implementations}/OrganizationIntegrationConfigurationValidator.cs (92%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/RabbitMqEventListenerService.cs (91%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/RabbitMqIntegrationListenerService.cs (96%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/RabbitMqService.cs (98%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/SlackIntegrationHandler.cs (96%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/SlackService.cs (98%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/TeamsIntegrationHandler.cs (94%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/TeamsService.cs (96%) rename src/Core/{AdminConsole/Services/Implementations/EventIntegrations => Dirt/Services/Implementations}/WebhookIntegrationHandler.cs (92%) rename src/Core/{AdminConsole => Dirt}/Services/NoopImplementations/NoopSlackService.cs (88%) rename src/Core/{AdminConsole => Dirt}/Services/NoopImplementations/NoopTeamsService.cs (83%) rename src/Infrastructure.Dapper/{AdminConsole => Dirt}/Repositories/OrganizationIntegrationConfigurationRepository.cs (93%) rename src/Infrastructure.Dapper/{AdminConsole => Dirt}/Repositories/OrganizationIntegrationRepository.cs (90%) delete mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs delete mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegration.cs create mode 100644 src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegrationConfiguration.cs rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/OrganizationIntegrationConfigurationRepository.cs (75%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/OrganizationIntegrationRepository.cs (67%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs (82%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs (82%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs (91%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs (89%) rename src/Infrastructure.EntityFramework/{AdminConsole => Dirt}/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs (88%) rename test/Api.Test/{AdminConsole => Dirt}/Controllers/OrganizationIntegrationControllerTests.cs (95%) rename test/Api.Test/{AdminConsole => Dirt}/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs (96%) rename test/Api.Test/{AdminConsole => Dirt}/Controllers/SlackIntegrationControllerTests.cs (98%) rename test/Api.Test/{AdminConsole => Dirt}/Controllers/TeamsIntegrationControllerTests.cs (98%) rename test/Api.Test/{AdminConsole/Models/Request/Organizations => Dirt/Models/Request}/OrganizationIntegrationRequestModelTests.cs (97%) rename test/Api.Test/{AdminConsole/Models/Response/Organizations => Dirt/Models/Response}/OrganizationIntegrationResponseModelTests.cs (94%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs (98%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs (96%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs (94%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs (98%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs (93%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs (92%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs (86%) rename test/Core.Test/{AdminConsole => Dirt}/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs (95%) rename test/Core.Test/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs (96%) rename test/Core.Test/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationMessageTests.cs (96%) rename test/Core.Test/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs (94%) rename test/Core.Test/{AdminConsole => Dirt}/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs (97%) rename test/Core.Test/{AdminConsole/Models/Data/Organizations => Dirt/Models/Data/EventIntegrations}/OrganizationIntegrationConfigurationDetailsTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Models/Data/EventIntegrations/TestListenerConfiguration.cs (86%) rename test/Core.Test/{AdminConsole => Dirt}/Models/Data/Teams/TeamsBotCredentialProviderTests.cs (95%) rename test/Core.Test/{AdminConsole => Dirt}/Services/AzureServiceBusEventListenerServiceTests.cs (96%) rename test/Core.Test/{AdminConsole => Dirt}/Services/AzureServiceBusIntegrationListenerServiceTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Services/DatadogIntegrationHandlerTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Services/EventIntegrationEventWriteServiceTests.cs (95%) rename test/Core.Test/{AdminConsole => Dirt}/Services/EventIntegrationHandlerTests.cs (99%) rename test/Core.Test/{AdminConsole => Dirt}/Services/EventRepositoryHandlerTests.cs (90%) rename test/Core.Test/{AdminConsole => Dirt}/Services/IntegrationFilterFactoryTests.cs (91%) rename test/Core.Test/{AdminConsole => Dirt}/Services/IntegrationFilterServiceTests.cs (99%) rename test/Core.Test/{AdminConsole => Dirt}/Services/IntegrationHandlerTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Services/OrganizationIntegrationConfigurationValidatorTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Services/RabbitMqEventListenerServiceTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Services/RabbitMqIntegrationListenerServiceTests.cs (98%) rename test/Core.Test/{AdminConsole => Dirt}/Services/SlackIntegrationHandlerTests.cs (96%) rename test/Core.Test/{AdminConsole => Dirt}/Services/SlackServiceTests.cs (99%) rename test/Core.Test/{AdminConsole => Dirt}/Services/TeamsIntegrationHandlerTests.cs (98%) rename test/Core.Test/{AdminConsole => Dirt}/Services/TeamsServiceTests.cs (97%) rename test/Core.Test/{AdminConsole => Dirt}/Services/WebhookIntegrationHandlerTests.cs (98%) diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs b/src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs similarity index 92% rename from src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs rename to src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs index f172a23529..4296aa3edd 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs +++ b/src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs @@ -1,12 +1,12 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Api.Dirt.Models.Request; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; using Bit.Core.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs b/src/Api/Dirt/Controllers/OrganizationIntegrationController.cs similarity index 91% rename from src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs rename to src/Api/Dirt/Controllers/OrganizationIntegrationController.cs index b82fe3dfa8..960db648c2 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs +++ b/src/Api/Dirt/Controllers/OrganizationIntegrationController.cs @@ -1,12 +1,12 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Api.Dirt.Models.Request; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; using Bit.Core.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations/{organizationId:guid}/integrations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/Dirt/Controllers/SlackIntegrationController.cs similarity index 94% rename from src/Api/AdminConsole/Controllers/SlackIntegrationController.cs rename to src/Api/Dirt/Controllers/SlackIntegrationController.cs index 7b53f73f81..e98ed0d3fa 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/Dirt/Controllers/SlackIntegrationController.cs @@ -1,16 +1,16 @@ using System.Text.Json; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs b/src/Api/Dirt/Controllers/TeamsIntegrationController.cs similarity index 94% rename from src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs rename to src/Api/Dirt/Controllers/TeamsIntegrationController.cs index 36d107bbcc..b2bd55017c 100644 --- a/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs +++ b/src/Api/Dirt/Controllers/TeamsIntegrationController.cs @@ -1,18 +1,18 @@ using System.Text.Json; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs similarity index 86% rename from src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs rename to src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs index 9341392d68..e918bea2d6 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,8 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Enums; - -namespace Bit.Api.AdminConsole.Models.Request.Organizations; +namespace Bit.Api.Dirt.Models.Request; public class OrganizationIntegrationConfigurationRequestModel { diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs b/src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs similarity index 94% rename from src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs rename to src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs index 668afe70bf..259671bd66 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs +++ b/src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs @@ -1,10 +1,10 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Api.AdminConsole.Models.Request.Organizations; +namespace Bit.Api.Dirt.Models.Request; public class OrganizationIntegrationRequestModel : IValidatableObject { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs b/src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs similarity index 90% rename from src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs rename to src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs index d070375d88..62a3aea405 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs +++ b/src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; -namespace Bit.Api.AdminConsole.Models.Response.Organizations; +namespace Bit.Api.Dirt.Models.Response; public class OrganizationIntegrationConfigurationResponseModel : ResponseModel { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs b/src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs similarity index 93% rename from src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs rename to src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs index 0c31e07bef..60e885fe82 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs +++ b/src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Api; -namespace Bit.Api.AdminConsole.Models.Response.Organizations; +namespace Bit.Api.Dirt.Models.Response; public class OrganizationIntegrationResponseModel : ResponseModel { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs deleted file mode 100644 index 8785a74896..0000000000 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; - -public record DatadogIntegration(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs deleted file mode 100644 index dc2733c889..0000000000 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; - -public record SlackIntegration(string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs deleted file mode 100644 index 5b4fae0c76..0000000000 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; - -public record SlackIntegrationConfiguration(string ChannelId); diff --git a/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs b/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs deleted file mode 100644 index ad27429112..0000000000 --- a/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable enable - -using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; - -namespace Bit.Core.Services; - -public interface IIntegrationConfigurationDetailsCache -{ - List GetConfigurationDetails( - Guid organizationId, - IntegrationType integrationType, - EventType eventType); -} diff --git a/src/Core/AdminConsole/Entities/OrganizationIntegration.cs b/src/Core/Dirt/Entities/OrganizationIntegration.cs similarity index 83% rename from src/Core/AdminConsole/Entities/OrganizationIntegration.cs rename to src/Core/Dirt/Entities/OrganizationIntegration.cs index f1c96c8b98..42b4e89e27 100644 --- a/src/Core/AdminConsole/Entities/OrganizationIntegration.cs +++ b/src/Core/Dirt/Entities/OrganizationIntegration.cs @@ -1,8 +1,8 @@ -using Bit.Core.Entities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Entities; using Bit.Core.Utilities; -namespace Bit.Core.AdminConsole.Entities; +namespace Bit.Core.Dirt.Entities; public class OrganizationIntegration : ITableObject { diff --git a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs b/src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs similarity index 93% rename from src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs rename to src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs index a9ce676062..2b8dbf9220 100644 --- a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs +++ b/src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs @@ -2,7 +2,7 @@ using Bit.Core.Enums; using Bit.Core.Utilities; -namespace Bit.Core.AdminConsole.Entities; +namespace Bit.Core.Dirt.Entities; public class OrganizationIntegrationConfiguration : ITableObject { diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/Dirt/Enums/IntegrationType.cs similarity index 96% rename from src/Core/AdminConsole/Enums/IntegrationType.cs rename to src/Core/Dirt/Enums/IntegrationType.cs index 84e4de94e9..767f2feb06 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/Dirt/Enums/IntegrationType.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Enums; +namespace Bit.Core.Dirt.Enums; public enum IntegrationType : int { diff --git a/src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs b/src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs similarity index 66% rename from src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs rename to src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs index 78a7bc6d63..aad0530971 100644 --- a/src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs +++ b/src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs @@ -1,4 +1,4 @@ -namespace Bit.Api.AdminConsole.Models.Response.Organizations; +namespace Bit.Core.Dirt.Enums; public enum OrganizationIntegrationStatus : int { diff --git a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs b/src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs similarity index 98% rename from src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs rename to src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs index ebeef44484..b03a68cfa6 100644 --- a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs +++ b/src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs @@ -1,13 +1,15 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.AdminConsole.Models.Teams; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; -using Bit.Core.AdminConsole.Services.NoopImplementations; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Dirt.Services.NoopImplementations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs similarity index 89% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs index cb3ce8b9ea..478b43bb7e 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs @@ -1,13 +1,13 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Command implementation for creating organization integration configurations with validation and cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs similarity index 90% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs index 78768fd0d4..d6369f1b1b 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs @@ -1,11 +1,11 @@ -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Command implementation for deleting organization integration configurations with cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs similarity index 78% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs index a2078c3c98..6dfe2949a4 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs @@ -1,9 +1,9 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Query implementation for retrieving organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs similarity index 88% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs index 140cc79d1a..629a1ee8ed 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Command interface for creating organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs similarity index 89% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs index 3970676d40..d6866443c2 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Command interface for deleting organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs similarity index 85% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs index 2bf806c458..a6635cb3be 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Query interface for retrieving organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs similarity index 90% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs index 3e60a0af07..3ed680b808 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Command interface for updating organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs similarity index 92% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs index f619e2ddf2..69c28f3e7e 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs @@ -1,13 +1,13 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Command implementation for updating organization integration configurations with validation and cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs similarity index 85% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs index 376451977c..4423c103f9 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs @@ -1,12 +1,12 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Command implementation for creating organization integrations with cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs similarity index 85% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs index 614693cd82..dc1e7fb1dc 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs @@ -1,11 +1,11 @@ -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Command implementation for deleting organization integrations with cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs similarity index 68% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs index f7bbaadb4a..807f0b0b59 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; -using Bit.Core.Repositories; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Query implementation for retrieving organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs similarity index 83% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs index e7b79eab13..0b06d79bdb 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Command interface for creating an OrganizationIntegration. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs similarity index 87% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs index be22b4e482..8640f03ec8 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Command interface for deleting organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs similarity index 80% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs index 8cdea7f301..1f378abe9b 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Query interface for retrieving organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs similarity index 87% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs index f40086600d..ddba2bd233 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Command interface for updating organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs similarity index 86% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs index 12a8620926..77a3448276 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs @@ -1,12 +1,12 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Command implementation for updating organization integrations with cache invalidation support. diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/Dirt/EventIntegrations/README.md similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md rename to src/Core/Dirt/EventIntegrations/README.md diff --git a/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs new file mode 100644 index 0000000000..69a4deb66b --- /dev/null +++ b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; + +public record DatadogIntegration(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs similarity index 54% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs index 07aafa4bd8..ed91c3828b 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record DatadogIntegrationConfigurationDetails(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs index 1c74826791..ce35e29927 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class DatadogListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs similarity index 58% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs index 33ae5dadbe..df943e0bfc 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record HecIntegration(Uri Uri, string Scheme, string Token, string? Service = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs index 37a0d68beb..5ceb42be64 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class HecListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs similarity index 80% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs index 7df1459941..206dc2cc0b 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public interface IEventListenerConfiguration { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs similarity index 86% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs index 30401bb072..1fbfefa420 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public interface IIntegrationListenerConfiguration : IEventListenerConfiguration { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs similarity index 77% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs index 5b6bfe2e53..2d333dfee4 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public interface IIntegrationMessage { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs similarity index 93% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs index 544e671d51..f9d8f2ab68 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; /// /// Categories of event integration failures used for classification and retry logic. diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs similarity index 76% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs index 276ca3a14b..0c129883cf 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationFilterGroup { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs similarity index 61% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs index fddf630e26..d98ab1e13e 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public enum IntegrationFilterOperation { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs similarity index 76% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs index b5f90f5e63..9ac3ef753e 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationFilterRule { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs similarity index 97% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs index 375f2489cb..bbdce50ec0 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; /// /// Represents the result of an integration handler operation, including success status, diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs similarity index 93% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs index b0fc2161ba..edf31a2a1f 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs @@ -1,7 +1,7 @@ using System.Text.Json; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationMessage : IIntegrationMessage { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthState.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs similarity index 95% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthState.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs index 3b29bbebb4..d75780d6c6 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthState.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs @@ -1,8 +1,8 @@ using System.Security.Cryptography; using System.Text; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationOAuthState { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs similarity index 97% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index c44e550d15..3b527469fa 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -4,7 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationTemplateContext(EventMessage eventMessage) { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs similarity index 94% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs index 40eb2b3e77..2a970ce670 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs @@ -1,6 +1,6 @@ using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public abstract class ListenerConfiguration { diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs similarity index 95% rename from src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs index 5fdc760c90..6517ceccf0 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs @@ -1,9 +1,8 @@ using System.Text.Json.Nodes; +using Bit.Core.Dirt.Enums; using Bit.Core.Enums; -#nullable enable - -namespace Bit.Core.Models.Data.Organizations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class OrganizationIntegrationConfigurationDetails { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs similarity index 87% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs index 118b3a17fe..20299dd651 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs @@ -1,6 +1,6 @@ using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class RepositoryListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IEventListenerConfiguration diff --git a/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs new file mode 100644 index 0000000000..fcfd07f574 --- /dev/null +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; + +public record SlackIntegration(string Token); diff --git a/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs new file mode 100644 index 0000000000..164a132e8c --- /dev/null +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; + +public record SlackIntegrationConfiguration(string ChannelId); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs similarity index 56% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs index d22f43bb92..b81617118d 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record SlackIntegrationConfigurationDetails(string ChannelId, string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs index 7dd834f51e..ef2cf83837 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class SlackListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs similarity index 71% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs index 8390022839..fcb42a5261 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.Teams; +using Bit.Core.Dirt.Models.Data.Teams; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record TeamsIntegration( string TenantId, diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs similarity index 56% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs index 66fe558dff..a890f553f5 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record TeamsIntegrationConfigurationDetails(string ChannelId, Uri ServiceUrl); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs index 24cf674648..4111c96601 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class TeamsListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs similarity index 57% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs index dcda4caa92..d12ea16ee1 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs similarity index 60% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index 851bd3f411..8d7bf90e2c 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs similarity index 62% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs index dba9b1714d..49508f8454 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs index 9d5bf811c7..9afc26168c 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class WebhookListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs similarity index 97% rename from src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs rename to src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs index 3c811e2b28..a70e623ae3 100644 --- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs +++ b/src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Bit.Core.Models.Slack; +namespace Bit.Core.Dirt.Models.Data.Slack; public abstract class SlackApiResponse { diff --git a/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs b/src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs similarity index 97% rename from src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs rename to src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs index 131e45264f..b4b6a2542d 100644 --- a/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs +++ b/src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Bit.Core.Models.Teams; +namespace Bit.Core.Dirt.Models.Data.Teams; /// Represents the response returned by the Microsoft OAuth 2.0 token endpoint. /// See Microsoft identity platform and OAuth 2.0 diff --git a/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs b/src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs similarity index 94% rename from src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs rename to src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs index eeb17131a3..d8740f9e90 100644 --- a/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs +++ b/src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs @@ -1,6 +1,6 @@ using Microsoft.Bot.Connector.Authentication; -namespace Bit.Core.AdminConsole.Models.Teams; +namespace Bit.Core.Dirt.Models.Data.Teams; public class TeamsBotCredentialProvider(string clientId, string clientSecret) : ICredentialProvider { diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs b/src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs similarity index 88% rename from src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs rename to src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs index fb42ffa000..f6f90c7c9f 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs @@ -1,8 +1,10 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; -namespace Bit.Core.Repositories; +namespace Bit.Core.Dirt.Repositories; public interface IOrganizationIntegrationConfigurationRepository : IRepository { diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs b/src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs similarity index 74% rename from src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs rename to src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs index 1d8b8be0ec..03775e8d20 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs @@ -1,6 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Repositories; -namespace Bit.Core.Repositories; +namespace Bit.Core.Dirt.Repositories; public interface IOrganizationIntegrationRepository : IRepository { diff --git a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs b/src/Core/Dirt/Services/IAzureServiceBusService.cs similarity index 77% rename from src/Core/AdminConsole/Services/IAzureServiceBusService.cs rename to src/Core/Dirt/Services/IAzureServiceBusService.cs index 75864255c2..6b425511ab 100644 --- a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs +++ b/src/Core/Dirt/Services/IAzureServiceBusService.cs @@ -1,7 +1,7 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IAzureServiceBusService : IEventIntegrationPublisher, IAsyncDisposable { diff --git a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs b/src/Core/Dirt/Services/IEventIntegrationPublisher.cs similarity index 67% rename from src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs rename to src/Core/Dirt/Services/IEventIntegrationPublisher.cs index 4d95707e90..583c2448fe 100644 --- a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs +++ b/src/Core/Dirt/Services/IEventIntegrationPublisher.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IEventIntegrationPublisher : IAsyncDisposable { diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/Dirt/Services/IEventMessageHandler.cs similarity index 85% rename from src/Core/AdminConsole/Services/IEventMessageHandler.cs rename to src/Core/Dirt/Services/IEventMessageHandler.cs index 83c5e33ecb..9b1385129b 100644 --- a/src/Core/AdminConsole/Services/IEventMessageHandler.cs +++ b/src/Core/Dirt/Services/IEventMessageHandler.cs @@ -1,6 +1,6 @@ using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IEventMessageHandler { diff --git a/src/Core/AdminConsole/Services/IIntegrationFilterService.cs b/src/Core/Dirt/Services/IIntegrationFilterService.cs similarity index 67% rename from src/Core/AdminConsole/Services/IIntegrationFilterService.cs rename to src/Core/Dirt/Services/IIntegrationFilterService.cs index 5bc035d468..f46ab83f54 100644 --- a/src/Core/AdminConsole/Services/IIntegrationFilterService.cs +++ b/src/Core/Dirt/Services/IIntegrationFilterService.cs @@ -1,9 +1,9 @@ #nullable enable -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IIntegrationFilterService { diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/Dirt/Services/IIntegrationHandler.cs similarity index 98% rename from src/Core/AdminConsole/Services/IIntegrationHandler.cs rename to src/Core/Dirt/Services/IIntegrationHandler.cs index c36081cb52..81103b453d 100644 --- a/src/Core/AdminConsole/Services/IIntegrationHandler.cs +++ b/src/Core/Dirt/Services/IIntegrationHandler.cs @@ -1,8 +1,8 @@ using System.Globalization; using System.Net; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IIntegrationHandler { diff --git a/src/Core/AdminConsole/Services/IOrganizationIntegrationConfigurationValidator.cs b/src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs similarity index 86% rename from src/Core/AdminConsole/Services/IOrganizationIntegrationConfigurationValidator.cs rename to src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs index 48346cbae7..4a3a089f26 100644 --- a/src/Core/AdminConsole/Services/IOrganizationIntegrationConfigurationValidator.cs +++ b/src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs @@ -1,7 +1,7 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Services; +namespace Bit.Core.Dirt.Services; public interface IOrganizationIntegrationConfigurationValidator { diff --git a/src/Core/AdminConsole/Services/IRabbitMqService.cs b/src/Core/Dirt/Services/IRabbitMqService.cs similarity index 89% rename from src/Core/AdminConsole/Services/IRabbitMqService.cs rename to src/Core/Dirt/Services/IRabbitMqService.cs index 12c40c3b98..b9f824506f 100644 --- a/src/Core/AdminConsole/Services/IRabbitMqService.cs +++ b/src/Core/Dirt/Services/IRabbitMqService.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IRabbitMqService : IEventIntegrationPublisher { diff --git a/src/Core/AdminConsole/Services/ISlackService.cs b/src/Core/Dirt/Services/ISlackService.cs similarity index 97% rename from src/Core/AdminConsole/Services/ISlackService.cs rename to src/Core/Dirt/Services/ISlackService.cs index 60d3da8af4..111fcb5440 100644 --- a/src/Core/AdminConsole/Services/ISlackService.cs +++ b/src/Core/Dirt/Services/ISlackService.cs @@ -1,6 +1,7 @@ -using Bit.Core.Models.Slack; +using Bit.Core.Dirt.Models.Data.Slack; +using Bit.Core.Dirt.Services.Implementations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; /// Defines operations for interacting with Slack, including OAuth authentication, channel discovery, /// and sending messages. diff --git a/src/Core/AdminConsole/Services/ITeamsService.cs b/src/Core/Dirt/Services/ITeamsService.cs similarity index 95% rename from src/Core/AdminConsole/Services/ITeamsService.cs rename to src/Core/Dirt/Services/ITeamsService.cs index e3757987c3..30a324f9a4 100644 --- a/src/Core/AdminConsole/Services/ITeamsService.cs +++ b/src/Core/Dirt/Services/ITeamsService.cs @@ -1,6 +1,7 @@ -using Bit.Core.Models.Teams; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Services.Implementations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; /// /// Service that provides functionality relating to the Microsoft Teams integration including OAuth, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs b/src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs similarity index 89% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs rename to src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs index a589211687..6175374e2f 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -1,9 +1,9 @@ using System.Text; using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureServiceBusEventListenerService : EventLoggingListenerService where TConfiguration : IEventListenerConfiguration @@ -42,7 +42,7 @@ public class AzureServiceBusEventListenerService : EventLoggingL private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration) { return loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.AzureServiceBusEventListenerService.{configuration.EventSubscriptionName}"); + categoryName: $"Bit.Core.Dirt.Services.Implementations.AzureServiceBusEventListenerService.{configuration.EventSubscriptionName}"); } internal Task ProcessErrorAsync(ProcessErrorEventArgs args) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs b/src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs similarity index 94% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs rename to src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs index c97c5f7efe..32132ddb37 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -1,9 +1,9 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureServiceBusIntegrationListenerService : BackgroundService where TConfiguration : IIntegrationListenerConfiguration @@ -23,7 +23,7 @@ public class AzureServiceBusIntegrationListenerService : Backgro { _handler = handler; _logger = loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.AzureServiceBusIntegrationListenerService.{configuration.IntegrationSubscriptionName}"); + categoryName: $"Bit.Core.Dirt.Services.Implementations.AzureServiceBusIntegrationListenerService.{configuration.IntegrationSubscriptionName}"); _maxRetries = configuration.MaxRetries; _serviceBusService = serviceBusService; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs b/src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs similarity index 94% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs rename to src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs index 953a9bb56e..7b87850fe3 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs +++ b/src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs @@ -1,9 +1,9 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Settings; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureServiceBusService : IAzureServiceBusService { diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs similarity index 84% rename from src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs rename to src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs index 578dde9485..73d22b21a7 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs +++ b/src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs @@ -1,9 +1,8 @@ -#nullable enable - -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; +using Bit.Core.Services; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureTableStorageEventHandler( [FromKeyedServices("persistent")] IEventWriteService eventWriteService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs similarity index 90% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs index 45bb5b6d7d..e5c684ceec 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs @@ -1,7 +1,7 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class DatadogIntegrationHandler( IHttpClientFactory httpClientFactory, diff --git a/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs b/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs index 4ac97df763..44e0513ee0 100644 --- a/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs +++ b/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs @@ -1,7 +1,8 @@ using System.Text.Json; using Bit.Core.Models.Data; +using Bit.Core.Services; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDisposable { private readonly IEventIntegrationPublisher _eventIntegrationPublisher; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs index b4246884f7..bcd1f1dd8c 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs @@ -1,18 +1,18 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class EventIntegrationHandler( IntegrationType integrationType, diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs similarity index 97% rename from src/Core/AdminConsole/Services/EventLoggingListenerService.cs rename to src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs index 84a862ce94..29e3f8dec3 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs @@ -1,11 +1,9 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.Models.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public abstract class EventLoggingListenerService : BackgroundService { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs b/src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs similarity index 87% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs rename to src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs index ee3a2d5db2..32173b8da0 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs +++ b/src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs @@ -1,7 +1,8 @@ using Bit.Core.Models.Data; +using Bit.Core.Services; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class EventRepositoryHandler( [FromKeyedServices("persistent")] IEventWriteService eventWriteService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs b/src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs rename to src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs index d28ac910b7..8c25c80208 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs +++ b/src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public delegate bool IntegrationFilter(EventMessage message, object? value); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs b/src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs rename to src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs index 1c8fae4000..7d56b7c7ce 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs +++ b/src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class IntegrationFilterService : IIntegrationFilterService { diff --git a/src/Core/AdminConsole/Services/OrganizationIntegrationConfigurationValidator.cs b/src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs similarity index 92% rename from src/Core/AdminConsole/Services/OrganizationIntegrationConfigurationValidator.cs rename to src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs index 2769565675..7b6ab320b8 100644 --- a/src/Core/AdminConsole/Services/OrganizationIntegrationConfigurationValidator.cs +++ b/src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.AdminConsole.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class OrganizationIntegrationConfigurationValidator : IOrganizationIntegrationConfigurationValidator { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs b/src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs similarity index 91% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs rename to src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs index 430540a2f7..ca7cd5ef16 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,10 +1,10 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class RabbitMqEventListenerService : EventLoggingListenerService where TConfiguration : IEventListenerConfiguration @@ -69,6 +69,6 @@ public class RabbitMqEventListenerService : EventLoggingListener private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration) { return loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.RabbitMqEventListenerService.{configuration.EventQueueName}"); + categoryName: $"Bit.Core.Dirt.Services.Implementations.RabbitMqEventListenerService.{configuration.EventQueueName}"); } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs similarity index 96% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs rename to src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs index 0762edc040..eced9131bb 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -1,12 +1,12 @@ using System.Text; using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class RabbitMqIntegrationListenerService : BackgroundService where TConfiguration : IIntegrationListenerConfiguration @@ -37,7 +37,7 @@ public class RabbitMqIntegrationListenerService : BackgroundServ _timeProvider = timeProvider; _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); _logger = loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.RabbitMqIntegrationListenerService.{configuration.IntegrationQueueName}"); ; + categoryName: $"Bit.Core.Dirt.Services.Implementations.RabbitMqIntegrationListenerService.{configuration.IntegrationQueueName}"); ; } public override async Task StartAsync(CancellationToken cancellationToken) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs b/src/Core/Dirt/Services/Implementations/RabbitMqService.cs similarity index 98% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs rename to src/Core/Dirt/Services/Implementations/RabbitMqService.cs index 8976530cf4..c27fb37d08 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs +++ b/src/Core/Dirt/Services/Implementations/RabbitMqService.cs @@ -1,11 +1,11 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Settings; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class RabbitMqService : IRabbitMqService { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs similarity index 96% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs index e681140afe..6c6a4dd356 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class SlackIntegrationHandler( ISlackService slackService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs b/src/Core/Dirt/Services/Implementations/SlackService.cs similarity index 98% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs rename to src/Core/Dirt/Services/Implementations/SlackService.cs index 7eec2ec374..7683f718b5 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs +++ b/src/Core/Dirt/Services/Implementations/SlackService.cs @@ -2,11 +2,11 @@ using System.Net.Http.Json; using System.Text.Json; using System.Web; -using Bit.Core.Models.Slack; +using Bit.Core.Dirt.Models.Data.Slack; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class SlackService( IHttpClientFactory httpClientFactory, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs similarity index 94% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs index 9e3645a99f..7aaed6c647 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Rest; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class TeamsIntegrationHandler( ITeamsService teamsService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs b/src/Core/Dirt/Services/Implementations/TeamsService.cs similarity index 96% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs rename to src/Core/Dirt/Services/Implementations/TeamsService.cs index f9911760bb..edb43bf85e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs +++ b/src/Core/Dirt/Services/Implementations/TeamsService.cs @@ -2,9 +2,9 @@ using System.Net.Http.Json; using System.Text.Json; using System.Web; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Models.Teams; -using Bit.Core.Repositories; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Repositories; using Bit.Core.Settings; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Teams; @@ -12,9 +12,9 @@ using Microsoft.Bot.Connector; using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; -using TeamInfo = Bit.Core.Models.Teams.TeamInfo; +using TeamInfo = Bit.Core.Dirt.Models.Data.Teams.TeamInfo; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class TeamsService( IHttpClientFactory httpClientFactory, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs similarity index 92% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs index 0599f6e9d4..6caa1b9a6e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs @@ -1,8 +1,8 @@ using System.Net.Http.Headers; using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class WebhookIntegrationHandler( IHttpClientFactory httpClientFactory, diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs b/src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs similarity index 88% rename from src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs rename to src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs index a54df94814..30b68186bc 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs +++ b/src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs @@ -1,7 +1,6 @@ -using Bit.Core.Models.Slack; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.Slack; -namespace Bit.Core.AdminConsole.Services.NoopImplementations; +namespace Bit.Core.Dirt.Services.NoopImplementations; public class NoopSlackService : ISlackService { diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs b/src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs similarity index 83% rename from src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs rename to src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs index fafb23f570..3ebd58d996 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs +++ b/src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs @@ -1,7 +1,6 @@ -using Bit.Core.Models.Teams; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.Teams; -namespace Bit.Core.AdminConsole.Services.NoopImplementations; +namespace Bit.Core.Dirt.Services.NoopImplementations; public class NoopTeamsService : ITeamsService { diff --git a/src/Core/Utilities/EventIntegrationsCacheConstants.cs b/src/Core/Utilities/EventIntegrationsCacheConstants.cs index 19cc3f949c..000a9c230e 100644 --- a/src/Core/Utilities/EventIntegrationsCacheConstants.cs +++ b/src/Core/Utilities/EventIntegrationsCacheConstants.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Utilities; diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 445ff77109..e3ee82270f 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ using Bit.Infrastructure.Dapper.AdminConsole.Repositories; using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.Dapper.Billing.Repositories; using Bit.Infrastructure.Dapper.Dirt; +using Bit.Infrastructure.Dapper.Dirt.Repositories; using Bit.Infrastructure.Dapper.KeyManagement.Repositories; using Bit.Infrastructure.Dapper.NotificationCenter.Repositories; using Bit.Infrastructure.Dapper.Platform; diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs similarity index 93% rename from src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs rename to src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs index af24e11a0e..2b6b45f3c8 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -1,14 +1,15 @@ using System.Data; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; using Microsoft.Data.SqlClient; -namespace Bit.Infrastructure.Dapper.AdminConsole.Repositories; +namespace Bit.Infrastructure.Dapper.Dirt.Repositories; public class OrganizationIntegrationConfigurationRepository : Repository, IOrganizationIntegrationConfigurationRepository { diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationRepository.cs similarity index 90% rename from src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs rename to src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationRepository.cs index 4f8fb979d3..a094bbc669 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationRepository.cs @@ -1,11 +1,12 @@ using System.Data; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Repositories; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Repositories; using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; using Dapper; using Microsoft.Data.SqlClient; -namespace Bit.Infrastructure.Dapper.Repositories; +namespace Bit.Infrastructure.Dapper.Dirt.Repositories; public class OrganizationIntegrationRepository : Repository, IOrganizationIntegrationRepository { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs index 935473deaa..bc57c8ed15 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs @@ -1,4 +1,4 @@ -using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Dirt.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs index 3434d735d0..b14c156832 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs @@ -1,4 +1,4 @@ -using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Dirt.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs deleted file mode 100644 index 0f47d5947b..0000000000 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs +++ /dev/null @@ -1,16 +0,0 @@ -using AutoMapper; - -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; - -public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration -{ - public virtual required Organization Organization { get; set; } -} - -public class OrganizationIntegrationMapperProfile : Profile -{ - public OrganizationIntegrationMapperProfile() - { - CreateMap().ReverseMap(); - } -} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs deleted file mode 100644 index 21b282f767..0000000000 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs +++ /dev/null @@ -1,16 +0,0 @@ -using AutoMapper; - -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; - -public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration -{ - public virtual required OrganizationIntegration OrganizationIntegration { get; set; } -} - -public class OrganizationIntegrationConfigurationMapperProfile : Profile -{ - public OrganizationIntegrationConfigurationMapperProfile() - { - CreateMap().ReverseMap(); - } -} diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegration.cs b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegration.cs new file mode 100644 index 0000000000..f3472915a9 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegration.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Models; + +public class OrganizationIntegration : Core.Dirt.Entities.OrganizationIntegration +{ + public virtual required Organization Organization { get; set; } +} + +public class OrganizationIntegrationMapperProfile : Profile +{ + public OrganizationIntegrationMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegrationConfiguration.cs b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..11632d6530 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegrationConfiguration.cs @@ -0,0 +1,16 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Dirt.Models; + +public class OrganizationIntegrationConfiguration : Core.Dirt.Entities.OrganizationIntegrationConfiguration +{ + public virtual required OrganizationIntegration OrganizationIntegration { get; set; } +} + +public class OrganizationIntegrationConfigurationMapperProfile : Profile +{ + public OrganizationIntegrationConfigurationMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs similarity index 75% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs index ff8f92fd91..b0d545d3c3 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -1,17 +1,17 @@ using AutoMapper; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Infrastructure.EntityFramework.AdminConsole.Models; -using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; +using Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; -using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using OrganizationIntegrationConfiguration = Bit.Core.Dirt.Entities.OrganizationIntegrationConfiguration; -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories; -public class OrganizationIntegrationConfigurationRepository : Repository, IOrganizationIntegrationConfigurationRepository +public class OrganizationIntegrationConfigurationRepository : Repository, IOrganizationIntegrationConfigurationRepository { public OrganizationIntegrationConfigurationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations) @@ -43,7 +43,7 @@ public class OrganizationIntegrationConfigurationRepository : Repository> GetManyByIntegrationAsync( + public async Task> GetManyByIntegrationAsync( Guid organizationIntegrationId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationRepository.cs similarity index 67% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationRepository.cs index c11591efcd..cbcd574854 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationRepository.cs @@ -1,15 +1,15 @@ using AutoMapper; -using Bit.Core.Repositories; -using Bit.Infrastructure.EntityFramework.AdminConsole.Models; -using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; +using Bit.Core.Dirt.Repositories; +using Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using OrganizationIntegration = Bit.Core.Dirt.Entities.OrganizationIntegration; -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories; public class OrganizationIntegrationRepository : - Repository, + Repository, IOrganizationIntegrationRepository { public OrganizationIntegrationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) @@ -17,7 +17,7 @@ public class OrganizationIntegrationRepository : { } - public async Task> GetManyByOrganizationAsync(Guid organizationId) + public async Task> GetManyByOrganizationAsync(Guid organizationId) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -27,7 +27,7 @@ public class OrganizationIntegrationRepository : } } - public async Task GetByTeamsConfigurationTenantIdTeamId( + public async Task GetByTeamsConfigurationTenantIdTeamId( string tenantId, string teamId) { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs similarity index 82% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs index 421bb9407a..25fd06c04d 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs @@ -1,7 +1,10 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; -namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery( Guid organizationId, diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs similarity index 82% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs index 8141292c81..4d5be520d2 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs @@ -1,8 +1,8 @@ -#nullable enable +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; -using Bit.Core.Models.Data.Organizations; - -namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; public class OrganizationIntegrationConfigurationDetailsReadManyQuery : IQuery { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs similarity index 91% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs index 3ed3a48723..3ae2f5f66d 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; public class OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery : IQuery { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs similarity index 89% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs index a1e86d9add..fd06c6d296 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs @@ -1,9 +1,9 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; public class OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery : IQuery { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs similarity index 88% rename from src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs rename to src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs index df87ad0bc1..477983ebab 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; -namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; +namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries; public class OrganizationIntegrationReadManyByOrganizationIdQuery : IQuery { diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs b/test/Api.Test/Dirt/Controllers/OrganizationIntegrationControllerTests.cs similarity index 95% rename from test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs rename to test/Api.Test/Dirt/Controllers/OrganizationIntegrationControllerTests.cs index c9131f3505..85f4e7ca7f 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs +++ b/test/Api.Test/Dirt/Controllers/OrganizationIntegrationControllerTests.cs @@ -1,10 +1,10 @@ -using Bit.Api.AdminConsole.Controllers; -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Api.Dirt.Controllers; +using Bit.Api.Dirt.Models.Request; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc; using NSubstitute; using Xunit; -namespace Bit.Api.Test.AdminConsole.Controllers; +namespace Bit.Api.Test.Dirt.Controllers; [ControllerCustomize(typeof(OrganizationIntegrationController))] [SutProviderCustomize] diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/Dirt/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs similarity index 96% rename from test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs rename to test/Api.Test/Dirt/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index 6e1dadb92f..ec8e5c3e36 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/Dirt/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -1,9 +1,9 @@ -using Bit.Api.AdminConsole.Controllers; -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Api.Dirt.Controllers; +using Bit.Api.Dirt.Models.Request; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc; using NSubstitute; using Xunit; -namespace Bit.Api.Test.AdminConsole.Controllers; +namespace Bit.Api.Test.Dirt.Controllers; [ControllerCustomize(typeof(OrganizationIntegrationConfigurationController))] [SutProviderCustomize] diff --git a/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs b/test/Api.Test/Dirt/Controllers/SlackIntegrationControllerTests.cs similarity index 98% rename from test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs rename to test/Api.Test/Dirt/Controllers/SlackIntegrationControllerTests.cs index c079445559..a8dcfc3395 100644 --- a/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs +++ b/test/Api.Test/Dirt/Controllers/SlackIntegrationControllerTests.cs @@ -1,13 +1,13 @@ #nullable enable -using Bit.Api.AdminConsole.Controllers; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Api.Dirt.Controllers; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Mvc; @@ -16,7 +16,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; -namespace Bit.Api.Test.AdminConsole.Controllers; +namespace Bit.Api.Test.Dirt.Controllers; [ControllerCustomize(typeof(SlackIntegrationController))] [SutProviderCustomize] diff --git a/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs b/test/Api.Test/Dirt/Controllers/TeamsIntegrationControllerTests.cs similarity index 98% rename from test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs rename to test/Api.Test/Dirt/Controllers/TeamsIntegrationControllerTests.cs index 3302a87372..b7e778339b 100644 --- a/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs +++ b/test/Api.Test/Dirt/Controllers/TeamsIntegrationControllerTests.cs @@ -1,14 +1,14 @@ #nullable enable -using Bit.Api.AdminConsole.Controllers; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Api.Dirt.Controllers; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Models.Teams; -using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; @@ -20,7 +20,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; -namespace Bit.Api.Test.AdminConsole.Controllers; +namespace Bit.Api.Test.Dirt.Controllers; [ControllerCustomize(typeof(TeamsIntegrationController))] [SutProviderCustomize] diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs b/test/Api.Test/Dirt/Models/Request/OrganizationIntegrationRequestModelTests.cs similarity index 97% rename from test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs rename to test/Api.Test/Dirt/Models/Request/OrganizationIntegrationRequestModelTests.cs index 76e206abf4..190eae260c 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs +++ b/test/Api.Test/Dirt/Models/Request/OrganizationIntegrationRequestModelTests.cs @@ -1,13 +1,13 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Api.Dirt.Models.Request; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations; +namespace Bit.Api.Test.Dirt.Models.Request; public class OrganizationIntegrationRequestModelTests { diff --git a/test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs b/test/Api.Test/Dirt/Models/Response/OrganizationIntegrationResponseModelTests.cs similarity index 94% rename from test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs rename to test/Api.Test/Dirt/Models/Response/OrganizationIntegrationResponseModelTests.cs index 28bc07de38..e6f8d5d756 100644 --- a/test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs +++ b/test/Api.Test/Dirt/Models/Response/OrganizationIntegrationResponseModelTests.cs @@ -1,15 +1,15 @@ #nullable enable using System.Text.Json; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; -using Bit.Core.Models.Teams; +using Bit.Api.Dirt.Models.Response; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Api.Test.AdminConsole.Models.Response.Organizations; +namespace Bit.Api.Test.Dirt.Models.Response; public class OrganizationIntegrationResponseModelTests { diff --git a/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs index 715bffaab1..134aa17129 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Xunit; namespace Bit.Core.Test.Services; diff --git a/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs b/test/Core.Test/Dirt/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs similarity index 98% rename from test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs rename to test/Core.Test/Dirt/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs index 0ca2d55c78..37b303b735 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs @@ -1,12 +1,15 @@ -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; -using Bit.Core.AdminConsole.Services.NoopImplementations; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Dirt.Services.NoopImplementations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Test.Dirt.Models.Data.EventIntegrations; using Bit.Core.Utilities; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; @@ -19,7 +22,7 @@ using StackExchange.Redis; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations; +namespace Bit.Core.Test.Dirt.EventIntegrations; public class EventIntegrationServiceCollectionExtensionsTests { diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs similarity index 96% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs index c6c8a44955..3ad3569c07 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs @@ -1,9 +1,10 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; -using Bit.Core.AdminConsole.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -11,7 +12,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; [SutProviderCustomize] public class CreateOrganizationIntegrationConfigurationCommandTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs index 3b12f4bd88..c053a761bb 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs @@ -1,8 +1,9 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -10,7 +11,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; [SutProviderCustomize] public class DeleteOrganizationIntegrationConfigurationCommandTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs similarity index 94% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs index 18541df53e..780467a91a 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs @@ -1,13 +1,13 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; [SutProviderCustomize] public class GetOrganizationIntegrationConfigurationsQueryTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs similarity index 98% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs index c2eeefc087..42ea278aa6 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs @@ -1,9 +1,10 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; -using Bit.Core.AdminConsole.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -11,7 +12,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; [SutProviderCustomize] public class UpdateOrganizationIntegrationConfigurationCommandTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs similarity index 93% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs index 62af1eb3ed..4933656eb3 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -10,7 +10,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations; [SutProviderCustomize] public class CreateOrganizationIntegrationCommandTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs similarity index 92% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs index 25a00bded1..15a3b44bcf 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -10,7 +10,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations; [SutProviderCustomize] public class DeleteOrganizationIntegrationCommandTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs similarity index 86% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs index dfa8e4b306..19b35ac340 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs @@ -1,12 +1,12 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; -using Bit.Core.Repositories; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations; [SutProviderCustomize] public class GetOrganizationIntegrationsQueryTests diff --git a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs similarity index 95% rename from test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs rename to test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs index fdedec2e51..34bf02c34b 100644 --- a/test/Core.Test/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs +++ b/test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -10,7 +10,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations; [SutProviderCustomize] public class UpdateOrganizationIntegrationCommandTests diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs similarity index 96% rename from test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs rename to test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs index 6925a978eb..4b6292b7c4 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs +++ b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations; public class IntegrationHandlerResultTests { diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationMessageTests.cs similarity index 96% rename from test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs rename to test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationMessageTests.cs index 71f9a15037..6f0ce11db8 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs +++ b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationMessageTests.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Xunit; -namespace Bit.Core.Test.Models.Data.EventIntegrations; +namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations; public class IntegrationMessageTests { diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs similarity index 94% rename from test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs rename to test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs index 8605a3dcab..a3e05ffe37 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs +++ b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs @@ -1,12 +1,12 @@ #nullable enable -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Time.Testing; using Xunit; -namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations; public class IntegrationOAuthStateTests { diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs rename to test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs index d9a3cd6e8a..7bacb4046b 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs +++ b/test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs @@ -1,13 +1,13 @@ #nullable enable using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations; public class IntegrationTemplateContextTests { diff --git a/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs b/test/Core.Test/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetailsTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs rename to test/Core.Test/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetailsTests.cs index 4b8cd4f47c..ae574d7ee6 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs +++ b/test/Core.Test/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetailsTests.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using Bit.Core.Models.Data.Organizations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Xunit; -namespace Bit.Core.Test.Models.Data.Organizations; +namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations; public class OrganizationIntegrationConfigurationDetailsTests { diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs b/test/Core.Test/Dirt/Models/Data/EventIntegrations/TestListenerConfiguration.cs similarity index 86% rename from test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs rename to test/Core.Test/Dirt/Models/Data/EventIntegrations/TestListenerConfiguration.cs index 50442dd463..2c811e06f5 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs +++ b/test/Core.Test/Dirt/Models/Data/EventIntegrations/TestListenerConfiguration.cs @@ -1,6 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations; public class TestListenerConfiguration : IIntegrationListenerConfiguration { diff --git a/test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs b/test/Core.Test/Dirt/Models/Data/Teams/TeamsBotCredentialProviderTests.cs similarity index 95% rename from test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs rename to test/Core.Test/Dirt/Models/Data/Teams/TeamsBotCredentialProviderTests.cs index d3d433727f..24576899d5 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs +++ b/test/Core.Test/Dirt/Models/Data/Teams/TeamsBotCredentialProviderTests.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Models.Teams; +using Bit.Core.Dirt.Models.Data.Teams; using Microsoft.Bot.Connector.Authentication; using Xunit; -namespace Bit.Core.Test.Models.Data.Teams; +namespace Bit.Core.Test.Dirt.Models.Data.Teams; public class TeamsBotCredentialProviderTests { diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs b/test/Core.Test/Dirt/Services/AzureServiceBusEventListenerServiceTests.cs similarity index 96% rename from test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs rename to test/Core.Test/Dirt/Services/AzureServiceBusEventListenerServiceTests.cs index c6ef3063e2..92f0b16b3f 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs +++ b/test/Core.Test/Dirt/Services/AzureServiceBusEventListenerServiceTests.cs @@ -2,9 +2,10 @@ using System.Text.Json; using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Models.Data; -using Bit.Core.Services; +using Bit.Core.Test.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -12,7 +13,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class AzureServiceBusEventListenerServiceTests diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/Dirt/Services/AzureServiceBusIntegrationListenerServiceTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs rename to test/Core.Test/Dirt/Services/AzureServiceBusIntegrationListenerServiceTests.cs index 9e46a3a99a..88688f49ff 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/Dirt/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -2,8 +2,10 @@ using System.Text.Json; using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Test.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Logging; @@ -11,7 +13,7 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class AzureServiceBusIntegrationListenerServiceTests diff --git a/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs b/test/Core.Test/Dirt/Services/DatadogIntegrationHandlerTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs rename to test/Core.Test/Dirt/Services/DatadogIntegrationHandlerTests.cs index 9cb21f012a..a8c5d7da95 100644 --- a/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/DatadogIntegrationHandlerTests.cs @@ -1,8 +1,8 @@ #nullable enable using System.Net; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -11,7 +11,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class DatadogIntegrationHandlerTests diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs b/test/Core.Test/Dirt/Services/EventIntegrationEventWriteServiceTests.cs similarity index 95% rename from test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs rename to test/Core.Test/Dirt/Services/EventIntegrationEventWriteServiceTests.cs index 16df234004..3870601604 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs +++ b/test/Core.Test/Dirt/Services/EventIntegrationEventWriteServiceTests.cs @@ -1,12 +1,13 @@ using System.Text.Json; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Models.Data; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class EventIntegrationEventWriteServiceTests diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/Dirt/Services/EventIntegrationHandlerTests.cs similarity index 99% rename from test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs rename to test/Core.Test/Dirt/Services/EventIntegrationHandlerTests.cs index 235d597b12..e15a254b39 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/EventIntegrationHandlerTests.cs @@ -2,14 +2,15 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -19,7 +20,7 @@ using NSubstitute; using Xunit; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class EventIntegrationHandlerTests diff --git a/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs b/test/Core.Test/Dirt/Services/EventRepositoryHandlerTests.cs similarity index 90% rename from test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs rename to test/Core.Test/Dirt/Services/EventRepositoryHandlerTests.cs index 48c3a143d4..6392f0138d 100644 --- a/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/EventRepositoryHandlerTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Models.Data; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Models.Data; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -6,7 +7,7 @@ using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class EventRepositoryHandlerTests diff --git a/test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs b/test/Core.Test/Dirt/Services/IntegrationFilterFactoryTests.cs similarity index 91% rename from test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs rename to test/Core.Test/Dirt/Services/IntegrationFilterFactoryTests.cs index b408bc1501..83780b1fe0 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationFilterFactoryTests.cs +++ b/test/Core.Test/Dirt/Services/IntegrationFilterFactoryTests.cs @@ -1,9 +1,9 @@ -using Bit.Core.Models.Data; -using Bit.Core.Services; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Models.Data; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; public class IntegrationFilterFactoryTests { diff --git a/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs b/test/Core.Test/Dirt/Services/IntegrationFilterServiceTests.cs similarity index 99% rename from test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs rename to test/Core.Test/Dirt/Services/IntegrationFilterServiceTests.cs index fb33737c16..b7510b0e92 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationFilterServiceTests.cs +++ b/test/Core.Test/Dirt/Services/IntegrationFilterServiceTests.cs @@ -1,13 +1,13 @@ #nullable enable using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Models.Data; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; public class IntegrationFilterServiceTests { diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/Dirt/Services/IntegrationHandlerTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs rename to test/Core.Test/Dirt/Services/IntegrationHandlerTests.cs index b3bbcb7ef2..096fcc11bb 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/IntegrationHandlerTests.cs @@ -1,10 +1,10 @@ using System.Net; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; -using Bit.Core.Services; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; public class IntegrationHandlerTests { diff --git a/test/Core.Test/AdminConsole/Services/OrganizationIntegrationConfigurationValidatorTests.cs b/test/Core.Test/Dirt/Services/OrganizationIntegrationConfigurationValidatorTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Services/OrganizationIntegrationConfigurationValidatorTests.cs rename to test/Core.Test/Dirt/Services/OrganizationIntegrationConfigurationValidatorTests.cs index 1154ad8025..bee6a5182c 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationIntegrationConfigurationValidatorTests.cs +++ b/test/Core.Test/Dirt/Services/OrganizationIntegrationConfigurationValidatorTests.cs @@ -1,11 +1,11 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.AdminConsole.Services; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services.Implementations; using Xunit; -namespace Bit.Core.Test.AdminConsole.Services; +namespace Bit.Core.Test.Dirt.Services; public class OrganizationIntegrationConfigurationValidatorTests { diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs b/test/Core.Test/Dirt/Services/RabbitMqEventListenerServiceTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs rename to test/Core.Test/Dirt/Services/RabbitMqEventListenerServiceTests.cs index 22e297a00d..560cf589ed 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs +++ b/test/Core.Test/Dirt/Services/RabbitMqEventListenerServiceTests.cs @@ -1,9 +1,10 @@ #nullable enable using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Core.Models.Data; -using Bit.Core.Services; +using Bit.Core.Test.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -13,7 +14,7 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class RabbitMqEventListenerServiceTests diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/Dirt/Services/RabbitMqIntegrationListenerServiceTests.cs similarity index 98% rename from test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs rename to test/Core.Test/Dirt/Services/RabbitMqIntegrationListenerServiceTests.cs index 71985889f8..453a4e6527 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs +++ b/test/Core.Test/Dirt/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -1,8 +1,10 @@ #nullable enable using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Test.Dirt.Models.Data.EventIntegrations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -13,7 +15,7 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class RabbitMqIntegrationListenerServiceTests diff --git a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs b/test/Core.Test/Dirt/Services/SlackIntegrationHandlerTests.cs similarity index 96% rename from test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs rename to test/Core.Test/Dirt/Services/SlackIntegrationHandlerTests.cs index e455100995..52bb7a03a4 100644 --- a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/SlackIntegrationHandlerTests.cs @@ -1,13 +1,14 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Models.Slack; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Slack; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class SlackIntegrationHandlerTests diff --git a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs b/test/Core.Test/Dirt/Services/SlackServiceTests.cs similarity index 99% rename from test/Core.Test/AdminConsole/Services/SlackServiceTests.cs rename to test/Core.Test/Dirt/Services/SlackServiceTests.cs index 068e5e8c82..bbb505f5d3 100644 --- a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs +++ b/test/Core.Test/Dirt/Services/SlackServiceTests.cs @@ -3,7 +3,7 @@ using System.Net; using System.Text.Json; using System.Web; -using Bit.Core.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.MockedHttpClient; @@ -11,7 +11,7 @@ using NSubstitute; using Xunit; using GlobalSettings = Bit.Core.Settings.GlobalSettings; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class SlackServiceTests diff --git a/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs b/test/Core.Test/Dirt/Services/TeamsIntegrationHandlerTests.cs similarity index 98% rename from test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs rename to test/Core.Test/Dirt/Services/TeamsIntegrationHandlerTests.cs index 11056ec2cc..b608ed7ff8 100644 --- a/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/TeamsIntegrationHandlerTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -9,7 +10,7 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class TeamsIntegrationHandlerTests diff --git a/test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs b/test/Core.Test/Dirt/Services/TeamsServiceTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs rename to test/Core.Test/Dirt/Services/TeamsServiceTests.cs index 17d65f3237..61d20cc0af 100644 --- a/test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs +++ b/test/Core.Test/Dirt/Services/TeamsServiceTests.cs @@ -3,11 +3,11 @@ using System.Net; using System.Text.Json; using System.Web; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Models.Teams; -using Bit.Core.Repositories; -using Bit.Core.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.MockedHttpClient; @@ -15,7 +15,7 @@ using NSubstitute; using Xunit; using GlobalSettings = Bit.Core.Settings.GlobalSettings; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class TeamsServiceTests diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/Dirt/Services/WebhookIntegrationHandlerTests.cs similarity index 98% rename from test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs rename to test/Core.Test/Dirt/Services/WebhookIntegrationHandlerTests.cs index 05aa46681a..5d8bbfe439 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/Dirt/Services/WebhookIntegrationHandlerTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http.Headers; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -10,7 +10,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Dirt.Services; [SutProviderCustomize] public class WebhookIntegrationHandlerTests diff --git a/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs b/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs index a87392c2c1..7b467c0af4 100644 --- a/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs +++ b/test/Core.Test/Utilities/EventIntegrationsCacheConstantsTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; From c98f31a9f7b6ded57d6f74d7dd37fff52eb9a807 Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Tue, 30 Dec 2025 18:22:09 +0100 Subject: [PATCH 43/68] Review Code Triggered by labeled event (#6782) --- .github/workflows/review-code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml index 0e0597fccf..908664209d 100644 --- a/.github/workflows/review-code.yml +++ b/.github/workflows/review-code.yml @@ -2,7 +2,7 @@ name: Code Review on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, labeled] permissions: {} From f82552fba93a9a68f7cf71f2d54f72d6bfce4ad1 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:08:10 -0500 Subject: [PATCH 44/68] [PM-29568] Fix footer styling (#6722) * fix: update footer background color to match UIF Tailwind standards. * fix: modify spacing for footer * chore: build templates * fix: update social icon assets * fix: update footer image source * fix: update send access --- .../Auth/SendAccessEmailOtpEmailv2.html.hbs | 68 ++++----- ...ion-confirmation-enterprise-teams.html.hbs | 68 ++++----- ...nization-confirmation-family-free.html.hbs | 68 ++++----- .../Onboarding/welcome-family-user.html.hbs | 142 ++++++++---------- .../welcome-individual-user.html.hbs | 142 ++++++++---------- .../Auth/Onboarding/welcome-org-user.html.hbs | 142 ++++++++---------- .../MailTemplates/Mjml/components/footer.mjml | 20 +-- .../components/mj-bw-learn-more-footer.js | 2 +- 8 files changed, 302 insertions(+), 350 deletions(-) diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs index f9cc04f73e..352bb447c8 100644 --- a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs @@ -378,12 +378,12 @@ - + -
    +
    - +
    @@ -471,8 +471,8 @@ - -
    - + +
    @@ -488,13 +488,13 @@
    - +
    + - @@ -511,13 +511,13 @@ -
    + - +
    - +
    + - @@ -534,13 +534,13 @@ -
    + - +
    - +
    + - @@ -557,13 +557,13 @@ -
    + - +
    - +
    + - @@ -580,13 +580,13 @@ -
    + - +
    - +
    + - @@ -603,13 +603,13 @@ -
    + - +
    - +
    + - @@ -626,13 +626,13 @@ -
    + - +
    - +
    + - @@ -653,7 +653,7 @@
    + - +
    -

    +

    © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA

    diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs index 65e37e87dd..be1a3854b5 100644 --- a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs +++ b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs @@ -502,12 +502,12 @@
    - + -
    +
    - +
    @@ -595,8 +595,8 @@ - -
    - + +
    @@ -612,13 +612,13 @@
    - +
    + - @@ -635,13 +635,13 @@ -
    + - +
    - +
    + - @@ -658,13 +658,13 @@ -
    + - +
    - +
    + - @@ -681,13 +681,13 @@ -
    + - +
    - +
    + - @@ -704,13 +704,13 @@ -
    + - +
    - +
    + - @@ -727,13 +727,13 @@ -
    + - +
    - +
    + - @@ -750,13 +750,13 @@ -
    + - +
    - +
    + - @@ -777,7 +777,7 @@
    + - +
    -

    +

    © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA

    diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs index c22bc80a51..b9984343d5 100644 --- a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs +++ b/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs @@ -670,12 +670,12 @@
    - + -
    +
    - +
    @@ -763,8 +763,8 @@ - -
    - + +
    @@ -780,13 +780,13 @@
    - +
    + - @@ -803,13 +803,13 @@ -
    + - +
    - +
    + - @@ -826,13 +826,13 @@ -
    + - +
    - +
    + - @@ -849,13 +849,13 @@ -
    + - +
    - +
    + - @@ -872,13 +872,13 @@ -
    + - +
    - +
    + - @@ -895,13 +895,13 @@ -
    + - +
    - +
    + - @@ -918,13 +918,13 @@ -
    + - +
    - +
    + - @@ -945,7 +945,7 @@
    + - +
    -

    +

    © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA

    diff --git a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs index 9c4b2406d4..1998cf10ba 100644 --- a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs +++ b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs @@ -30,6 +30,14 @@ + + + + + + + + + + + + + + + + + +