From ad2ea4ca21a7ca770fd04b123638d6b8c8578dc8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:26:34 -0500 Subject: [PATCH 01/22] Don't enable tax for customer without tax info (#5347) --- .../Implementations/OrganizationBillingService.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 201de22525..1bc4f792d7 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -356,11 +356,20 @@ public class OrganizationBillingService( } } + var customerHasTaxInfo = customer is + { + Address: + { + Country: not null and not "", + PostalCode: not null and not "" + } + }; + var subscriptionCreateOptions = new SubscriptionCreateOptions { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true + Enabled = customerHasTaxInfo }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, From 23dce5810326b5a307ebdbd6bc09528964de5222 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:58:14 -0500 Subject: [PATCH 02/22] [deps] Billing: Update xunit to 2.9.3 (#5289) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../Infrastructure.Dapper.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj index 65041c3023..1ba67ba61e 100644 --- a/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj +++ b/test/Infrastructure.Dapper.Test/Infrastructure.Dapper.Test.csproj @@ -11,7 +11,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 443a147433d30c45ddff1f8b80e626e890253197 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:55:05 -0500 Subject: [PATCH 03/22] Replace StripePaymentService with PremiumUserBillingService in ReplacePaymentMethodAsync call (#5350) --- .../Services/IPremiumUserBillingService.cs | 8 ++++++- .../PremiumUserBillingService.cs | 23 +++++++++++++++++++ .../Services/Implementations/UserService.cs | 11 +++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Core/Billing/Services/IPremiumUserBillingService.cs b/src/Core/Billing/Services/IPremiumUserBillingService.cs index f74bf6c8da..2161b247b9 100644 --- a/src/Core/Billing/Services/IPremiumUserBillingService.cs +++ b/src/Core/Billing/Services/IPremiumUserBillingService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Sales; using Bit.Core.Entities; namespace Bit.Core.Billing.Services; @@ -27,4 +28,9 @@ public interface IPremiumUserBillingService /// /// Task Finalize(PremiumUserSale sale); + + Task UpdatePaymentMethod( + User user, + TokenizedPaymentSource tokenizedPaymentSource, + TaxInformation taxInformation); } diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 0672a8d5e7..ed841c9576 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -1,5 +1,6 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Entities; using Bit.Core.Enums; @@ -58,6 +59,28 @@ public class PremiumUserBillingService( await userRepository.ReplaceAsync(user); } + public async Task UpdatePaymentMethod( + User user, + TokenizedPaymentSource tokenizedPaymentSource, + TaxInformation taxInformation) + { + if (string.IsNullOrEmpty(user.GatewayCustomerId)) + { + var customer = await CreateCustomerAsync(user, + new CustomerSetup { TokenizedPaymentSource = tokenizedPaymentSource, TaxInformation = taxInformation }); + + user.Gateway = GatewayType.Stripe; + user.GatewayCustomerId = customer.Id; + + await userRepository.ReplaceAsync(user); + } + else + { + await subscriberService.UpdatePaymentSource(user, tokenizedPaymentSource); + await subscriberService.UpdateTaxInformation(user, taxInformation); + } + } + private async Task CreateCustomerAsync( User user, CustomerSetup customerSetup) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 157bfd3a6e..11d4042def 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -1044,11 +1045,11 @@ public class UserService : UserManager, IUserService, IDisposable throw new BadRequestException("Invalid token."); } - var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo: taxInfo); - if (updated) - { - await SaveUserAsync(user); - } + var tokenizedPaymentSource = new TokenizedPaymentSource(paymentMethodType, paymentToken); + var taxInformation = TaxInformation.From(taxInfo); + + await _premiumUserBillingService.UpdatePaymentMethod(user, tokenizedPaymentSource, taxInformation); + await SaveUserAsync(user); } public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null) From 5efd68cf51ab7437a6e92b4279c3dc8f73ac21db Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 30 Jan 2025 11:07:02 -0600 Subject: [PATCH 04/22] [PM-17562] Initial POC of Distributed Events (#5323) * Initial POC of Distributed Events * Apply suggestions from code review Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Clean up files to support accepted changes. Address PR Feedback * Removed unneeded using to fix lint warning * Moved config into a common EventLogging top-level item. Fixed issues from PR review * Optimized per suggestion from justinbaur Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Updated to add IAsyncDisposable as suggested in PR review * Updated with suggestion to use KeyedSingleton for the IEventWriteService * Changed key case to lowercase --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- dev/.env.example | 6 +- dev/docker-compose.yml | 15 +++ .../RabbitMqEventHttpPostListener.cs | 35 +++++++ .../RabbitMqEventListenerBase.cs | 93 +++++++++++++++++++ .../RabbitMqEventRepositoryListener.cs | 29 ++++++ .../RabbitMqEventWriteService.cs | 65 +++++++++++++ src/Core/Core.csproj | 5 +- src/Core/Settings/GlobalSettings.cs | 39 ++++++++ src/Events/Startup.cs | 16 ++++ .../Utilities/ServiceCollectionExtensions.cs | 12 ++- 10 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs diff --git a/dev/.env.example b/dev/.env.example index d0ebf50efb..f0aed83a59 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -20,4 +20,8 @@ IDP_SP_ACS_URL=http://localhost:51822/saml2/yourOrgIdHere/Acs # Optional reverse proxy configuration # Should match server listen ports in reverse-proxy.conf API_PROXY_PORT=4100 -IDENTITY_PROXY_PORT=33756 \ No newline at end of file +IDENTITY_PROXY_PORT=33756 + +# Optional RabbitMQ configuration +RABBITMQ_DEFAULT_USER=bitwarden +RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123 diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index c02d3c872b..d23eaefbb0 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -84,6 +84,20 @@ services: profiles: - idp + rabbitmq: + image: rabbitmq:management + container_name: rabbitmq + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + volumes: + - rabbitmq_data:/var/lib/rabbitmq_data + profiles: + - rabbitmq + reverse-proxy: image: nginx:alpine container_name: reverse-proxy @@ -99,3 +113,4 @@ volumes: mssql_dev_data: postgres_dev_data: mysql_dev_data: + rabbitmq_data: diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs new file mode 100644 index 0000000000..5a875f9278 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs @@ -0,0 +1,35 @@ +using System.Net.Http.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class RabbitMqEventHttpPostListener : RabbitMqEventListenerBase +{ + private readonly HttpClient _httpClient; + private readonly string _httpPostUrl; + private readonly string _queueName; + + protected override string QueueName => _queueName; + + public const string HttpClientName = "EventHttpPostListenerHttpClient"; + + public RabbitMqEventHttpPostListener( + IHttpClientFactory httpClientFactory, + ILogger logger, + GlobalSettings globalSettings) + : base(logger, globalSettings) + { + _httpClient = httpClientFactory.CreateClient(HttpClientName); + _httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl; + _queueName = globalSettings.EventLogging.RabbitMq.HttpPostQueueName; + } + + protected override async Task HandleMessageAsync(EventMessage eventMessage) + { + var content = JsonContent.Create(eventMessage); + var response = await _httpClient.PostAsync(_httpPostUrl, content); + response.EnsureSuccessStatusCode(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs new file mode 100644 index 0000000000..48a549d261 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public abstract class RabbitMqEventListenerBase : BackgroundService +{ + private IChannel _channel; + private IConnection _connection; + private readonly string _exchangeName; + private readonly ConnectionFactory _factory; + private readonly ILogger _logger; + + protected abstract string QueueName { get; } + + protected RabbitMqEventListenerBase( + ILogger logger, + GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + _logger = logger; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + _connection = await _factory.CreateConnectionAsync(cancellationToken); + _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + + await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + await _channel.QueueDeclareAsync(queue: QueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: QueueName, + exchange: _exchangeName, + routingKey: string.Empty, + cancellationToken: cancellationToken); + await base.StartAsync(cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += async (_, eventArgs) => + { + try + { + var eventMessage = JsonSerializer.Deserialize(eventArgs.Body.Span); + await HandleMessageAsync(eventMessage); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while processing the message"); + } + }; + + await _channel.BasicConsumeAsync(QueueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(1_000, stoppingToken); + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _channel.CloseAsync(); + await _connection.CloseAsync(); + await base.StopAsync(cancellationToken); + } + + public override void Dispose() + { + _channel.Dispose(); + _connection.Dispose(); + base.Dispose(); + } + + protected abstract Task HandleMessageAsync(EventMessage eventMessage); +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs new file mode 100644 index 0000000000..25d85bddeb --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs @@ -0,0 +1,29 @@ +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class RabbitMqEventRepositoryListener : RabbitMqEventListenerBase +{ + private readonly IEventWriteService _eventWriteService; + private readonly string _queueName; + + protected override string QueueName => _queueName; + + public RabbitMqEventRepositoryListener( + [FromKeyedServices("persistent")] IEventWriteService eventWriteService, + ILogger logger, + GlobalSettings globalSettings) + : base(logger, globalSettings) + { + _eventWriteService = eventWriteService; + _queueName = globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName; + } + + protected override Task HandleMessageAsync(EventMessage eventMessage) + { + return _eventWriteService.CreateAsync(eventMessage); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs new file mode 100644 index 0000000000..d89cf890ac --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; +using RabbitMQ.Client; + +namespace Bit.Core.Services; +public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable +{ + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _exchangeName; + + public RabbitMqEventWriteService(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task CreateAsync(IEvent e) + { + var connection = await _lazyConnection.Value; + using var channel = await connection.CreateChannelAsync(); + + await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + + var body = JsonSerializer.SerializeToUtf8Bytes(e); + + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); + } + + public async Task CreateManyAsync(IEnumerable events) + { + var connection = await _lazyConnection.Value; + using var channel = await connection.CreateChannelAsync(); + await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); + + foreach (var e in events) + { + var body = JsonSerializer.SerializeToUtf8Bytes(e); + + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); + } + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + return await _factory.CreateConnectionAsync(); + } +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 7a5f7e2543..210a33f3f7 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -40,7 +40,7 @@ - + @@ -70,12 +70,13 @@ + - + diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 97d66aed53..718293891b 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings(); public virtual SqlSettings MySql { get; set; } = new SqlSettings(); public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" }; + public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings(); public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); public virtual ConnectionStringSettings Events { get; set; } = new ConnectionStringSettings(); @@ -256,6 +257,44 @@ public class GlobalSettings : IGlobalSettings } } + public class EventLoggingSettings + { + public RabbitMqSettings RabbitMq { get; set; } + + public class RabbitMqSettings + { + private string _hostName; + private string _username; + private string _password; + private string _exchangeName; + + public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; + public virtual string HttpPostQueueName { get; set; } = "events-httpPost-queue"; + public virtual string HttpPostUrl { get; set; } + + public string HostName + { + get => _hostName; + set => _hostName = value.Trim('"'); + } + public string Username + { + get => _username; + set => _username = value.Trim('"'); + } + public string Password + { + get => _password; + set => _password = value.Trim('"'); + } + public string ExchangeName + { + get => _exchangeName; + set => _exchangeName = value.Trim('"'); + } + } + } + public class ConnectionStringSettings : IConnectionStringSettings { private string _connectionString; diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index bac39c68dd..03e99f14e8 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -82,6 +82,22 @@ public class Startup { services.AddHostedService(); } + + // Optional RabbitMQ Listeners + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) + { + services.AddKeyedSingleton("persistent"); + services.AddHostedService(); + + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl)) + { + services.AddHttpClient(RabbitMqEventHttpPostListener.HttpClientName); + services.AddHostedService(); + } + } } public void Configure( diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index e1369d5366..622b3d7f39 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -325,7 +325,17 @@ public static class ServiceCollectionExtensions } else if (globalSettings.SelfHosted) { - services.AddSingleton(); + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } } else { From ab0cab2072d7c925752ee7aa63125cbe2f10ed22 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:59:58 -0500 Subject: [PATCH 05/22] Fix Events Startup (#5352) --- src/Core/Settings/GlobalSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 718293891b..d039102eb9 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -259,7 +259,7 @@ public class GlobalSettings : IGlobalSettings public class EventLoggingSettings { - public RabbitMqSettings RabbitMq { get; set; } + public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); public class RabbitMqSettings { From 148a6311783b2db87eab6781327570d26eec6e5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:59:39 +0100 Subject: [PATCH 06/22] [deps]: Update github/codeql-action action to v3.28.8 (#5292) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/scan.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d64612aba..3b96eeb468 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -314,7 +314,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 156ebee165..ec2eb7789a 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 with: sarif_file: cx_result.sarif From d239170c1ca3996703c59ea3a46cf6315b925a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:01:26 +0000 Subject: [PATCH 07/22] [PM-17697] Save Organization Name changes in Bitwarden Portal (#5337) * Add Org_Name_Edit permission to the Permissions enum * Add Org_Name_Edit permission to RolePermissionMapping * Implement Org_Name_Edit permission check in UpdateOrganization method * Add Org_Name_Edit permission check to Organization form input --- .../AdminConsole/Controllers/OrganizationsController.cs | 5 +++++ src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml | 3 ++- src/Admin/Enums/Permissions.cs | 1 + src/Admin/Utilities/RolePermissionMapping.cs | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 3fdef169b4..60a5a39612 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -421,6 +421,11 @@ public class OrganizationsController : Controller private void UpdateOrganization(Organization organization, OrganizationEditModel model) { + if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit)) + { + organization.Name = WebUtility.HtmlEncode(model.Name); + } + if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox)) { organization.Enabled = model.Enabled; diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index cdc7608675..aeff65c900 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -12,6 +12,7 @@ var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View); var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View); var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View); + var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit); var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox); var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit); var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit); @@ -28,7 +29,7 @@
- +
diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 20c500c061..4edcd742b4 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -22,6 +22,7 @@ public enum Permission Org_List_View, Org_OrgInformation_View, Org_GeneralDetails_View, + Org_Name_Edit, Org_CheckEnabledBox, Org_BusinessInformation_View, Org_InitiateTrial, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 4b5a4e3802..3b510781be 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -24,6 +24,7 @@ public static class RolePermissionMapping Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -71,6 +72,7 @@ public static class RolePermissionMapping Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -116,6 +118,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_LaunchGateway, Permission.User_NewDeviceException_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -148,6 +151,7 @@ public static class RolePermissionMapping Permission.User_Billing_View, Permission.User_Billing_Edit, Permission.User_Billing_LaunchGateway, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, @@ -185,6 +189,7 @@ public static class RolePermissionMapping Permission.User_Premium_View, Permission.User_Licensing_View, Permission.User_Licensing_Edit, + Permission.Org_Name_Edit, Permission.Org_CheckEnabledBox, Permission.Org_List_View, Permission.Org_OrgInformation_View, From e43a8011f10d94d9ca3271fe2a21e703ce6023d1 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:46:09 -0500 Subject: [PATCH 08/22] [PM-17709] Send New Device Login email for all new devices (#5340) * Send New Device Login email regardless of New Device Verification * Adjusted tests * Linting * Clarified test names. --- .../RequestValidators/DeviceValidator.cs | 34 ++++++++++--------- .../IdentityServer/DeviceValidatorTests.cs | 12 ++----- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 1b148c5974..fee10e10ff 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -85,28 +85,17 @@ public class DeviceValidator( } } - // At this point we have established either new device verification is not required or the NewDeviceOtp is valid + // At this point we have established either new device verification is not required or the NewDeviceOtp is valid, + // so we save the device to the database and proceed with authentication requestDevice.UserId = context.User.Id; await _deviceService.SaveAsync(requestDevice); context.Device = requestDevice; - // backwards compatibility -- If NewDeviceVerification not enabled send the new login emails - // PM-13340: removal Task; remove entire if block emails should no longer be sent - if (!_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification)) + if (!_globalSettings.DisableEmailNewDevice) { - // This ensures the user doesn't receive a "new device" email on the first login - var now = DateTime.UtcNow; - if (now - context.User.CreationDate > TimeSpan.FromMinutes(10)) - { - var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName(); - if (!_globalSettings.DisableEmailNewDevice) - { - await _mailService.SendNewDeviceLoggedInEmail(context.User.Email, deviceType, now, - _currentContext.IpAddress); - } - } + await SendNewDeviceLoginEmail(context.User, requestDevice); } + return true; } @@ -174,6 +163,19 @@ public class DeviceValidator( return DeviceValidationResultType.NewDeviceVerificationRequired; } + private async Task SendNewDeviceLoginEmail(User user, Device requestDevice) + { + // Ensure that the user doesn't receive a "new device" email on the first login + var now = DateTime.UtcNow; + if (now - user.CreationDate > TimeSpan.FromMinutes(10)) + { + var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName(); + await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, + _currentContext.IpAddress); + } + } + public async Task GetKnownDeviceAsync(User user, Device device) { if (user == null || device == null) diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index fa3a117c55..6e6406f16b 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -227,7 +227,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_SendsEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsync_ExistingUserNewDeviceLogin_SendNewDeviceLoginEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -237,8 +237,6 @@ public class DeviceValidatorTests _globalSettings.DisableEmailNewDevice = false; _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(false); // set user creation to more than 10 minutes ago context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11); @@ -253,7 +251,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_NewUser_DoesNotSendEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsync_NewUserNewDeviceLogin_DoesNotSendNewDeviceLoginEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -263,8 +261,6 @@ public class DeviceValidatorTests _globalSettings.DisableEmailNewDevice = false; _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(false); // set user creation to less than 10 minutes ago context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(9); @@ -279,7 +275,7 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_NewDeviceVerificationFeatureFlagFalse_DisableEmailTrue_DoesNotSendEmail_ReturnsTrue( + public async void ValidateRequestDeviceAsynce_DisableNewDeviceLoginEmailTrue_DoesNotSendNewDeviceEmail_ReturnsTrue( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { @@ -289,8 +285,6 @@ public class DeviceValidatorTests _globalSettings.DisableEmailNewDevice = true; _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(false); // Act var result = await _sut.ValidateRequestDeviceAsync(request, context); From bd394eabe9c887e43893a961e191e6c0e11e1d08 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 31 Jan 2025 10:50:14 -0500 Subject: [PATCH 09/22] [pm-16528] Fix entity framework query (#5333) --- .../OrganizationDomainRepository.cs | 28 ++--- .../OrganizationDomainRepositoryTests.cs | 118 ++++++++++++++++++ 2 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index e339c13351..50d791b81b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -46,27 +46,17 @@ public class OrganizationDomainRepository : Repository x.VerifiedDate == null - && x.JobRunCount != 3 - && x.NextRunDate.Year == date.Year - && x.NextRunDate.Month == date.Month - && x.NextRunDate.Day == date.Day - && x.NextRunDate.Hour == date.Hour) - .AsNoTracking() + var start36HoursWindow = date.AddHours(-36); + var end36HoursWindow = date; + + var pastDomains = await dbContext.OrganizationDomains + .Where(x => x.NextRunDate >= start36HoursWindow + && x.NextRunDate <= end36HoursWindow + && x.VerifiedDate == null + && x.JobRunCount != 3) .ToListAsync(); - //Get records that have ignored/failed by the background service - var pastDomains = dbContext.OrganizationDomains - .AsEnumerable() - .Where(x => (date - x.NextRunDate).TotalHours > 36 - && x.VerifiedDate == null - && x.JobRunCount != 3) - .ToList(); - - var results = domains.Union(pastDomains); - - return Mapper.Map>(results); + return Mapper.Map>(pastDomains); } public async Task GetOrganizationDomainSsoDetailsAsync(string email) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index 8e0b502a47..ad92f40efc 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -188,4 +188,122 @@ public class OrganizationDomainRepositoryTests var expectedDomain2 = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain2.DomainName); Assert.Null(expectedDomain2); } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var within36HoursWindow = 1; + organizationDomain.SetNextRunDate(within36HoursWindow); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = organizationDomain.NextRunDate; + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.NotNull(expectedDomain); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var outside36HoursWindow = 20; + organizationDomain.SetNextRunDate(outside36HoursWindow); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = DateTimeOffset.UtcNow.Date.AddDays(1); + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.Null(expectedDomain); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var id = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = $"test+{id}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{id}@example.com", + Txt = "btw+12345" + }; + + var within36HoursWindow = 1; + organizationDomain.SetNextRunDate(within36HoursWindow); + organizationDomain.SetVerifiedDate(); + + await organizationDomainRepository.CreateAsync(organizationDomain); + + var date = DateTimeOffset.UtcNow.Date.AddDays(1); + + // Act + var domains = await organizationDomainRepository.GetManyByNextRunDateAsync(date); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); + Assert.Null(expectedDomain); + } } From 408ddd938893448cfce270ef0754168d4d65e037 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:08:07 -0500 Subject: [PATCH 10/22] Scaffold Events Integration Tests (#5355) * Scaffold Events Integration Tests * Format --- bitwarden-server.sln | 7 ++ .../Controllers/CollectControllerTests.cs | 29 ++++++++ .../Events.IntegrationTest.csproj | 29 ++++++++ .../EventsApplicationFactory.cs | 57 +++++++++++++++ test/Events.IntegrationTest/GlobalUsings.cs | 1 + .../Factories/WebApplicationFactoryBase.cs | 72 ++++++++++--------- 6 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 test/Events.IntegrationTest/Controllers/CollectControllerTests.cs create mode 100644 test/Events.IntegrationTest/Events.IntegrationTest.csproj create mode 100644 test/Events.IntegrationTest/EventsApplicationFactory.cs create mode 100644 test/Events.IntegrationTest/GlobalUsings.cs diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 75e7d7fade..e9aff53f8e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -313,6 +315,10 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -363,6 +369,7 @@ Global {81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs new file mode 100644 index 0000000000..7f86758144 --- /dev/null +++ b/test/Events.IntegrationTest/Controllers/CollectControllerTests.cs @@ -0,0 +1,29 @@ +using System.Net.Http.Json; +using Bit.Core.Enums; +using Bit.Events.Models; + +namespace Bit.Events.IntegrationTest.Controllers; + +public class CollectControllerTests +{ + // This is a very simple test, and should be updated to assert more things, but for now + // it ensures that the events startup doesn't throw any errors with fairly basic configuration. + [Fact] + public async Task Post_Works() + { + var eventsApplicationFactory = new EventsApplicationFactory(); + var (accessToken, _) = await eventsApplicationFactory.LoginWithNewAccount(); + var client = eventsApplicationFactory.CreateAuthedClient(accessToken); + + var response = await client.PostAsJsonAsync>("collect", + [ + new EventModel + { + Type = EventType.User_ClientExportedVault, + Date = DateTime.UtcNow, + }, + ]); + + response.EnsureSuccessStatusCode(); + } +} diff --git a/test/Events.IntegrationTest/Events.IntegrationTest.csproj b/test/Events.IntegrationTest/Events.IntegrationTest.csproj new file mode 100644 index 0000000000..0b51185298 --- /dev/null +++ b/test/Events.IntegrationTest/Events.IntegrationTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs new file mode 100644 index 0000000000..3faf5e81bf --- /dev/null +++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs @@ -0,0 +1,57 @@ +using Bit.Identity.Models.Request.Accounts; +using Bit.IntegrationTestCommon.Factories; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Events.IntegrationTest; + +public class EventsApplicationFactory : WebApplicationFactoryBase +{ + private readonly IdentityApplicationFactory _identityApplicationFactory; + private const string _connectionString = "DataSource=:memory:"; + + public EventsApplicationFactory() + { + SqliteConnection = new SqliteConnection(_connectionString); + SqliteConnection.Open(); + + _identityApplicationFactory = new IdentityApplicationFactory(); + _identityApplicationFactory.SqliteConnection = SqliteConnection; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + + builder.ConfigureTestServices(services => + { + services.Configure(JwtBearerDefaults.AuthenticationScheme, options => + { + options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler(); + }); + }); + } + + /// + /// Helper for registering and logging in to a new account + /// + public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") + { + await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + }); + + return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + SqliteConnection!.Dispose(); + } +} diff --git a/test/Events.IntegrationTest/GlobalUsings.cs b/test/Events.IntegrationTest/GlobalUsings.cs new file mode 100644 index 0000000000..9df1d42179 --- /dev/null +++ b/test/Events.IntegrationTest/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index d01e92ad4c..7c7f790cdc 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -14,6 +14,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; @@ -188,44 +189,27 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // QUESTION: The normal licensing service should run fine on developer machines but not in CI // should we have a fork here to leave the normal service for developers? // TODO: Eventually add the license file to CI - var licensingService = services.First(sd => sd.ServiceType == typeof(ILicensingService)); - services.Remove(licensingService); - services.AddSingleton(); + Replace(services); // FUTURE CONSIDERATION: Add way to run this self hosted/cloud, for now it is cloud only - var pushRegistrationService = services.First(sd => sd.ServiceType == typeof(IPushRegistrationService)); - services.Remove(pushRegistrationService); - services.AddSingleton(); + Replace(services); // Even though we are cloud we currently set this up as cloud, we can use the EF/selfhosted service // instead of using Noop for this service // TODO: Install and use azurite in CI pipeline - var eventWriteService = services.First(sd => sd.ServiceType == typeof(IEventWriteService)); - services.Remove(eventWriteService); - services.AddSingleton(); + Replace(services); - var eventRepositoryService = services.First(sd => sd.ServiceType == typeof(IEventRepository)); - services.Remove(eventRepositoryService); - services.AddSingleton(); + Replace(services); - var mailDeliveryService = services.First(sd => sd.ServiceType == typeof(IMailDeliveryService)); - services.Remove(mailDeliveryService); - services.AddSingleton(); + Replace(services); - var captchaValidationService = services.First(sd => sd.ServiceType == typeof(ICaptchaValidationService)); - services.Remove(captchaValidationService); - services.AddSingleton(); + Replace(services); // TODO: Install and use azurite in CI pipeline - var installationDeviceRepository = - services.First(sd => sd.ServiceType == typeof(IInstallationDeviceRepository)); - services.Remove(installationDeviceRepository); - services.AddSingleton(); + Replace(services); // TODO: Install and use azurite in CI pipeline - var referenceEventService = services.First(sd => sd.ServiceType == typeof(IReferenceEventService)); - services.Remove(referenceEventService); - services.AddSingleton(); + Replace(services); // Our Rate limiter works so well that it begins to fail tests unless we carve out // one whitelisted ip. We should still test the rate limiter though and they should change the Ip @@ -245,14 +229,9 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory services.AddSingleton(); // Noop StripePaymentService - this could be changed to integrate with our Stripe test account - var stripePaymentService = services.First(sd => sd.ServiceType == typeof(IPaymentService)); - services.Remove(stripePaymentService); - services.AddSingleton(Substitute.For()); + Replace(services, Substitute.For()); - var organizationBillingService = - services.First(sd => sd.ServiceType == typeof(IOrganizationBillingService)); - services.Remove(organizationBillingService); - services.AddSingleton(Substitute.For()); + Replace(services, Substitute.For()); }); foreach (var configureTestService in _configureTestServices) @@ -261,6 +240,35 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory } } + private static void Replace(IServiceCollection services) + where TService : class + where TNewImplementation : class, TService + { + services.RemoveAll(); + services.AddSingleton(); + } + + private static void Replace(IServiceCollection services, TService implementation) + where TService : class + { + services.RemoveAll(); + services.AddSingleton(implementation); + } + + public HttpClient CreateAuthedClient(string accessToken) + { + var handler = Server.CreateHandler((context) => + { + context.Request.Headers.Authorization = $"Bearer {accessToken}"; + }); + + return new HttpClient(handler) + { + BaseAddress = Server.BaseAddress, + Timeout = TimeSpan.FromSeconds(200), + }; + } + public DatabaseContext GetDatabaseContext() { var scope = Services.CreateScope(); From 669c253bc62639deffd08076843873030d66e223 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:18:10 -0600 Subject: [PATCH 11/22] chore: add limit item deletion feature flag constant, refs PM-17214 (#5356) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6d70f0b3ce..5643ed7654 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -107,6 +107,7 @@ public static class FeatureFlagKeys public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string IntegrationPage = "pm-14505-admin-console-integration-page"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; + public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; From 1adc5358a871885483296e2a997a147ab17fd84d Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Mon, 3 Feb 2025 09:35:38 -0500 Subject: [PATCH 12/22] Create a single feature flag for the Authenticator sync (#5353) * Create a single feature flag for the Authenticator sync * Update feature flag key --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5643ed7654..b196306409 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -171,6 +171,7 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; + public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public static List GetAllKeys() { From fe983aff7f43b076f9c1affeb654eaa9951d2fff Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 3 Feb 2025 12:35:46 -0500 Subject: [PATCH 13/22] [pm-17911] Refresh OrganizationView (#5360) --- .../2025-02-03_01_RefreshView_For_LimitItemDeletion.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql diff --git a/util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql b/util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql new file mode 100644 index 0000000000..98893bb030 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-03_01_RefreshView_For_LimitItemDeletion.sql @@ -0,0 +1,7 @@ +-- Refresh Views + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL + BEGIN + EXECUTE sp_refreshview N'[dbo].[OrganizationView]'; + END +GO From 060e9e60bff549b65cef12586c4fab0ec598a7fe Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 3 Feb 2025 14:55:57 -0500 Subject: [PATCH 14/22] [pm-337] Remove the continuation token from the ListResponseModel. (#5192) --- .../Public/Controllers/EventsController.cs | 4 ++-- src/Api/Models/Public/Response/ListResponseModel.cs | 7 +------ .../Models/Public/Response/PagedListResponseModel.cs | 10 ++++++++++ 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 src/Api/Models/Public/Response/PagedListResponseModel.cs diff --git a/src/Api/AdminConsole/Public/Controllers/EventsController.cs b/src/Api/AdminConsole/Public/Controllers/EventsController.cs index d2e198de17..992b7453aa 100644 --- a/src/Api/AdminConsole/Public/Controllers/EventsController.cs +++ b/src/Api/AdminConsole/Public/Controllers/EventsController.cs @@ -36,7 +36,7 @@ public class EventsController : Controller /// If no filters are provided, it will return the last 30 days of event for the organization. /// [HttpGet] - [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(PagedListResponseModel), (int)HttpStatusCode.OK)] public async Task List([FromQuery] EventFilterRequestModel request) { var dateRange = request.ToDateRange(); @@ -65,7 +65,7 @@ public class EventsController : Controller } var eventResponses = result.Data.Select(e => new EventResponseModel(e)); - var response = new ListResponseModel(eventResponses, result.ContinuationToken); + var response = new PagedListResponseModel(eventResponses, result.ContinuationToken); return new JsonResult(response); } } diff --git a/src/Api/Models/Public/Response/ListResponseModel.cs b/src/Api/Models/Public/Response/ListResponseModel.cs index 0865be3e8e..a55d6f62bb 100644 --- a/src/Api/Models/Public/Response/ListResponseModel.cs +++ b/src/Api/Models/Public/Response/ListResponseModel.cs @@ -4,10 +4,9 @@ namespace Bit.Api.Models.Public.Response; public class ListResponseModel : IResponseModel where T : IResponseModel { - public ListResponseModel(IEnumerable data, string continuationToken = null) + public ListResponseModel(IEnumerable data) { Data = data; - ContinuationToken = continuationToken; } /// @@ -21,8 +20,4 @@ public class ListResponseModel : IResponseModel where T : IResponseModel /// [Required] public IEnumerable Data { get; set; } - /// - /// A cursor for use in pagination. - /// - public string ContinuationToken { get; set; } } diff --git a/src/Api/Models/Public/Response/PagedListResponseModel.cs b/src/Api/Models/Public/Response/PagedListResponseModel.cs new file mode 100644 index 0000000000..b0f25cb4f8 --- /dev/null +++ b/src/Api/Models/Public/Response/PagedListResponseModel.cs @@ -0,0 +1,10 @@ +namespace Bit.Api.Models.Public.Response; + +public class PagedListResponseModel(IEnumerable data, string continuationToken) : ListResponseModel(data) + where T : IResponseModel +{ + /// + /// A cursor for use in pagination. + /// + public string ContinuationToken { get; set; } = continuationToken; +} From 90f308db34a6ce79967f3a93173bd32137125660 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:09:09 +0100 Subject: [PATCH 15/22] [deps] Tools: Update aws-sdk-net monorepo (#5278) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 210a33f3f7..7b319e56c9 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From f1b9bd9a099d6d542eb367597c7ef99e9842c233 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:02:18 -0500 Subject: [PATCH 16/22] [PM-15179] Implement endpoints to add existing organization to CB provider (#5310) * Implement endpoints to add existing organization to provider * Run dotnet format * Support MOE * Run dotnet format * Move ProviderClientsController under AC ownership * Move ProviderClientsControllerTests under AC ownership * Jared's feedback --- .../Billing/ProviderBillingService.cs | 179 ++++++++++++++++++ .../Controllers/ProviderClientsController.cs | 68 ++++++- .../AddExistingOrganizationRequestBody.cs | 12 ++ .../Repositories/IOrganizationRepository.cs | 2 + src/Core/Billing/Constants/PlanConstants.cs | 30 +++ src/Core/Billing/Constants/StripeConstants.cs | 10 + .../Billing/Models/AddableOrganization.cs | 8 + .../Services/IProviderBillingService.cs | 10 + src/Core/Constants.cs | 1 + .../Repositories/OrganizationRepository.cs | 16 ++ .../Repositories/OrganizationRepository.cs | 38 ++++ ...nization_ReadAddableToProviderByUserId.sql | 23 +++ .../ProviderClientsControllerTests.cs | 5 +- ...nization_ReadAddableToProviderByUserId.sql | 31 +++ 14 files changed, 427 insertions(+), 6 deletions(-) rename src/Api/{Billing => AdminConsole}/Controllers/ProviderClientsController.cs (67%) create mode 100644 src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs create mode 100644 src/Core/Billing/Constants/PlanConstants.cs create mode 100644 src/Core/Billing/Models/AddableOrganization.cs create mode 100644 src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql rename test/Api.Test/{Billing => AdminConsole}/Controllers/ProviderClientsControllerTests.cs (98%) create mode 100644 util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 2b834947af..abba8aff90 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -1,12 +1,15 @@ using System.Globalization; using Bit.Commercial.Core.Billing.Models; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; @@ -24,6 +27,7 @@ using Stripe; namespace Bit.Commercial.Core.Billing; public class ProviderBillingService( + IEventService eventService, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -31,10 +35,93 @@ public class ProviderBillingService( IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, + IProviderUserRepository providerUserRepository, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, ITaxService taxService) : IProviderBillingService { + [RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)] + public async Task AddExistingOrganization( + Provider provider, + Organization organization, + string key) + { + await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + CancelAtPeriodEnd = false + }); + + var subscription = + await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId, + new SubscriptionCancelOptions + { + CancellationDetails = new SubscriptionCancellationDetailsOptions + { + Comment = $"Organization was added to Provider with ID {provider.Id}" + }, + InvoiceNow = true, + Prorate = true, + Expand = ["latest_invoice", "test_clock"] + }); + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now; + + if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft) + { + await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId, + new InvoiceFinalizeOptions { AutoAdvance = true }); + } + + var managedPlanType = await GetManagedPlanTypeAsync(provider, organization); + + // TODO: Replace with PricingClient + var plan = StaticStore.GetPlan(managedPlanType); + organization.Plan = plan.Name; + organization.PlanType = plan.Type; + organization.MaxCollections = plan.PasswordManager.MaxCollections; + organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; + organization.UsePolicies = plan.HasPolicies; + organization.UseSso = plan.HasSso; + organization.UseGroups = plan.HasGroups; + organization.UseEvents = plan.HasEvents; + organization.UseDirectory = plan.HasDirectory; + organization.UseTotp = plan.HasTotp; + organization.Use2fa = plan.Has2fa; + organization.UseApi = plan.HasApi; + organization.UseResetPassword = plan.HasResetPassword; + organization.SelfHost = plan.HasSelfHost; + organization.UsersGetPremium = plan.UsersGetPremium; + organization.UseCustomPermissions = plan.HasCustomPermissions; + organization.UseScim = plan.HasScim; + organization.UseKeyConnector = plan.HasKeyConnector; + organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; + organization.BillingEmail = provider.BillingEmail!; + organization.GatewaySubscriptionId = null; + organization.ExpirationDate = null; + organization.MaxAutoscaleSeats = null; + organization.Status = OrganizationStatusType.Managed; + + var providerOrganization = new ProviderOrganization + { + ProviderId = provider.Id, + OrganizationId = organization.Id, + Key = key + }; + + await Task.WhenAll( + organizationRepository.ReplaceAsync(organization), + providerOrganizationRepository.CreateAsync(providerOrganization), + ScaleSeats(provider, organization.PlanType, organization.Seats!.Value) + ); + + await eventService.LogProviderOrganizationEventAsync( + providerOrganization, + EventType.ProviderOrganization_Added); + } + public async Task ChangePlan(ChangeProviderPlanCommand command) { var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId); @@ -206,6 +293,81 @@ public class ProviderBillingService( return memoryStream.ToArray(); } + [RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)] + public async Task> GetAddableOrganizations( + Provider provider, + Guid userId) + { + var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, userId); + + if (providerUser is not { Status: ProviderUserStatusType.Confirmed }) + { + throw new UnauthorizedAccessException(); + } + + var candidates = await organizationRepository.GetAddableToProviderByUserIdAsync(userId, provider.Type); + + var active = (await Task.WhenAll(candidates.Select(async organization => + { + var subscription = await subscriberService.GetSubscription(organization); + return (organization, subscription); + }))) + .Where(pair => pair.subscription is + { + Status: + StripeConstants.SubscriptionStatus.Active or + StripeConstants.SubscriptionStatus.Trialing or + StripeConstants.SubscriptionStatus.PastDue + }).ToList(); + + if (active.Count == 0) + { + return []; + } + + return await Task.WhenAll(active.Select(async pair => + { + var (organization, _) = pair; + + var planName = DerivePlanName(provider, organization); + + var addable = new AddableOrganization( + organization.Id, + organization.Name, + planName, + organization.Seats!.Value); + + if (providerUser.Type != ProviderUserType.ServiceUser) + { + return addable; + } + + var applicablePlanType = await GetManagedPlanTypeAsync(provider, organization); + + var requiresPurchase = + await SeatAdjustmentResultsInPurchase(provider, applicablePlanType, organization.Seats!.Value); + + return addable with { Disabled = requiresPurchase }; + })); + + string DerivePlanName(Provider localProvider, Organization localOrganization) + { + if (localProvider.Type == ProviderType.Msp) + { + return localOrganization.PlanType switch + { + var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => "Enterprise", + var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => "Teams", + _ => throw new BillingException() + }; + } + + // TODO: Replace with PricingClient + var plan = StaticStore.GetPlan(localOrganization.PlanType); + return plan.Name; + } + } + public async Task ScaleSeats( Provider provider, PlanType planType, @@ -582,4 +744,21 @@ public class ProviderBillingService( return providerPlan; } + + private async Task GetManagedPlanTypeAsync( + Provider provider, + Organization organization) + { + if (provider.Type == ProviderType.MultiOrganizationEnterprise) + { + return (await providerPlanRepository.GetByProviderId(provider.Id)).First().PlanType; + } + + return organization.PlanType switch + { + var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => PlanType.TeamsMonthly, + var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => PlanType.EnterpriseMonthly, + _ => throw new BillingException() + }; + } } diff --git a/src/Api/Billing/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs similarity index 67% rename from src/Api/Billing/Controllers/ProviderClientsController.cs rename to src/Api/AdminConsole/Controllers/ProviderClientsController.cs index 0c09fa7baf..38d8b254d7 100644 --- a/src/Api/Billing/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -1,4 +1,6 @@ -using Bit.Api.Billing.Models.Requests; +using Bit.Api.Billing.Controllers; +using Bit.Api.Billing.Models.Requests; +using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Services; @@ -7,13 +9,15 @@ using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.Billing.Controllers; +namespace Bit.Api.AdminConsole.Controllers; [Route("providers/{providerId:guid}/clients")] public class ProviderClientsController( ICurrentContext currentContext, + IFeatureService featureService, ILogger logger, IOrganizationRepository organizationRepository, IProviderBillingService providerBillingService, @@ -22,7 +26,10 @@ public class ProviderClientsController( IProviderService providerService, IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService) { + private readonly ICurrentContext _currentContext = currentContext; + [HttpPost] + [SelfHosted(NotSelfHostedOnly = true)] public async Task CreateAsync( [FromRoute] Guid providerId, [FromBody] CreateClientOrganizationRequestBody requestBody) @@ -80,6 +87,7 @@ public class ProviderClientsController( } [HttpPut("{providerOrganizationId:guid}")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task UpdateAsync( [FromRoute] Guid providerId, [FromRoute] Guid providerOrganizationId, @@ -113,7 +121,7 @@ public class ProviderClientsController( clientOrganization.PlanType, seatAdjustment); - if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id)) + if (seatAdjustmentResultsInPurchase && !_currentContext.ProviderProviderAdmin(provider.Id)) { return Error.Unauthorized("Service users cannot purchase additional seats."); } @@ -127,4 +135,58 @@ public class ProviderClientsController( return TypedResults.Ok(); } + + [HttpGet("addable")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task GetAddableOrganizationsAsync([FromRoute] Guid providerId) + { + if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)) + { + return Error.NotFound(); + } + + var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); + + if (provider == null) + { + return result; + } + + var userId = _currentContext.UserId; + + if (!userId.HasValue) + { + return Error.Unauthorized(); + } + + var addable = + await providerBillingService.GetAddableOrganizations(provider, userId.Value); + + return TypedResults.Ok(addable); + } + + [HttpPost("existing")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task AddExistingOrganizationAsync( + [FromRoute] Guid providerId, + [FromBody] AddExistingOrganizationRequestBody requestBody) + { + var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); + + if (provider == null) + { + return result; + } + + var organization = await organizationRepository.GetByIdAsync(requestBody.OrganizationId); + + if (organization == null) + { + return Error.BadRequest("The organization being added to the provider does not exist."); + } + + await providerBillingService.AddExistingOrganization(provider, organization, requestBody.Key); + + return TypedResults.Ok(); + } } diff --git a/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs b/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs new file mode 100644 index 0000000000..c2add17793 --- /dev/null +++ b/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Models.Requests; + +public class AddExistingOrganizationRequestBody +{ + [Required(ErrorMessage = "'key' must be provided")] + public string Key { get; set; } + + [Required(ErrorMessage = "'organizationId' must be provided")] + public Guid OrganizationId { get; set; } +} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 5b274d3f88..584d95ffe2 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Models.Data.Organizations; #nullable enable @@ -22,4 +23,5 @@ public interface IOrganizationRepository : IRepository /// Gets the organizations that have a verified domain matching the user's email domain. /// Task> GetByVerifiedUserEmailDomainAsync(Guid userId); + Task> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType); } diff --git a/src/Core/Billing/Constants/PlanConstants.cs b/src/Core/Billing/Constants/PlanConstants.cs new file mode 100644 index 0000000000..1ac5b8e750 --- /dev/null +++ b/src/Core/Billing/Constants/PlanConstants.cs @@ -0,0 +1,30 @@ +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Constants; + +public static class PlanConstants +{ + public static List EnterprisePlanTypes => + [ + PlanType.EnterpriseAnnually2019, + PlanType.EnterpriseAnnually2020, + PlanType.EnterpriseAnnually2023, + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly2019, + PlanType.EnterpriseMonthly2020, + PlanType.EnterpriseMonthly2023, + PlanType.EnterpriseMonthly + ]; + + public static List TeamsPlanTypes => + [ + PlanType.TeamsAnnually2019, + PlanType.TeamsAnnually2020, + PlanType.TeamsAnnually2023, + PlanType.TeamsAnnually, + PlanType.TeamsMonthly2019, + PlanType.TeamsMonthly2020, + PlanType.TeamsMonthly2023, + PlanType.TeamsMonthly + ]; +} diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 7371b8b7e9..e3c2b7245e 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -31,6 +31,16 @@ public static class StripeConstants public const string TaxIdInvalid = "tax_id_invalid"; } + public static class InvoiceStatus + { + public const string Draft = "draft"; + } + + public static class MetadataKeys + { + public const string OrganizationId = "organizationId"; + } + public static class PaymentBehavior { public const string DefaultIncomplete = "default_incomplete"; diff --git a/src/Core/Billing/Models/AddableOrganization.cs b/src/Core/Billing/Models/AddableOrganization.cs new file mode 100644 index 0000000000..fe6d5458bd --- /dev/null +++ b/src/Core/Billing/Models/AddableOrganization.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Billing.Models; + +public record AddableOrganization( + Guid Id, + string Name, + string Plan, + int Seats, + bool Disabled = false); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 20e7407628..d6983da03e 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Services.Contracts; using Bit.Core.Models.Business; using Stripe; @@ -10,6 +11,11 @@ namespace Bit.Core.Billing.Services; public interface IProviderBillingService { + Task AddExistingOrganization( + Provider provider, + Organization organization, + string key); + /// /// Changes the assigned provider plan for the provider. /// @@ -35,6 +41,10 @@ public interface IProviderBillingService Task GenerateClientInvoiceReport( string invoiceId); + Task> GetAddableOrganizations( + Provider provider, + Guid userId); + /// /// Scales the 's seats for the specified using the provided . /// This operation may autoscale the provider's Stripe depending on the 's seat minimum for the diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b196306409..8660010871 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -172,6 +172,7 @@ public static class FeatureFlagKeys public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; + public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal"; public static List GetAllKeys() { diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 20fdf83155..f624f7da28 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -1,5 +1,6 @@ using System.Data; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Entities; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -180,4 +181,19 @@ public class OrganizationRepository : Repository, IOrganizat return result.ToList(); } } + + public async Task> GetAddableToProviderByUserIdAsync( + Guid userId, + ProviderType providerType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadAddableToProviderByUserId]", + new { UserId = userId, ProviderType = providerType }, + commandType: CommandType.StoredProcedure); + + return result.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index c1c78eee60..b6ec2ddca0 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -1,9 +1,12 @@ using AutoMapper; using AutoMapper.QueryableExtensions; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; +using LinqToDB.Tools; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -298,6 +301,41 @@ public class OrganizationRepository : Repository> GetAddableToProviderByUserIdAsync( + Guid userId, + ProviderType providerType) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var planTypes = providerType switch + { + ProviderType.Msp => PlanConstants.EnterprisePlanTypes.Concat(PlanConstants.TeamsPlanTypes), + ProviderType.MultiOrganizationEnterprise => PlanConstants.EnterprisePlanTypes, + _ => [] + }; + + var query = + from organizationUser in dbContext.OrganizationUsers + join organization in dbContext.Organizations on organizationUser.OrganizationId equals organization.Id + where + organizationUser.UserId == userId && + organizationUser.Type == OrganizationUserType.Owner && + organizationUser.Status == OrganizationUserStatusType.Confirmed && + organization.Enabled && + organization.GatewayCustomerId != null && + organization.GatewaySubscriptionId != null && + organization.Seats > 0 && + organization.Status == OrganizationStatusType.Created && + !organization.UseSecretsManager && + organization.PlanType.In(planTypes) + select organization; + + return await query.ToArrayAsync(); + } + } + public Task EnableCollectionEnhancements(Guid organizationId) { throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework."); diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql new file mode 100644 index 0000000000..e11109ae10 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId] + @UserId UNIQUEIDENTIFIER, + @ProviderType TINYINT +AS +BEGIN + SET NOCOUNT ON + SELECT O.* FROM [dbo].[OrganizationUser] AS OU + JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId] + WHERE + OU.[UserId] = @UserId AND + OU.[Type] = 0 AND + OU.[Status] = 2 AND + O.[Enabled] = 1 AND + O.[GatewayCustomerId] IS NOT NULL AND + O.[GatewaySubscriptionId] IS NOT NULL AND + O.[Seats] > 0 AND + O.[Status] = 1 AND + O.[UseSecretsManager] = 0 AND + -- All Teams & Enterprise for MSP + (@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR + -- All Enterprise for MOE + @ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20)); +END diff --git a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs similarity index 98% rename from test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs rename to test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index 86bacd9aa3..8ddd92a5fa 100644 --- a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Bit.Api.Billing.Controllers; +using Bit.Api.AdminConsole.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; @@ -19,10 +19,9 @@ using Microsoft.AspNetCore.Http.HttpResults; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; - using static Bit.Api.Test.Billing.Utilities; -namespace Bit.Api.Test.Billing.Controllers; +namespace Bit.Api.Test.AdminConsole.Controllers; [ControllerCustomize(typeof(ProviderClientsController))] [SutProviderCustomize] diff --git a/util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql b/util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql new file mode 100644 index 0000000000..1255544d19 --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-28_00_Add_Organization_ReadAddableToProviderByUserId.sql @@ -0,0 +1,31 @@ +-- Drop existing SPROC +IF OBJECT_ID('[dbo].[Organization_ReadAddableToProviderByUserId') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId] +END +GO + +CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId] + @UserId UNIQUEIDENTIFIER, + @ProviderType TINYINT +AS +BEGIN + SET NOCOUNT ON + SELECT O.* FROM [dbo].[OrganizationUser] AS OU + JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId] + WHERE + OU.[UserId] = @UserId AND + OU.[Type] = 0 AND + OU.[Status] = 2 AND + O.[Enabled] = 1 AND + O.[GatewayCustomerId] IS NOT NULL AND + O.[GatewaySubscriptionId] IS NOT NULL AND + O.[Seats] > 0 AND + O.[Status] = 1 AND + O.[UseSecretsManager] = 0 AND + -- All Teams & Enterprise for MSP + (@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR + -- All Enterprise for MOE + @ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20)); +END +GO From 3f3da558b6c47a91c09f32e3535451d58f4858ee Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:02:43 -0600 Subject: [PATCH 17/22] [PM-17562] Refactor existing RabbitMq implementation (#5357) * [PM-17562] Refactor existing RabbitMq implementation * Fixed issues noted in PR review --- .../Services/IEventMessageHandler.cs | 8 +++ .../Implementations/EventRepositoryHandler.cs | 14 ++++ ...ostListener.cs => HttpPostEventHandler.cs} | 15 ++--- .../RabbitMqEventRepositoryListener.cs | 29 -------- .../Services/EventLoggingListenerService.cs | 13 ++++ .../RabbitMqEventListenerService.cs} | 27 ++++---- src/Core/Settings/IGlobalSettings.cs | 1 + src/Events/Startup.cs | 19 +++++- .../MockedHttpMessageHandler.cs | 3 + .../Services/EventRepositoryHandlerTests.cs | 24 +++++++ .../Services/HttpPostEventHandlerTests.cs | 66 +++++++++++++++++++ 11 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 src/Core/AdminConsole/Services/IEventMessageHandler.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs rename src/Core/AdminConsole/Services/Implementations/{RabbitMqEventHttpPostListener.cs => HttpPostEventHandler.cs} (52%) delete mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs create mode 100644 src/Core/Services/EventLoggingListenerService.cs rename src/Core/{AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs => Services/Implementations/RabbitMqEventListenerService.cs} (78%) create mode 100644 test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/AdminConsole/Services/IEventMessageHandler.cs new file mode 100644 index 0000000000..5df9544c29 --- /dev/null +++ b/src/Core/AdminConsole/Services/IEventMessageHandler.cs @@ -0,0 +1,8 @@ +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; + +public interface IEventMessageHandler +{ + Task HandleEventAsync(EventMessage eventMessage); +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs new file mode 100644 index 0000000000..6e4158122c --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Services; + +public class EventRepositoryHandler( + [FromKeyedServices("persistent")] IEventWriteService eventWriteService) + : IEventMessageHandler +{ + public Task HandleEventAsync(EventMessage eventMessage) + { + return eventWriteService.CreateAsync(eventMessage); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs b/src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs similarity index 52% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs rename to src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs index 5a875f9278..8aece0c1da 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventHttpPostListener.cs +++ b/src/Core/AdminConsole/Services/Implementations/HttpPostEventHandler.cs @@ -1,32 +1,25 @@ using System.Net.Http.Json; using Bit.Core.Models.Data; using Bit.Core.Settings; -using Microsoft.Extensions.Logging; namespace Bit.Core.Services; -public class RabbitMqEventHttpPostListener : RabbitMqEventListenerBase +public class HttpPostEventHandler : IEventMessageHandler { private readonly HttpClient _httpClient; private readonly string _httpPostUrl; - private readonly string _queueName; - protected override string QueueName => _queueName; + public const string HttpClientName = "HttpPostEventHandlerHttpClient"; - public const string HttpClientName = "EventHttpPostListenerHttpClient"; - - public RabbitMqEventHttpPostListener( + public HttpPostEventHandler( IHttpClientFactory httpClientFactory, - ILogger logger, GlobalSettings globalSettings) - : base(logger, globalSettings) { _httpClient = httpClientFactory.CreateClient(HttpClientName); _httpPostUrl = globalSettings.EventLogging.RabbitMq.HttpPostUrl; - _queueName = globalSettings.EventLogging.RabbitMq.HttpPostQueueName; } - protected override async Task HandleMessageAsync(EventMessage eventMessage) + public async Task HandleEventAsync(EventMessage eventMessage) { var content = JsonContent.Create(eventMessage); var response = await _httpClient.PostAsync(_httpPostUrl, content); diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs deleted file mode 100644 index 25d85bddeb..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventRepositoryListener.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Bit.Core.Models.Data; -using Bit.Core.Settings; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.Services; - -public class RabbitMqEventRepositoryListener : RabbitMqEventListenerBase -{ - private readonly IEventWriteService _eventWriteService; - private readonly string _queueName; - - protected override string QueueName => _queueName; - - public RabbitMqEventRepositoryListener( - [FromKeyedServices("persistent")] IEventWriteService eventWriteService, - ILogger logger, - GlobalSettings globalSettings) - : base(logger, globalSettings) - { - _eventWriteService = eventWriteService; - _queueName = globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName; - } - - protected override Task HandleMessageAsync(EventMessage eventMessage) - { - return _eventWriteService.CreateAsync(eventMessage); - } -} diff --git a/src/Core/Services/EventLoggingListenerService.cs b/src/Core/Services/EventLoggingListenerService.cs new file mode 100644 index 0000000000..60b8789a6b --- /dev/null +++ b/src/Core/Services/EventLoggingListenerService.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Hosting; + +namespace Bit.Core.Services; + +public abstract class EventLoggingListenerService : BackgroundService +{ + protected readonly IEventMessageHandler _handler; + + protected EventLoggingListenerService(IEventMessageHandler handler) + { + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs b/src/Core/Services/Implementations/RabbitMqEventListenerService.cs similarity index 78% rename from src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs rename to src/Core/Services/Implementations/RabbitMqEventListenerService.cs index 48a549d261..9360170368 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerBase.cs +++ b/src/Core/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,26 +1,26 @@ using System.Text.Json; using Bit.Core.Models.Data; using Bit.Core.Settings; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; namespace Bit.Core.Services; -public abstract class RabbitMqEventListenerBase : BackgroundService +public class RabbitMqEventListenerService : EventLoggingListenerService { private IChannel _channel; private IConnection _connection; private readonly string _exchangeName; private readonly ConnectionFactory _factory; - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly string _queueName; - protected abstract string QueueName { get; } - - protected RabbitMqEventListenerBase( - ILogger logger, - GlobalSettings globalSettings) + public RabbitMqEventListenerService( + IEventMessageHandler handler, + ILogger logger, + GlobalSettings globalSettings, + string queueName) : base(handler) { _factory = new ConnectionFactory { @@ -30,6 +30,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService }; _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; _logger = logger; + _queueName = queueName; } public override async Task StartAsync(CancellationToken cancellationToken) @@ -38,13 +39,13 @@ public abstract class RabbitMqEventListenerBase : BackgroundService _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); await _channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - await _channel.QueueDeclareAsync(queue: QueueName, + await _channel.QueueDeclareAsync(queue: _queueName, durable: true, exclusive: false, autoDelete: false, arguments: null, cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: QueueName, + await _channel.QueueBindAsync(queue: _queueName, exchange: _exchangeName, routingKey: string.Empty, cancellationToken: cancellationToken); @@ -59,7 +60,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService try { var eventMessage = JsonSerializer.Deserialize(eventArgs.Body.Span); - await HandleMessageAsync(eventMessage); + await _handler.HandleEventAsync(eventMessage); } catch (Exception ex) { @@ -67,7 +68,7 @@ public abstract class RabbitMqEventListenerBase : BackgroundService } }; - await _channel.BasicConsumeAsync(QueueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); + await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: stoppingToken); while (!stoppingToken.IsCancellationRequested) { @@ -88,6 +89,4 @@ public abstract class RabbitMqEventListenerBase : BackgroundService _connection.Dispose(); base.Dispose(); } - - protected abstract Task HandleMessageAsync(EventMessage eventMessage); } diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index afe35ed34b..b89df8abf5 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -27,4 +27,5 @@ public interface IGlobalSettings string DatabaseProvider { get; set; } GlobalSettings.SqlSettings SqlServer { get; set; } string DevelopmentDirectory { get; set; } + GlobalSettings.EventLoggingSettings EventLogging { get; set; } } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 03e99f14e8..b692733a55 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -89,13 +89,26 @@ public class Startup CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) { + services.AddSingleton(); services.AddKeyedSingleton("persistent"); - services.AddHostedService(); + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + provider.GetRequiredService(), + globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HttpPostUrl)) { - services.AddHttpClient(RabbitMqEventHttpPostListener.HttpClientName); - services.AddHostedService(); + services.AddSingleton(); + services.AddHttpClient(HttpPostEventHandler.HttpClientName); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + provider.GetRequiredService(), + globalSettings.EventLogging.RabbitMq.HttpPostQueueName)); } } } diff --git a/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs b/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs index 1b1bd52a03..8a6c1dae97 100644 --- a/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs +++ b/test/Common/MockedHttpClient/MockedHttpMessageHandler.cs @@ -8,6 +8,8 @@ public class MockedHttpMessageHandler : HttpMessageHandler { private readonly List _matchers = new(); + public List CapturedRequests { get; } = new List(); + /// /// The fallback handler to use when the request does not match any of the provided matchers. /// @@ -16,6 +18,7 @@ public class MockedHttpMessageHandler : HttpMessageHandler protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + CapturedRequests.Add(request); var matcher = _matchers.FirstOrDefault(x => x.Matches(request)); if (matcher == null) { diff --git a/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs new file mode 100644 index 0000000000..2b143f5cb8 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventRepositoryHandlerTests.cs @@ -0,0 +1,24 @@ +using Bit.Core.Models.Data; +using Bit.Core.Services; +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; + +[SutProviderCustomize] +public class EventRepositoryHandlerTests +{ + [Theory, BitAutoData] + public async Task HandleEventAsync_WritesEventToIEventWriteService( + EventMessage eventMessage, + SutProvider sutProvider) + { + await sutProvider.Sut.HandleEventAsync(eventMessage); + await sutProvider.GetDependency().Received(1).CreateAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(eventMessage)) + ); + } +} diff --git a/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs new file mode 100644 index 0000000000..414b1c54be --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/HttpPostEventHandlerTests.cs @@ -0,0 +1,66 @@ +using System.Net; +using System.Net.Http.Json; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Bit.Test.Common.MockedHttpClient; +using NSubstitute; +using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class HttpPostEventHandlerTests +{ + private readonly MockedHttpMessageHandler _handler; + private HttpClient _httpClient; + + private const string _httpPostUrl = "http://localhost/test/event"; + + public HttpPostEventHandlerTests() + { + _handler = new MockedHttpMessageHandler(); + _handler.Fallback + .WithStatusCode(HttpStatusCode.OK) + .WithContent(new StringContent("testtest")); + _httpClient = _handler.ToHttpClient(); + } + + public SutProvider GetSutProvider() + { + var clientFactory = Substitute.For(); + clientFactory.CreateClient(HttpPostEventHandler.HttpClientName).Returns(_httpClient); + + var globalSettings = new GlobalSettings(); + globalSettings.EventLogging.RabbitMq.HttpPostUrl = _httpPostUrl; + + return new SutProvider() + .SetDependency(globalSettings) + .SetDependency(clientFactory) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_PostsEventsToUrl(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(); + var content = JsonContent.Create(eventMessage); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(HttpPostEventHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = await request.Content.ReadFromJsonAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(_httpPostUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" }); + } +} From 37b5cef085972c29ffd6e615b7d27a19ffde81ae Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:06:04 -0500 Subject: [PATCH 18/22] [PM-16040] Update Organization_UnassignedToProviderSearch.sql SPROC to allow Reseller plan types (#5332) * Update Organization_UnassignedToProviderSearch.sql SPROC * Robert's feedback --- .../Repositories/OrganizationRepository.cs | 18 ++++--- ...rganization_UnassignedToProviderSearch.sql | 14 ++--- ...rganization_UnassignedToProviderSearch.sql | 54 +++++++++++++++++++ 3 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index b6ec2ddca0..ea4e1334c6 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -117,13 +117,19 @@ public class OrganizationRepository : Repository + { + PlanType.Free, + PlanType.Custom, + PlanType.FamiliesAnnually2019, + PlanType.FamiliesAnnually + }; + var query = from o in dbContext.Organizations - where - ((o.PlanType >= PlanType.TeamsMonthly2019 && o.PlanType <= PlanType.EnterpriseAnnually2019) || - (o.PlanType >= PlanType.TeamsMonthly2020 && o.PlanType <= PlanType.EnterpriseAnnually)) && - !dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && - (string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%")) + where o.PlanType.NotIn(disallowedPlanTypes) && + !dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && + (string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%")) select o; if (string.IsNullOrWhiteSpace(ownerEmail)) @@ -155,7 +161,7 @@ public class OrganizationRepository : Repository o.CreationDate).Skip(skip).Take(take).ToArrayAsync(); + return await query.OrderByDescending(o => o.CreationDate).ThenByDescending(o => o.Id).Skip(skip).Take(take).ToArrayAsync(); } public async Task UpdateStorageAsync(Guid id) diff --git a/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql b/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql index e40f78fee0..4f2269b583 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql @@ -1,5 +1,5 @@ CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch] - @Name NVARCHAR(50), + @Name NVARCHAR(55), @OwnerEmail NVARCHAR(256), @Skip INT = 0, @Take INT = 25 @@ -9,7 +9,7 @@ BEGIN SET NOCOUNT ON DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%' DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%' - + IF @OwnerEmail IS NOT NULL BEGIN SELECT @@ -21,11 +21,11 @@ BEGIN INNER JOIN [dbo].[User] U ON U.[Id] = OU.[UserId] WHERE - ((O.[PlanType] >= 2 AND O.[PlanType] <= 5) OR (O.[PlanType] >= 8 AND O.[PlanType] <= 20) AND (O.PlanType <> 16)) -- All 'Teams' and 'Enterprise' organizations + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) AND (U.[Email] LIKE @OwnerLikeSearch) - ORDER BY O.[CreationDate] DESC + ORDER BY O.[CreationDate] DESC, O.[Id] OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY END @@ -36,11 +36,11 @@ BEGIN FROM [dbo].[OrganizationView] O WHERE - ((O.[PlanType] >= 2 AND O.[PlanType] <= 5) OR (O.[PlanType] >= 8 AND O.[PlanType] <= 20) AND (O.PlanType <> 16)) -- All 'Teams' and 'Enterprise' organizations + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) - ORDER BY O.[CreationDate] DESC + ORDER BY O.[CreationDate] DESC, O.[Id] OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY END -END \ No newline at end of file +END diff --git a/util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql b/util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql new file mode 100644 index 0000000000..07ec9ae8ac --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-28_00_UpdateOrganization_UnassignedToProviderSearch.sql @@ -0,0 +1,54 @@ +-- Drop existing SPROC +IF OBJECT_ID('[dbo].[Organization_UnassignedToProviderSearch]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[Organization_UnassignedToProviderSearch] + END +GO + +CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch] + @Name NVARCHAR(55), + @OwnerEmail NVARCHAR(256), + @Skip INT = 0, + @Take INT = 25 + WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%' + DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%' + + IF @OwnerEmail IS NOT NULL + BEGIN + SELECT + O.* + FROM + [dbo].[OrganizationView] O + INNER JOIN + [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId] + INNER JOIN + [dbo].[User] U ON U.[Id] = OU.[UserId] + WHERE + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' + AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) + AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) + AND (U.[Email] LIKE @OwnerLikeSearch) + ORDER BY O.[CreationDate] DESC, O.[Id] + OFFSET @Skip ROWS + FETCH NEXT @Take ROWS ONLY + END + ELSE + BEGIN + SELECT + O.* + FROM + [dbo].[OrganizationView] O + WHERE + O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families' + AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id]) + AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch) + ORDER BY O.[CreationDate] DESC, O.[Id] + OFFSET @Skip ROWS + FETCH NEXT @Take ROWS ONLY + END +END +GO From 0337300eac108ad2965795758febd895f93781e2 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:27:58 +0100 Subject: [PATCH 19/22] [PM-15625]Disable trial/send-verification-email endpoint for self-host (#5265) * endpoint is shut off for self-hosted env Signed-off-by: Cy Okeke * Fix the reference issues Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- src/Identity/Billing/Controller/AccountsController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index aada40bcb2..96ec1280cd 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -4,6 +4,7 @@ using Bit.Core.Context; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; +using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Mvc; @@ -17,6 +18,7 @@ public class AccountsController( IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { var token = await sendTrialInitiationEmailForRegistrationCommand.Handle( From b5cfb4b9c73de206999c170519fde76a1fc86682 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 4 Feb 2025 12:14:55 -0500 Subject: [PATCH 20/22] Enabled SonarQube scanning for PRs (#5363) * Added scan workflow parameter for PR number to enable branch scanning * Added missing backslash --- .github/workflows/scan.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index ec2eb7789a..fbcff6b1c0 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -85,6 +85,7 @@ jobs: /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ /d:sonar.exclusions=test/,bitwarden_license/test/ \ /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ + /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} \ /d:sonar.host.url="https://sonarcloud.io" dotnet build dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" From bdbed7adc8ef7be9c6da07207405f2cc04366707 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:31:15 +0100 Subject: [PATCH 21/22] Group tools owned feature flags (#5362) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8660010871..6f9960919a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -109,9 +109,15 @@ public static class FeatureFlagKeys public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; + /* Tools Team */ + public const string ItemShare = "item-share"; + public const string GeneratorToolsModernization = "generator-tools-modernization"; + public const string MemberAccessReport = "ac-2059-member-access-report"; + public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; + public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; + public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; - public const string ItemShare = "item-share"; public const string DuoRedirect = "duo-redirect"; public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section"; @@ -121,7 +127,6 @@ public static class FeatureFlagKeys public const string RestrictProviderAccess = "restrict-provider-access"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string VaultBulkManagementAction = "vault-bulk-management-action"; - public const string MemberAccessReport = "ac-2059-member-access-report"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; @@ -146,9 +151,7 @@ public static class FeatureFlagKeys public const string TrialPayment = "PM-8163-trial-payment"; public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises"; - public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; - public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string SecurityTasks = "security-tasks"; @@ -170,7 +173,6 @@ public static class FeatureFlagKeys public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; - public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string P15179_AddExistingOrgsFromProviderPortal = "PM-15179-add-existing-orgs-from-provider-portal"; From d2fb3760d3da1debd260ba190741ea360644a848 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 4 Feb 2025 13:53:16 -0500 Subject: [PATCH 22/22] Reworked PR workflow logic to prevent missing parameter (#5367) --- .github/workflows/scan.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index fbcff6b1c0..1fa5c9587c 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -85,7 +85,6 @@ jobs: /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ /d:sonar.exclusions=test/,bitwarden_license/test/ \ /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ - /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} \ - /d:sonar.host.url="https://sonarcloud.io" + /d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }} dotnet build dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"