From ac443ed495ba087e04fc470163b2180f6b28f6f8 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Tue, 18 Feb 2025 09:53:49 -0500 Subject: [PATCH 01/84] [pm-13985] Add a cancel endpoint to prevent authorization errors (#5229) --- .../AdminConsole/Controllers/ProvidersController.cs | 12 ++++++++++++ .../Views/Providers/CreateOrganization.cshtml | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 6229a4deab..38e25939c4 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -251,6 +251,18 @@ public class ProvidersController : Controller return View(provider); } + [SelfHosted(NotSelfHostedOnly = true)] + public async Task Cancel(Guid id) + { + var provider = await GetEditModel(id); + if (provider == null) + { + return RedirectToAction("Index"); + } + + return RedirectToAction("Edit", new { id }); + } + [HttpPost] [ValidateAntiForgeryToken] [SelfHosted(NotSelfHostedOnly = true)] diff --git a/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml index 6b7ccbdb12..eb790f20ba 100644 --- a/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml @@ -19,8 +19,8 @@
-
+
From 055e4e3066593b386d18b01fca632e3af2ade50c Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:42:00 -0500 Subject: [PATCH 02/84] Add RequestDeviceIdentifier to response (#5403) --- src/Api/Auth/Models/Response/AuthRequestResponseModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs index 3a07873451..50f7f5a3e7 100644 --- a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs +++ b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs @@ -18,6 +18,7 @@ public class AuthRequestResponseModel : ResponseModel Id = authRequest.Id; PublicKey = authRequest.PublicKey; + RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier; RequestDeviceTypeValue = authRequest.RequestDeviceType; RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString()) .FirstOrDefault()?.GetCustomAttribute()?.GetName(); @@ -32,6 +33,7 @@ public class AuthRequestResponseModel : ResponseModel public Guid Id { get; set; } public string PublicKey { get; set; } + public string RequestDeviceIdentifier { get; set; } public DeviceType RequestDeviceTypeValue { get; set; } public string RequestDeviceType { get; set; } public string RequestIpAddress { get; set; } From f27886e3124976669515e239da9d8d1f9c3baeb8 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:54:11 -0500 Subject: [PATCH 03/84] [PM-17932] Convert Renovate config to JSON5 (#5414) * Migrated Renovate config to JSON5 * Apply Prettier * Added comment for demonstration --------- Co-authored-by: Matt Bishop --- .github/renovate.json | 199 ----------------------------------------- .github/renovate.json5 | 199 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 199 deletions(-) delete mode 100644 .github/renovate.json create mode 100644 .github/renovate.json5 diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 31d78a4d4e..0000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>bitwarden/renovate-config"], - "enabledManagers": [ - "dockerfile", - "docker-compose", - "github-actions", - "npm", - "nuget" - ], - "packageRules": [ - { - "groupName": "dockerfile minor", - "matchManagers": ["dockerfile"], - "matchUpdateTypes": ["minor"] - }, - { - "groupName": "docker-compose minor", - "matchManagers": ["docker-compose"], - "matchUpdateTypes": ["minor"] - }, - { - "groupName": "github-action minor", - "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor"] - }, - { - "matchManagers": ["dockerfile", "docker-compose"], - "commitMessagePrefix": "[deps] BRE:" - }, - { - "matchPackageNames": ["DnsClient"], - "description": "Admin Console owned dependencies", - "commitMessagePrefix": "[deps] AC:", - "reviewers": ["team:team-admin-console-dev"] - }, - { - "matchFileNames": ["src/Admin/package.json", "src/Sso/package.json"], - "description": "Admin & SSO npm packages", - "commitMessagePrefix": "[deps] Auth:", - "reviewers": ["team:team-auth-dev"] - }, - { - "matchPackageNames": [ - "Azure.Extensions.AspNetCore.DataProtection.Blobs", - "DuoUniversal", - "Fido2.AspNet", - "Duende.IdentityServer", - "Microsoft.Extensions.Identity.Stores", - "Otp.NET", - "Sustainsys.Saml2.AspNetCore2", - "YubicoDotNetClient" - ], - "description": "Auth owned dependencies", - "commitMessagePrefix": "[deps] Auth:", - "reviewers": ["team:team-auth-dev"] - }, - { - "matchPackageNames": [ - "AutoFixture.AutoNSubstitute", - "AutoFixture.Xunit2", - "BenchmarkDotNet", - "BitPay.Light", - "Braintree", - "coverlet.collector", - "CsvHelper", - "Kralizek.AutoFixture.Extensions.MockHttp", - "Microsoft.AspNetCore.Mvc.Testing", - "Microsoft.Extensions.Logging", - "Microsoft.Extensions.Logging.Console", - "Newtonsoft.Json", - "NSubstitute", - "Sentry.Serilog", - "Serilog.AspNetCore", - "Serilog.Extensions.Logging", - "Serilog.Extensions.Logging.File", - "Serilog.Sinks.AzureCosmosDB", - "Serilog.Sinks.SyslogMessages", - "Stripe.net", - "Swashbuckle.AspNetCore", - "Swashbuckle.AspNetCore.SwaggerGen", - "xunit", - "xunit.runner.visualstudio" - ], - "description": "Billing owned dependencies", - "commitMessagePrefix": "[deps] Billing:", - "reviewers": ["team:team-billing-dev"] - }, - { - "matchPackagePatterns": ["^Microsoft.Extensions.Logging"], - "groupName": "Microsoft.Extensions.Logging", - "description": "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset" - }, - { - "matchPackageNames": [ - "Dapper", - "dbup-sqlserver", - "dotnet-ef", - "linq2db.EntityFrameworkCore", - "Microsoft.Azure.Cosmos", - "Microsoft.Data.SqlClient", - "Microsoft.EntityFrameworkCore.Design", - "Microsoft.EntityFrameworkCore.InMemory", - "Microsoft.EntityFrameworkCore.Relational", - "Microsoft.EntityFrameworkCore.Sqlite", - "Microsoft.EntityFrameworkCore.SqlServer", - "Microsoft.Extensions.Caching.Cosmos", - "Microsoft.Extensions.Caching.SqlServer", - "Microsoft.Extensions.Caching.StackExchangeRedis", - "Npgsql.EntityFrameworkCore.PostgreSQL", - "Pomelo.EntityFrameworkCore.MySql" - ], - "description": "DbOps owned dependencies", - "commitMessagePrefix": "[deps] DbOps:", - "reviewers": ["team:dept-dbops"] - }, - { - "matchPackageNames": ["CommandDotNet", "YamlDotNet"], - "description": "DevOps owned dependencies", - "commitMessagePrefix": "[deps] BRE:", - "reviewers": ["team:dept-bre"] - }, - { - "matchPackageNames": [ - "AspNetCoreRateLimit", - "AspNetCoreRateLimit.Redis", - "Azure.Data.Tables", - "Azure.Messaging.EventGrid", - "Azure.Messaging.ServiceBus", - "Azure.Storage.Blobs", - "Azure.Storage.Queues", - "Microsoft.AspNetCore.Authentication.JwtBearer", - "Microsoft.AspNetCore.Http", - "Quartz" - ], - "description": "Platform owned dependencies", - "commitMessagePrefix": "[deps] Platform:", - "reviewers": ["team:team-platform-dev"] - }, - { - "matchPackagePatterns": ["EntityFrameworkCore", "^dotnet-ef"], - "groupName": "EntityFrameworkCore", - "description": "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset" - }, - { - "matchPackageNames": [ - "AutoMapper.Extensions.Microsoft.DependencyInjection", - "AWSSDK.SimpleEmail", - "AWSSDK.SQS", - "Handlebars.Net", - "LaunchDarkly.ServerSdk", - "MailKit", - "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", - "Microsoft.AspNetCore.SignalR.StackExchangeRedis", - "Microsoft.Azure.NotificationHubs", - "Microsoft.Extensions.Configuration.EnvironmentVariables", - "Microsoft.Extensions.Configuration.UserSecrets", - "Microsoft.Extensions.Configuration", - "Microsoft.Extensions.DependencyInjection.Abstractions", - "Microsoft.Extensions.DependencyInjection", - "SendGrid" - ], - "description": "Tools owned dependencies", - "commitMessagePrefix": "[deps] Tools:", - "reviewers": ["team:team-tools-dev"] - }, - { - "matchPackagePatterns": ["^Microsoft.AspNetCore.SignalR"], - "groupName": "SignalR", - "description": "Group SignalR to exclude them from the dotnet monorepo preset" - }, - { - "matchPackagePatterns": ["^Microsoft.Extensions.Configuration"], - "groupName": "Microsoft.Extensions.Configuration", - "description": "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset" - }, - { - "matchPackagePatterns": ["^Microsoft.Extensions.DependencyInjection"], - "groupName": "Microsoft.Extensions.DependencyInjection", - "description": "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset" - }, - { - "matchPackageNames": [ - "AngleSharp", - "AspNetCore.HealthChecks.AzureServiceBus", - "AspNetCore.HealthChecks.AzureStorage", - "AspNetCore.HealthChecks.Network", - "AspNetCore.HealthChecks.Redis", - "AspNetCore.HealthChecks.SendGrid", - "AspNetCore.HealthChecks.SqlServer", - "AspNetCore.HealthChecks.Uris" - ], - "description": "Vault owned dependencies", - "commitMessagePrefix": "[deps] Vault:", - "reviewers": ["team:team-vault-dev"] - } - ], - "ignoreDeps": ["dotnet-sdk"] -} diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..4722307d10 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,199 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies + enabledManagers: [ + "dockerfile", + "docker-compose", + "github-actions", + "npm", + "nuget", + ], + packageRules: [ + { + groupName: "dockerfile minor", + matchManagers: ["dockerfile"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "docker-compose minor", + matchManagers: ["docker-compose"], + matchUpdateTypes: ["minor"], + }, + { + groupName: "github-action minor", + matchManagers: ["github-actions"], + matchUpdateTypes: ["minor"], + }, + { + matchManagers: ["dockerfile", "docker-compose"], + commitMessagePrefix: "[deps] BRE:", + }, + { + matchPackageNames: ["DnsClient"], + description: "Admin Console owned dependencies", + commitMessagePrefix: "[deps] AC:", + reviewers: ["team:team-admin-console-dev"], + }, + { + matchFileNames: ["src/Admin/package.json", "src/Sso/package.json"], + description: "Admin & SSO npm packages", + commitMessagePrefix: "[deps] Auth:", + reviewers: ["team:team-auth-dev"], + }, + { + matchPackageNames: [ + "Azure.Extensions.AspNetCore.DataProtection.Blobs", + "DuoUniversal", + "Fido2.AspNet", + "Duende.IdentityServer", + "Microsoft.Extensions.Identity.Stores", + "Otp.NET", + "Sustainsys.Saml2.AspNetCore2", + "YubicoDotNetClient", + ], + description: "Auth owned dependencies", + commitMessagePrefix: "[deps] Auth:", + reviewers: ["team:team-auth-dev"], + }, + { + matchPackageNames: [ + "AutoFixture.AutoNSubstitute", + "AutoFixture.Xunit2", + "BenchmarkDotNet", + "BitPay.Light", + "Braintree", + "coverlet.collector", + "CsvHelper", + "Kralizek.AutoFixture.Extensions.MockHttp", + "Microsoft.AspNetCore.Mvc.Testing", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Console", + "Newtonsoft.Json", + "NSubstitute", + "Sentry.Serilog", + "Serilog.AspNetCore", + "Serilog.Extensions.Logging", + "Serilog.Extensions.Logging.File", + "Serilog.Sinks.AzureCosmosDB", + "Serilog.Sinks.SyslogMessages", + "Stripe.net", + "Swashbuckle.AspNetCore", + "Swashbuckle.AspNetCore.SwaggerGen", + "xunit", + "xunit.runner.visualstudio", + ], + description: "Billing owned dependencies", + commitMessagePrefix: "[deps] Billing:", + reviewers: ["team:team-billing-dev"], + }, + { + matchPackagePatterns: ["^Microsoft.Extensions.Logging"], + groupName: "Microsoft.Extensions.Logging", + description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: [ + "Dapper", + "dbup-sqlserver", + "dotnet-ef", + "linq2db.EntityFrameworkCore", + "Microsoft.Azure.Cosmos", + "Microsoft.Data.SqlClient", + "Microsoft.EntityFrameworkCore.Design", + "Microsoft.EntityFrameworkCore.InMemory", + "Microsoft.EntityFrameworkCore.Relational", + "Microsoft.EntityFrameworkCore.Sqlite", + "Microsoft.EntityFrameworkCore.SqlServer", + "Microsoft.Extensions.Caching.Cosmos", + "Microsoft.Extensions.Caching.SqlServer", + "Microsoft.Extensions.Caching.StackExchangeRedis", + "Npgsql.EntityFrameworkCore.PostgreSQL", + "Pomelo.EntityFrameworkCore.MySql", + ], + description: "DbOps owned dependencies", + commitMessagePrefix: "[deps] DbOps:", + reviewers: ["team:dept-dbops"], + }, + { + matchPackageNames: ["CommandDotNet", "YamlDotNet"], + description: "DevOps owned dependencies", + commitMessagePrefix: "[deps] BRE:", + reviewers: ["team:dept-bre"], + }, + { + matchPackageNames: [ + "AspNetCoreRateLimit", + "AspNetCoreRateLimit.Redis", + "Azure.Data.Tables", + "Azure.Messaging.EventGrid", + "Azure.Messaging.ServiceBus", + "Azure.Storage.Blobs", + "Azure.Storage.Queues", + "Microsoft.AspNetCore.Authentication.JwtBearer", + "Microsoft.AspNetCore.Http", + "Quartz", + ], + description: "Platform owned dependencies", + commitMessagePrefix: "[deps] Platform:", + reviewers: ["team:team-platform-dev"], + }, + { + matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"], + groupName: "EntityFrameworkCore", + description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: [ + "AutoMapper.Extensions.Microsoft.DependencyInjection", + "AWSSDK.SimpleEmail", + "AWSSDK.SQS", + "Handlebars.Net", + "LaunchDarkly.ServerSdk", + "MailKit", + "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.Azure.NotificationHubs", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "SendGrid", + ], + description: "Tools owned dependencies", + commitMessagePrefix: "[deps] Tools:", + reviewers: ["team:team-tools-dev"], + }, + { + matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"], + groupName: "SignalR", + description: "Group SignalR to exclude them from the dotnet monorepo preset", + }, + { + matchPackagePatterns: ["^Microsoft.Extensions.Configuration"], + groupName: "Microsoft.Extensions.Configuration", + description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset", + }, + { + matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"], + groupName: "Microsoft.Extensions.DependencyInjection", + description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset", + }, + { + matchPackageNames: [ + "AngleSharp", + "AspNetCore.HealthChecks.AzureServiceBus", + "AspNetCore.HealthChecks.AzureStorage", + "AspNetCore.HealthChecks.Network", + "AspNetCore.HealthChecks.Redis", + "AspNetCore.HealthChecks.SendGrid", + "AspNetCore.HealthChecks.SqlServer", + "AspNetCore.HealthChecks.Uris", + ], + description: "Vault owned dependencies", + commitMessagePrefix: "[deps] Vault:", + reviewers: ["team:team-vault-dev"], + }, + ], + ignoreDeps: ["dotnet-sdk"], +} From fcb98481800fc410c2d445be04ebee350f9e37a3 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:13:48 +0100 Subject: [PATCH 04/84] [PM-13620]Existing user email linking to create-organization (#5315) * Changes for the existing customer Signed-off-by: Cy Okeke * removed the added character Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- .../Models/Mail/TrialInititaionVerifyEmail.cs | 19 +++++++++++++++---- ...alInitiationEmailForRegistrationCommand.cs | 5 +---- src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 2 ++ .../NoopImplementations/NoopMailService.cs | 1 + 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs index df08296083..33b9578d0e 100644 --- a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs +++ b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs @@ -5,6 +5,7 @@ namespace Bit.Core.Billing.Models.Mail; public class TrialInitiationVerifyEmail : RegisterVerifyEmail { + public bool IsExistingUser { get; set; } /// /// See comment on . /// @@ -26,8 +27,18 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail /// Currently we only support one product type at a time, despite Product being a collection. /// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route /// - private string Route => - Product.Any(p => p == ProductType.PasswordManager) - ? "trial-initiation" - : "secrets-manager-trial-initiation"; + private string Route + { + get + { + if (IsExistingUser) + { + return "create-organization"; + } + + return Product.Any(p => p == ProductType.PasswordManager) + ? "trial-initiation" + : "secrets-manager-trial-initiation"; + } + } } diff --git a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs index 6657be085e..385d7ebbd6 100644 --- a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs @@ -43,10 +43,7 @@ public class SendTrialInitiationEmailForRegistrationCommand( await PerformConstantTimeOperationsAsync(); - if (!userExists) - { - await mailService.SendTrialInitiationSignupEmailAsync(email, token, productTier, products); - } + await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products); return null; } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 77914c0188..92d05ddb7d 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -14,6 +14,7 @@ public interface IMailService Task SendVerifyEmailEmailAsync(string email, Guid userId, string token); Task SendRegistrationVerificationEmailAsync(string email, string token); Task SendTrialInitiationSignupEmailAsync( + bool isExistingUser, string email, string token, ProductTierType productTier, diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 630c5b0bf0..d18a29b13a 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -74,6 +74,7 @@ public class HandlebarsMailService : IMailService } public async Task SendTrialInitiationSignupEmailAsync( + bool isExistingUser, string email, string token, ProductTierType productTier, @@ -82,6 +83,7 @@ public class HandlebarsMailService : IMailService var message = CreateDefaultMessage("Verify your email", email); var model = new TrialInitiationVerifyEmail { + IsExistingUser = isExistingUser, Token = WebUtility.UrlEncode(token), Email = WebUtility.UrlEncode(email), WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 13914ddd86..d6b330294d 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -26,6 +26,7 @@ public class NoopMailService : IMailService } public Task SendTrialInitiationSignupEmailAsync( + bool isExistingUser, string email, string token, ProductTierType productTier, From 43be2dbc8304e1fdc36aa58f14e63471e9bdada0 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:52:17 -0500 Subject: [PATCH 05/84] Prevent organization disablement on addition to provider (#5419) --- .../Services/Implementations/SubscriptionDeletedHandler.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index 06692ab016..26a1c30c14 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -33,13 +33,16 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled; const string providerMigrationCancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; + const string addedToProviderCancellationComment = "Organization was added to Provider"; if (!subCanceled) { return; } - if (organizationId.HasValue && subscription is not { CancellationDetails.Comment: providerMigrationCancellationComment }) + if (organizationId.HasValue && + subscription.CancellationDetails.Comment != providerMigrationCancellationComment && + !subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)) { await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); } From 4f73081e4158bafad74dbd9e2142ec423885a9bf Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:13:03 -0500 Subject: [PATCH 06/84] Give provider credit for unused client organization time (#5421) --- .../Billing/ProviderBillingService.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index abba8aff90..da59c3c35c 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -117,6 +117,19 @@ public class ProviderBillingService( ScaleSeats(provider, organization.PlanType, organization.Seats!.Value) ); + var clientCustomer = await subscriberService.GetCustomer(organization); + + if (clientCustomer.Balance != 0) + { + await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, + new CustomerBalanceTransactionCreateOptions + { + Amount = clientCustomer.Balance, + Currency = "USD", + Description = $"Unused, prorated time for client organization with ID {organization.Id}." + }); + } + await eventService.LogProviderOrganizationEventAsync( providerOrganization, EventType.ProviderOrganization_Added); From 228ce3b2e9d9b58fae89532d5858c1e0ec1139d2 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 19 Feb 2025 12:01:11 -0500 Subject: [PATCH 07/84] Scale seats before inserting ProviderOrganization when adding existing organization (#5420) --- .../Commercial.Core/Billing/ProviderBillingService.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index da59c3c35c..7b10793283 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -111,10 +111,15 @@ public class ProviderBillingService( Key = key }; + /* + * We have to scale the provider's seats before the ProviderOrganization + * row is inserted so the added organization's seats don't get double counted. + */ + await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value); + await Task.WhenAll( organizationRepository.ReplaceAsync(organization), - providerOrganizationRepository.CreateAsync(providerOrganization), - ScaleSeats(provider, organization.PlanType, organization.Seats!.Value) + providerOrganizationRepository.CreateAsync(providerOrganization) ); var clientCustomer = await subscriberService.GetCustomer(organization); From 9f4aa1ab2bcda4aaef9dcf25350a1f3f78eb3417 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:35:48 +0100 Subject: [PATCH 08/84] [PM-15084] Push global notification creation to affected clients (#5079) * PM-10600: Notification push notification * PM-10600: Sending to specific client types for relay push notifications * PM-10600: Sending to specific client types for other clients * PM-10600: Send push notification on notification creation * PM-10600: Explicit group names * PM-10600: Id typos * PM-10600: Revert global push notifications * PM-10600: Added DeviceType claim * PM-10600: Sent to organization typo * PM-10600: UT coverage * PM-10600: Small refactor, UTs coverage * PM-10600: UTs coverage * PM-10600: Startup fix * PM-10600: Test fix * PM-10600: Required attribute, organization group for push notification fix * PM-10600: UT coverage * PM-10600: Fix Mobile devices not registering to organization push notifications We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications. * PM-10600: Unit Test coverage for NotificationHubPushRegistrationService Fixed IFeatureService substitute mocking for Android tests. Added user part of organization test with organizationId tags expectation. * PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict * PM-10600: Organization push notifications not sending to mobile device from self-hosted. Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server. * PM-10600: Fix self-hosted organization notification not being received by mobile device. When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id. * PM-10600: Broken NotificationsController integration test Device type is now part of JWT access token, so the notification center results in the integration test are now scoped to client type web and all. * PM-10600: Merge conflicts fix * merge conflict fix * PM-10600: Push notification with full notification center content. Notification Center push notification now includes all the fields. * PM-10564: Push notification updates to other clients Cherry-picked and squashed commits: d9711b6031a1bc1d96b920e521e6f37de1b434ec 6e69c8a0ce9a5ee29df9988b20c6e531c0b4e4a3 01c814595e572911574066802b661c83b116a865 3885885d5f4be39fdc2b8d258867c8a7536491cd 1285a7e994921b0e6f9ba78f9b84d8e7a6ceda2f fcf346985f367c462ef7b65ce7d5d2612f7345cc 28ff53c293f4d37de5fa40d2964f924368e13c95 57804ae27cbf25d88d148f399ce81c1c09997e10 1c9339b6869926e59076202e06341e5d4a403cc7 * PM-15084: Push global notification creation to affected clients Cherry-picked and squashed commits: ed5051e0ebc578ac6c5fce1f406d66bede3fa2b6 181f3e4ae643072c737ac00bf44a2fbbdd458ee8 49fe7c93fd5eb6fd5df680194403cf4b2beabace a8efb45a63d685cce83a6e5ea28f2320c3e52dae 7b4122c8379df5444e839297b4e7f9163550861a d21d4a67b32af85f5cd4d7dff2491852fd7d2028 186a09bb9206417616d8645cbbd18478f31a305c 1531f564b54ec1a031399fc1e2754e59dbd7e743 * PM-15084: Log warning when invalid notification push notification sent * explicit Guid default value * push notification tests in wrong namespace * Installation push notification not received for on global notification center message * wrong merge conflict * wrong merge conflict * installation id type Guid in push registration request --- .../Push/Controllers/PushController.cs | 32 +- src/Core/Enums/PushType.cs | 4 +- .../Request/PushRegistrationRequestModel.cs | 16 +- .../Api/Request/PushSendRequestModel.cs | 8 +- src/Core/Models/PushNotification.cs | 1 + .../NotificationHubPushNotificationService.cs | 94 +++++- .../NotificationHubPushRegistrationService.cs | 20 +- .../AzureQueuePushNotificationService.cs | 24 +- .../Push/Services/IPushNotificationService.cs | 3 + .../Push/Services/IPushRegistrationService.cs | 2 +- .../MultiServicePushNotificationService.cs | 8 + .../Services/NoopPushNotificationService.cs | 13 +- .../Services/NoopPushRegistrationService.cs | 2 +- ...NotificationsApiPushNotificationService.cs | 18 +- .../Services/RelayPushNotificationService.cs | 56 +++- .../Services/RelayPushRegistrationService.cs | 5 +- .../Services/Implementations/DeviceService.cs | 9 +- src/Notifications/HubHelpers.cs | 41 +-- src/Notifications/NotificationsHub.cs | 28 ++ .../Utilities/ServiceCollectionExtensions.cs | 10 +- .../Push/Controllers/PushControllerTests.cs | 288 ++++++++++++++++++ .../Api/Request/PushSendRequestModelTests.cs | 72 ++++- ...ficationHubPushNotificationServiceTests.cs | 180 +++++++++-- ...ficationHubPushRegistrationServiceTests.cs | 64 ++-- .../AzureQueuePushNotificationServiceTests.cs | 74 ++++- ...ultiServicePushNotificationServiceTests.cs | 22 +- ...icationsApiPushNotificationServiceTests.cs | 5 +- .../RelayPushNotificationServiceTests.cs | 5 +- .../RelayPushRegistrationServiceTests.cs | 5 +- test/Core.Test/Services/DeviceServiceTests.cs | 17 +- 30 files changed, 963 insertions(+), 163 deletions(-) create mode 100644 test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index 8b9e8b52a0..f88fa4aa9e 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -22,14 +22,14 @@ public class PushController : Controller private readonly IPushNotificationService _pushNotificationService; private readonly IWebHostEnvironment _environment; private readonly ICurrentContext _currentContext; - private readonly GlobalSettings _globalSettings; + private readonly IGlobalSettings _globalSettings; public PushController( IPushRegistrationService pushRegistrationService, IPushNotificationService pushNotificationService, IWebHostEnvironment environment, ICurrentContext currentContext, - GlobalSettings globalSettings) + IGlobalSettings globalSettings) { _currentContext = currentContext; _environment = environment; @@ -39,22 +39,23 @@ public class PushController : Controller } [HttpPost("register")] - public async Task PostRegister([FromBody] PushRegistrationRequestModel model) + public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model) { CheckUsage(); await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId), - Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix)); + Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), + model.InstallationId); } [HttpPost("delete")] - public async Task PostDelete([FromBody] PushDeviceRequestModel model) + public async Task DeleteAsync([FromBody] PushDeviceRequestModel model) { CheckUsage(); await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id)); } [HttpPut("add-organization")] - public async Task PutAddOrganization([FromBody] PushUpdateRequestModel model) + public async Task AddOrganizationAsync([FromBody] PushUpdateRequestModel model) { CheckUsage(); await _pushRegistrationService.AddUserRegistrationOrganizationAsync( @@ -63,7 +64,7 @@ public class PushController : Controller } [HttpPut("delete-organization")] - public async Task PutDeleteOrganization([FromBody] PushUpdateRequestModel model) + public async Task DeleteOrganizationAsync([FromBody] PushUpdateRequestModel model) { CheckUsage(); await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( @@ -72,11 +73,22 @@ public class PushController : Controller } [HttpPost("send")] - public async Task PostSend([FromBody] PushSendRequestModel model) + public async Task SendAsync([FromBody] PushSendRequestModel model) { CheckUsage(); - if (!string.IsNullOrWhiteSpace(model.UserId)) + if (!string.IsNullOrWhiteSpace(model.InstallationId)) + { + if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!) + { + throw new BadRequestException("InstallationId does not match current context."); + } + + await _pushNotificationService.SendPayloadToInstallationAsync( + _currentContext.InstallationId.Value.ToString(), model.Type, model.Payload, Prefix(model.Identifier), + Prefix(model.DeviceId), model.ClientType); + } + else if (!string.IsNullOrWhiteSpace(model.UserId)) { await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId), model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType); @@ -95,7 +107,7 @@ public class PushController : Controller return null; } - return $"{_currentContext.InstallationId.Value}_{value}"; + return $"{_currentContext.InstallationId!.Value}_{value}"; } private void CheckUsage() diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs index d4a4caeb9e..6d0dd9393c 100644 --- a/src/Core/Enums/PushType.cs +++ b/src/Core/Enums/PushType.cs @@ -28,6 +28,6 @@ public enum PushType : byte SyncOrganizationStatusChanged = 18, SyncOrganizationCollectionSettingChanged = 19, - SyncNotification = 20, - SyncNotificationStatus = 21 + Notification = 20, + NotificationStatus = 21 } diff --git a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs index ee787dd083..0c87bf98d1 100644 --- a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs +++ b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs @@ -5,15 +5,11 @@ namespace Bit.Core.Models.Api; public class PushRegistrationRequestModel { - [Required] - public string DeviceId { get; set; } - [Required] - public string PushToken { get; set; } - [Required] - public string UserId { get; set; } - [Required] - public DeviceType Type { get; set; } - [Required] - public string Identifier { get; set; } + [Required] public string DeviceId { get; set; } + [Required] public string PushToken { get; set; } + [Required] public string UserId { get; set; } + [Required] public DeviceType Type { get; set; } + [Required] public string Identifier { get; set; } public IEnumerable OrganizationIds { get; set; } + public Guid InstallationId { get; set; } } diff --git a/src/Core/Models/Api/Request/PushSendRequestModel.cs b/src/Core/Models/Api/Request/PushSendRequestModel.cs index 7247e6d25f..0ef7e999e3 100644 --- a/src/Core/Models/Api/Request/PushSendRequestModel.cs +++ b/src/Core/Models/Api/Request/PushSendRequestModel.cs @@ -13,12 +13,16 @@ public class PushSendRequestModel : IValidatableObject public required PushType Type { get; set; } public required object Payload { get; set; } public ClientType? ClientType { get; set; } + public string? InstallationId { get; set; } public IEnumerable Validate(ValidationContext validationContext) { - if (string.IsNullOrWhiteSpace(UserId) && string.IsNullOrWhiteSpace(OrganizationId)) + if (string.IsNullOrWhiteSpace(UserId) && + string.IsNullOrWhiteSpace(OrganizationId) && + string.IsNullOrWhiteSpace(InstallationId)) { - yield return new ValidationResult($"{nameof(UserId)} or {nameof(OrganizationId)} is required."); + yield return new ValidationResult( + $"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required."); } } } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index 775c3443f2..63058be692 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -55,6 +55,7 @@ public class NotificationPushNotification public ClientType ClientType { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public Guid? InstallationId { get; set; } public string? Title { get; set; } public string? Body { get; set; } public DateTime CreationDate { get; set; } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index 7baf0352ee..6bc5b0db6b 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -10,6 +10,7 @@ using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push; using Bit.Core.Repositories; +using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; @@ -18,6 +19,11 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification; namespace Bit.Core.NotificationHub; +/// +/// Sends mobile push notifications to the Azure Notification Hub. +/// Used by Cloud-Hosted environments. +/// Received by Firebase for Android or APNS for iOS. +/// public class NotificationHubPushNotificationService : IPushNotificationService { private readonly IInstallationDeviceRepository _installationDeviceRepository; @@ -25,17 +31,25 @@ public class NotificationHubPushNotificationService : IPushNotificationService private readonly bool _enableTracing = false; private readonly INotificationHubPool _notificationHubPool; private readonly ILogger _logger; + private readonly IGlobalSettings _globalSettings; public NotificationHubPushNotificationService( IInstallationDeviceRepository installationDeviceRepository, INotificationHubPool notificationHubPool, IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger, + IGlobalSettings globalSettings) { _installationDeviceRepository = installationDeviceRepository; _httpContextAccessor = httpContextAccessor; _notificationHubPool = notificationHubPool; _logger = logger; + _globalSettings = globalSettings; + + if (globalSettings.Installation.Id == Guid.Empty) + { + logger.LogWarning("Installation ID is not set. Push notifications for installations will not work."); + } } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) @@ -185,6 +199,10 @@ public class NotificationHubPushNotificationService : IPushNotificationService public async Task PushNotificationAsync(Notification notification) { + Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty + ? _globalSettings.Installation.Id + : null; + var message = new NotificationPushNotification { Id = notification.Id, @@ -193,26 +211,49 @@ public class NotificationHubPushNotificationService : IPushNotificationService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = installationId, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; - if (notification.UserId.HasValue) + if (notification.Global) { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true, + if (installationId.HasValue) + { + await SendPayloadToInstallationAsync(installationId.Value, PushType.Notification, message, true, + notification.ClientType); + } + else + { + _logger.LogWarning( + "Invalid global notification id {NotificationId} push notification. No installation id provided.", + notification.Id); + } + } + else if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message, + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message, true, notification.ClientType); } + else + { + _logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id); + } } public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) { + Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty + ? _globalSettings.Installation.Id + : null; + var message = new NotificationPushNotification { Id = notification.Id, @@ -221,6 +262,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = installationId, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, @@ -229,15 +271,33 @@ public class NotificationHubPushNotificationService : IPushNotificationService DeletedDate = notificationStatus.DeletedDate }; - if (notification.UserId.HasValue) + if (notification.Global) { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true, + if (installationId.HasValue) + { + await SendPayloadToInstallationAsync(installationId.Value, PushType.NotificationStatus, message, true, + notification.ClientType); + } + else + { + _logger.LogWarning( + "Invalid global notification status id {NotificationId} push notification. No installation id provided.", + notification.Id); + } + } + else if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message, - true, notification.ClientType); + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, + message, true, notification.ClientType); + } + else + { + _logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id); } } @@ -248,6 +308,13 @@ public class NotificationHubPushNotificationService : IPushNotificationService await SendPayloadToUserAsync(authRequest.UserId, type, message, true); } + private async Task SendPayloadToInstallationAsync(Guid installationId, PushType type, object payload, + bool excludeCurrentContext, ClientType? clientType = null) + { + await SendPayloadToInstallationAsync(installationId.ToString(), type, payload, + GetContextIdentifier(excludeCurrentContext), clientType: clientType); + } + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, ClientType? clientType = null) { @@ -262,6 +329,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService GetContextIdentifier(excludeCurrentContext), clientType: clientType); } + public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, + string? identifier, string? deviceId = null, ClientType? clientType = null) + { + var tag = BuildTag($"template:payload && installationId:{installationId}", identifier, clientType); + await SendPayloadAsync(tag, type, payload); + if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) + { + await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); + } + } + public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index 0c9bbea425..9793c8198a 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -21,7 +21,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds) + string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { if (string.IsNullOrWhiteSpace(pushToken)) { @@ -50,6 +50,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService installation.Tags.Add($"organizationId:{organizationId}"); } + if (installationId != Guid.Empty) + { + installation.Tags.Add($"installationId:{installationId}"); + } + string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null; switch (type) { @@ -80,11 +85,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType, - organizationIdsList); + organizationIdsList, installationId); BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType, - organizationIdsList); + organizationIdsList, installationId); BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, - userId, identifier, clientType, organizationIdsList); + userId, identifier, clientType, organizationIdsList, installationId); await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) @@ -94,7 +99,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody, - string userId, string identifier, ClientType clientType, List organizationIds) + string userId, string identifier, ClientType clientType, List organizationIds, Guid installationId) { if (templateBody == null) { @@ -122,6 +127,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService template.Tags.Add($"organizationId:{organizationId}"); } + if (installationId != Guid.Empty) + { + template.Tags.Add($"installationId:{installationId}"); + } + installation.Templates.Add(fullTemplateId, template); } diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index c32212c6b2..f88c0641c5 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -7,11 +7,13 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push.Internal; @@ -19,13 +21,22 @@ public class AzureQueuePushNotificationService : IPushNotificationService { private readonly QueueClient _queueClient; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IGlobalSettings _globalSettings; public AzureQueuePushNotificationService( [FromKeyedServices("notifications")] QueueClient queueClient, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IGlobalSettings globalSettings, + ILogger logger) { _queueClient = queueClient; _httpContextAccessor = httpContextAccessor; + _globalSettings = globalSettings; + + if (globalSettings.Installation.Id == Guid.Empty) + { + logger.LogWarning("Installation ID is not set. Push notifications for installations will not work."); + } } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) @@ -176,13 +187,14 @@ public class AzureQueuePushNotificationService : IPushNotificationService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; - await SendMessageAsync(PushType.SyncNotification, message, true); + await SendMessageAsync(PushType.Notification, message, true); } public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) @@ -195,6 +207,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, @@ -203,7 +216,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService DeletedDate = notificationStatus.DeletedDate }; - await SendMessageAsync(PushType.SyncNotificationStatus, message, true); + await SendMessageAsync(PushType.NotificationStatus, message, true); } private async Task PushSendAsync(Send send, PushType type) @@ -241,6 +254,11 @@ public class AzureQueuePushNotificationService : IPushNotificationService return currentContext?.DeviceIdentifier; } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => + // Noop + Task.CompletedTask; + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index 1c7fdc659b..d0f18cd8ac 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -31,6 +31,9 @@ public interface IPushNotificationService Task PushAuthRequestResponseAsync(AuthRequest authRequest); Task PushSyncOrganizationStatusAsync(Organization organization); Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization); + + Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null); Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null); Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, diff --git a/src/Core/Platform/Push/Services/IPushRegistrationService.cs b/src/Core/Platform/Push/Services/IPushRegistrationService.cs index 0c4271f061..0f2a28700b 100644 --- a/src/Core/Platform/Push/Services/IPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/IPushRegistrationService.cs @@ -5,7 +5,7 @@ namespace Bit.Core.Platform.Push; public interface IPushRegistrationService { Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds); + string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId); Task DeleteRegistrationAsync(string deviceId); Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs index 9b4e66ae1a..1f88f5dcc6 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs @@ -157,6 +157,14 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.CompletedTask; } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) + { + PushToServices((s) => + s.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType)); + return Task.CompletedTask; + } + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs index 57c446c5e5..e005f9d7af 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs @@ -108,14 +108,17 @@ public class NoopPushNotificationService : IPushNotificationService return Task.FromResult(0); } + public Task PushNotificationAsync(Notification notification) => Task.CompletedTask; + + public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) => + Task.CompletedTask; + + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => Task.CompletedTask; + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { return Task.FromResult(0); } - - public Task PushNotificationAsync(Notification notification) => Task.CompletedTask; - - public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) => - Task.CompletedTask; } diff --git a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs index 6bcf9e893a..ac6f8a814b 100644 --- a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs @@ -10,7 +10,7 @@ public class NoopPushRegistrationService : IPushRegistrationService } public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds) + string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { return Task.FromResult(0); } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index 7a557e8978..2833c43985 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -15,8 +15,14 @@ using Microsoft.Extensions.Logging; // This service is not in the `Internal` namespace because it has direct external references. namespace Bit.Core.Platform.Push; +/// +/// Sends non-mobile push notifications to the Azure Queue Api, later received by Notifications Api. +/// Used by Cloud-Hosted environments. +/// Received by AzureQueueHostedService message receiver in Notifications project. +/// public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService { + private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; public NotificationsApiPushNotificationService( @@ -33,6 +39,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService globalSettings.InternalIdentityKey, logger) { + _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; } @@ -193,13 +200,14 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; - await SendMessageAsync(PushType.SyncNotification, message, true); + await SendMessageAsync(PushType.Notification, message, true); } public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) @@ -212,6 +220,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, @@ -220,7 +229,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService DeletedDate = notificationStatus.DeletedDate }; - await SendMessageAsync(PushType.SyncNotificationStatus, message, true); + await SendMessageAsync(PushType.NotificationStatus, message, true); } private async Task PushSendAsync(Send send, PushType type) @@ -257,6 +266,11 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService return currentContext?.DeviceIdentifier; } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => + // Noop + Task.CompletedTask; + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index 09f42fd0d1..d111efa2a8 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -17,9 +17,15 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push.Internal; +/// +/// Sends mobile push notifications to the Bitwarden Cloud API, then relayed to Azure Notification Hub. +/// Used by Self-Hosted environments. +/// Received by PushController endpoint in Api project. +/// public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService { private readonly IDeviceRepository _deviceRepository; + private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; public RelayPushNotificationService( @@ -38,6 +44,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti logger) { _deviceRepository = deviceRepository; + _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; } @@ -202,22 +209,31 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, RevisionDate = notification.RevisionDate }; - if (notification.UserId.HasValue) + if (notification.Global) { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotification, message, true, + await SendPayloadToInstallationAsync(PushType.Notification, message, true, notification.ClientType); + } + else if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotification, message, + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message, true, notification.ClientType); } + else + { + _logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id); + } } public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) @@ -230,6 +246,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, @@ -238,16 +255,24 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti DeletedDate = notificationStatus.DeletedDate }; - if (notification.UserId.HasValue) + if (notification.Global) { - await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationStatus, message, true, + await SendPayloadToInstallationAsync(PushType.NotificationStatus, message, true, notification.ClientType); + } + else if (notification.UserId.HasValue) + { + await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationStatus, message, + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, message, true, notification.ClientType); } + else + { + _logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id); + } } public async Task PushSyncOrganizationStatusAsync(Organization organization) @@ -275,6 +300,21 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti false ); + private async Task SendPayloadToInstallationAsync(PushType type, object payload, bool excludeCurrentContext, + ClientType? clientType = null) + { + var request = new PushSendRequestModel + { + InstallationId = _globalSettings.Installation.Id.ToString(), + Type = type, + Payload = payload, + ClientType = clientType + }; + + await AddCurrentContextAsync(request, excludeCurrentContext); + await SendAsync(HttpMethod.Post, "push/send", request); + } + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, ClientType? clientType = null) { @@ -324,6 +364,10 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti } } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => + throw new NotImplementedException(); + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs index b838fbde59..58a34c15c5 100644 --- a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs @@ -25,7 +25,7 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds) + string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { var requestModel = new PushRegistrationRequestModel { @@ -34,7 +34,8 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi PushToken = pushToken, Type = type, UserId = userId, - OrganizationIds = organizationIds + OrganizationIds = organizationIds, + InstallationId = installationId }; await SendAsync(HttpMethod.Post, "push/register", requestModel); } diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index 28823eeda7..c8b0134932 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -5,6 +5,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Repositories; +using Bit.Core.Settings; namespace Bit.Core.Services; @@ -13,15 +14,18 @@ public class DeviceService : IDeviceService private readonly IDeviceRepository _deviceRepository; private readonly IPushRegistrationService _pushRegistrationService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IGlobalSettings _globalSettings; public DeviceService( IDeviceRepository deviceRepository, IPushRegistrationService pushRegistrationService, - IOrganizationUserRepository organizationUserRepository) + IOrganizationUserRepository organizationUserRepository, + IGlobalSettings globalSettings) { _deviceRepository = deviceRepository; _pushRegistrationService = pushRegistrationService; _organizationUserRepository = organizationUserRepository; + _globalSettings = globalSettings; } public async Task SaveAsync(Device device) @@ -42,7 +46,8 @@ public class DeviceService : IDeviceService .Select(ou => ou.OrganizationId.ToString()); await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(), - device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString); + device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString, + _globalSettings.Installation.Id); } public async Task ClearTokenAsync(Device device) diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index af571e48c4..8fa74f7b84 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -93,40 +93,45 @@ public static class HubHelpers var orgStatusNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.Group($"Organization_{orgStatusNotification.Payload.OrganizationId}") - .SendAsync("ReceiveMessage", orgStatusNotification, cancellationToken); + await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(orgStatusNotification.Payload.OrganizationId)) + .SendAsync(_receiveMessageMethod, orgStatusNotification, cancellationToken); break; case PushType.SyncOrganizationCollectionSettingChanged: var organizationCollectionSettingsChangedNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.Group($"Organization_{organizationCollectionSettingsChangedNotification.Payload.OrganizationId}") - .SendAsync("ReceiveMessage", organizationCollectionSettingsChangedNotification, cancellationToken); + await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification.Payload.OrganizationId)) + .SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, cancellationToken); break; - case PushType.SyncNotification: - case PushType.SyncNotificationStatus: - var syncNotification = - JsonSerializer.Deserialize>( - notificationJson, _deserializerOptions); - if (syncNotification.Payload.UserId.HasValue) + case PushType.Notification: + case PushType.NotificationStatus: + var notificationData = JsonSerializer.Deserialize>( + notificationJson, _deserializerOptions); + if (notificationData.Payload.InstallationId.HasValue) { - if (syncNotification.Payload.ClientType == ClientType.All) + await hubContext.Clients.Group(NotificationsHub.GetInstallationGroup( + notificationData.Payload.InstallationId.Value, notificationData.Payload.ClientType)) + .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); + } + else if (notificationData.Payload.UserId.HasValue) + { + if (notificationData.Payload.ClientType == ClientType.All) { - await hubContext.Clients.User(syncNotification.Payload.UserId.ToString()) - .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + await hubContext.Clients.User(notificationData.Payload.UserId.ToString()) + .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } else { await hubContext.Clients.Group(NotificationsHub.GetUserGroup( - syncNotification.Payload.UserId.Value, syncNotification.Payload.ClientType)) - .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + notificationData.Payload.UserId.Value, notificationData.Payload.ClientType)) + .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } } - else if (syncNotification.Payload.OrganizationId.HasValue) + else if (notificationData.Payload.OrganizationId.HasValue) { await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup( - syncNotification.Payload.OrganizationId.Value, syncNotification.Payload.ClientType)) - .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + notificationData.Payload.OrganizationId.Value, notificationData.Payload.ClientType)) + .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } break; diff --git a/src/Notifications/NotificationsHub.cs b/src/Notifications/NotificationsHub.cs index 27cd19c0a0..ed62dbbd66 100644 --- a/src/Notifications/NotificationsHub.cs +++ b/src/Notifications/NotificationsHub.cs @@ -29,6 +29,16 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub await Groups.AddToGroupAsync(Context.ConnectionId, GetUserGroup(currentContext.UserId.Value, clientType)); } + if (_globalSettings.Installation.Id != Guid.Empty) + { + await Groups.AddToGroupAsync(Context.ConnectionId, GetInstallationGroup(_globalSettings.Installation.Id)); + if (clientType != ClientType.All) + { + await Groups.AddToGroupAsync(Context.ConnectionId, + GetInstallationGroup(_globalSettings.Installation.Id, clientType)); + } + } + if (currentContext.Organizations != null) { foreach (var org in currentContext.Organizations) @@ -57,6 +67,17 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub GetUserGroup(currentContext.UserId.Value, clientType)); } + if (_globalSettings.Installation.Id != Guid.Empty) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, + GetInstallationGroup(_globalSettings.Installation.Id)); + if (clientType != ClientType.All) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, + GetInstallationGroup(_globalSettings.Installation.Id, clientType)); + } + } + if (currentContext.Organizations != null) { foreach (var org in currentContext.Organizations) @@ -73,6 +94,13 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub await base.OnDisconnectedAsync(exception); } + public static string GetInstallationGroup(Guid installationId, ClientType? clientType = null) + { + return clientType is null or ClientType.All + ? $"Installation_{installationId}" + : $"Installation_ClientType_{installationId}_{clientType}"; + } + public static string GetUserGroup(Guid userId, ClientType clientType) { return $"UserClientType_{userId}_{clientType}"; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5a1205c961..85bd014c91 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -282,9 +282,13 @@ public static class ServiceCollectionExtensions services.AddSingleton(); if (globalSettings.SelfHosted) { + if (globalSettings.Installation.Id == Guid.Empty) + { + throw new InvalidOperationException("Installation Id must be set for self-hosted installations."); + } + if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && - globalSettings.Installation?.Id != null && - CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) + CoreHelpers.SettingHasValue(globalSettings.Installation.Key)) { services.AddKeyedSingleton("implementation"); services.AddSingleton(); @@ -300,7 +304,7 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("implementation"); } } - else if (!globalSettings.SelfHosted) + else { services.AddSingleton(); services.AddSingleton(); diff --git a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs new file mode 100644 index 0000000000..70e1e83edb --- /dev/null +++ b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs @@ -0,0 +1,288 @@ +#nullable enable +using Bit.Api.Platform.Push; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using Bit.Core.Platform.Push; +using Bit.Core.Settings; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Platform.Push.Controllers; + +[ControllerCustomize(typeof(PushController))] +[SutProviderCustomize] +public class PushControllerTests +{ + [Theory] + [BitAutoData(false, true)] + [BitAutoData(false, false)] + [BitAutoData(true, true)] + public async Task SendAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, bool selfHosted, + SutProvider sutProvider, Guid installationId, Guid userId, Guid organizationId) + { + sutProvider.GetDependency().SelfHosted = selfHosted; + if (haveInstallationId) + { + sutProvider.GetDependency().InstallationId.Returns(installationId); + } + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = userId.ToString(), + OrganizationId = organizationId.ToString(), + InstallationId = installationId.ToString(), + Payload = "test-payload" + })); + + Assert.Equal("Not correctly configured for push relays.", exception.Message); + + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task SendAsync_UserIdAndOrganizationIdAndInstallationIdEmpty_NoPushNotificationSent( + SutProvider sutProvider, Guid installationId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + await sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = null, + OrganizationId = null, + InstallationId = null, + Payload = "test-payload" + }); + + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] + public async Task SendAsync_UserIdSet_SendPayloadToUserAsync(bool haveIdentifier, bool haveDeviceId, + bool haveOrganizationId, SutProvider sutProvider, Guid installationId, Guid userId, + Guid identifier, Guid deviceId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var expectedUserId = $"{installationId}_{userId}"; + var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; + var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; + + await sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = userId.ToString(), + OrganizationId = haveOrganizationId ? Guid.NewGuid().ToString() : null, + InstallationId = null, + Payload = "test-payload", + DeviceId = haveDeviceId ? deviceId.ToString() : null, + Identifier = haveIdentifier ? identifier.ToString() : null, + ClientType = ClientType.All, + }); + + await sutProvider.GetDependency().Received(1) + .SendPayloadToUserAsync(expectedUserId, PushType.Notification, "test-payload", expectedIdentifier, + expectedDeviceId, ClientType.All); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [RepeatingPatternBitAutoData([false, true], [false, true])] + public async Task SendAsync_OrganizationIdSet_SendPayloadToOrganizationAsync(bool haveIdentifier, bool haveDeviceId, + SutProvider sutProvider, Guid installationId, Guid organizationId, Guid identifier, + Guid deviceId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var expectedOrganizationId = $"{installationId}_{organizationId}"; + var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; + var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; + + await sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = null, + OrganizationId = organizationId.ToString(), + InstallationId = null, + Payload = "test-payload", + DeviceId = haveDeviceId ? deviceId.ToString() : null, + Identifier = haveIdentifier ? identifier.ToString() : null, + ClientType = ClientType.All, + }); + + await sutProvider.GetDependency().Received(1) + .SendPayloadToOrganizationAsync(expectedOrganizationId, PushType.Notification, "test-payload", + expectedIdentifier, expectedDeviceId, ClientType.All); + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [RepeatingPatternBitAutoData([false, true], [false, true])] + public async Task SendAsync_InstallationIdSet_SendPayloadToInstallationAsync(bool haveIdentifier, bool haveDeviceId, + SutProvider sutProvider, Guid installationId, Guid identifier, Guid deviceId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; + var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; + + await sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = null, + OrganizationId = null, + InstallationId = installationId.ToString(), + Payload = "test-payload", + DeviceId = haveDeviceId ? deviceId.ToString() : null, + Identifier = haveIdentifier ? identifier.ToString() : null, + ClientType = ClientType.All, + }); + + await sutProvider.GetDependency().Received(1) + .SendPayloadToInstallationAsync(installationId.ToString(), PushType.Notification, "test-payload", + expectedIdentifier, expectedDeviceId, ClientType.All); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task SendAsync_InstallationIdNotMatching_BadRequest(SutProvider sutProvider, + Guid installationId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.Notification, + UserId = null, + OrganizationId = null, + InstallationId = Guid.NewGuid().ToString(), + Payload = "test-payload", + DeviceId = null, + Identifier = null, + ClientType = ClientType.All, + })); + + Assert.Equal("InstallationId does not match current context.", exception.Message); + + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(false, true)] + [BitAutoData(false, false)] + [BitAutoData(true, true)] + public async Task RegisterAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, + bool selfHosted, + SutProvider sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId) + { + sutProvider.GetDependency().SelfHosted = selfHosted; + if (haveInstallationId) + { + sutProvider.GetDependency().InstallationId.Returns(installationId); + } + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterAsync(new PushRegistrationRequestModel + { + DeviceId = deviceId.ToString(), + PushToken = "test-push-token", + UserId = userId.ToString(), + Type = DeviceType.Android, + Identifier = identifier.ToString() + })); + + Assert.Equal("Not correctly configured for push relays.", exception.Message); + + await sutProvider.GetDependency().Received(0) + .CreateOrUpdateRegistrationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task? RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(SutProvider sutProvider, + Guid installationId, Guid userId, Guid identifier, Guid deviceId, Guid organizationId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var expectedUserId = $"{installationId}_{userId}"; + var expectedIdentifier = $"{installationId}_{identifier}"; + var expectedDeviceId = $"{installationId}_{deviceId}"; + var expectedOrganizationId = $"{installationId}_{organizationId}"; + + await sutProvider.Sut.RegisterAsync(new PushRegistrationRequestModel + { + DeviceId = deviceId.ToString(), + PushToken = "test-push-token", + UserId = userId.ToString(), + Type = DeviceType.Android, + Identifier = identifier.ToString(), + OrganizationIds = [organizationId.ToString()], + InstallationId = installationId + }); + + await sutProvider.GetDependency().Received(1) + .CreateOrUpdateRegistrationAsync("test-push-token", expectedDeviceId, expectedUserId, + expectedIdentifier, DeviceType.Android, Arg.Do>(organizationIds => + { + var organizationIdsList = organizationIds.ToList(); + Assert.Contains(expectedOrganizationId, organizationIdsList); + Assert.Single(organizationIdsList); + }), installationId); + } +} diff --git a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs index 41a6c25bf2..2d3dbffcf6 100644 --- a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs +++ b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs @@ -12,19 +12,15 @@ namespace Bit.Core.Test.Models.Api.Request; public class PushSendRequestModelTests { [Theory] - [InlineData(null, null)] - [InlineData(null, "")] - [InlineData(null, " ")] - [InlineData("", null)] - [InlineData(" ", null)] - [InlineData("", "")] - [InlineData(" ", " ")] - public void Validate_UserIdOrganizationIdNullOrEmpty_Invalid(string? userId, string? organizationId) + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "], [null, "", " "])] + public void Validate_UserIdOrganizationIdInstallationIdNullOrEmpty_Invalid(string? userId, string? organizationId, + string? installationId) { var model = new PushSendRequestModel { UserId = userId, OrganizationId = organizationId, + InstallationId = installationId, Type = PushType.SyncCiphers, Payload = "test" }; @@ -32,7 +28,65 @@ public class PushSendRequestModelTests var results = Validate(model); Assert.Single(results); - Assert.Contains(results, result => result.ErrorMessage == "UserId or OrganizationId is required."); + Assert.Contains(results, + result => result.ErrorMessage == "UserId or OrganizationId or InstallationId is required."); + } + + [Theory] + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] + public void Validate_UserIdProvidedOrganizationIdInstallationIdNullOrEmpty_Valid(string? organizationId, + string? installationId) + { + var model = new PushSendRequestModel + { + UserId = Guid.NewGuid().ToString(), + OrganizationId = organizationId, + InstallationId = installationId, + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Empty(results); + } + + [Theory] + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] + public void Validate_OrganizationIdProvidedUserIdInstallationIdNullOrEmpty_Valid(string? userId, + string? installationId) + { + var model = new PushSendRequestModel + { + UserId = userId, + OrganizationId = Guid.NewGuid().ToString(), + InstallationId = installationId, + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Empty(results); + } + + [Theory] + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] + public void Validate_InstallationIdProvidedUserIdOrganizationIdNullOrEmpty_Valid(string? userId, + string? organizationId) + { + var model = new PushSendRequestModel + { + UserId = userId, + OrganizationId = organizationId, + InstallationId = Guid.NewGuid().ToString(), + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Empty(results); } [Theory] diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index 2b8ff88dc1..831d048224 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationHub; using Bit.Core.Repositories; +using Bit.Core.Settings; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -21,9 +22,11 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize] - public async Task PushNotificationAsync_Global_NotSent( + public async Task PushNotificationAsync_GlobalInstallationIdDefault_NotSent( SutProvider sutProvider, Notification notification) { + sutProvider.GetDependency().Installation.Id = default; + await sutProvider.Sut.PushNotificationAsync(notification); await sutProvider.GetDependency() @@ -36,6 +39,50 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } + [Theory] + [BitAutoData] + [NotificationCustomize] + public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId( + SutProvider sutProvider, Notification notification, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = ClientType.All; + var expectedNotification = ToNotificationPushNotification(notification, null, installationId); + + await sutProvider.Sut.PushNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, + expectedNotification, + $"(template:payload && installationId:{installationId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize] + public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType( + ClientType clientType, SutProvider sutProvider, + Notification notification, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = clientType; + var expectedNotification = ToNotificationPushNotification(notification, null, installationId); + + await sutProvider.Sut.PushNotificationAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, + expectedNotification, + $"(template:payload && installationId:{installationId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + [Theory] [BitAutoData(false)] [BitAutoData(true)] @@ -50,11 +97,11 @@ public class NotificationHubPushNotificationServiceTests } notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, null); + var expectedNotification = ToNotificationPushNotification(notification, null, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, expectedNotification, $"(template:payload_userId:{notification.UserId})"); await sutProvider.GetDependency() @@ -74,11 +121,11 @@ public class NotificationHubPushNotificationServiceTests { notification.OrganizationId = null; notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null); + var expectedNotification = ToNotificationPushNotification(notification, null, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, expectedNotification, $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); await sutProvider.GetDependency() @@ -97,11 +144,11 @@ public class NotificationHubPushNotificationServiceTests Notification notification) { notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null); + var expectedNotification = ToNotificationPushNotification(notification, null, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, expectedNotification, $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); await sutProvider.GetDependency() @@ -117,11 +164,11 @@ public class NotificationHubPushNotificationServiceTests { notification.UserId = null; notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, null); + var expectedNotification = ToNotificationPushNotification(notification, null, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, expectedNotification, $"(template:payload && organizationId:{notification.OrganizationId})"); await sutProvider.GetDependency() @@ -141,11 +188,11 @@ public class NotificationHubPushNotificationServiceTests { notification.UserId = null; notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, null); + var expectedNotification = ToNotificationPushNotification(notification, null, null); await sutProvider.Sut.PushNotificationAsync(notification); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotification, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification, expectedNotification, $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); await sutProvider.GetDependency() @@ -156,10 +203,12 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize] - public async Task PushNotificationStatusAsync_Global_NotSent( + public async Task PushNotificationStatusAsync_GlobalInstallationIdDefault_NotSent( SutProvider sutProvider, Notification notification, NotificationStatus notificationStatus) { + sutProvider.GetDependency().Installation.Id = default; + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); await sutProvider.GetDependency() @@ -172,6 +221,54 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } + [Theory] + [BitAutoData] + [NotificationCustomize] + public async Task PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = ClientType.All; + + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, + expectedNotification, + $"(template:payload && installationId:{installationId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize] + public async Task + PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType( + ClientType clientType, SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = clientType; + + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId); + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, + expectedNotification, + $"(template:payload && installationId:{installationId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + [Theory] [BitAutoData(false)] [BitAutoData(true)] @@ -186,11 +283,11 @@ public class NotificationHubPushNotificationServiceTests } notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, expectedNotification, $"(template:payload_userId:{notification.UserId})"); await sutProvider.GetDependency() @@ -210,11 +307,11 @@ public class NotificationHubPushNotificationServiceTests { notification.OrganizationId = null; notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, expectedNotification, $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); await sutProvider.GetDependency() @@ -233,11 +330,11 @@ public class NotificationHubPushNotificationServiceTests Notification notification, NotificationStatus notificationStatus) { notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, expectedNotification, $"(template:payload_userId:{notification.UserId} && clientType:{clientType})"); await sutProvider.GetDependency() @@ -254,11 +351,11 @@ public class NotificationHubPushNotificationServiceTests { notification.UserId = null; notification.ClientType = ClientType.All; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, expectedNotification, $"(template:payload && organizationId:{notification.OrganizationId})"); await sutProvider.GetDependency() @@ -279,11 +376,11 @@ public class NotificationHubPushNotificationServiceTests { notification.UserId = null; notification.ClientType = clientType; - var expectedNotification = ToNotificationPushNotification(notification, notificationStatus); + var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null); await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); - await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationStatus, + await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus, expectedNotification, $"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})"); await sutProvider.GetDependency() @@ -363,8 +460,44 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } + [Theory] + [BitAutoData([null])] + [BitAutoData(ClientType.All)] + public async Task SendPayloadToInstallationAsync_ClientTypeNullOrAll_SentToInstallation(ClientType? clientType, + SutProvider sutProvider, Guid installationId, PushType pushType, + string payload, string identifier) + { + await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier, + null, clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Mobile)] + [BitAutoData(ClientType.Web)] + public async Task SendPayloadToInstallationAsync_ClientTypeExplicit_SentToInstallationAndClientType( + ClientType clientType, SutProvider sutProvider, Guid installationId, + PushType pushType, string payload, string identifier) + { + await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier, + null, clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + private static NotificationPushNotification ToNotificationPushNotification(Notification notification, - NotificationStatus? notificationStatus) => + NotificationStatus? notificationStatus, Guid? installationId) => new() { Id = notification.Id, @@ -373,6 +506,7 @@ public class NotificationHubPushNotificationServiceTests ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = installationId, Title = notification.Title, Body = notification.Body, CreationDate = notification.CreationDate, diff --git a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs index d51df9c882..77551f53e7 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs @@ -14,15 +14,13 @@ namespace Bit.Core.Test.NotificationHub; public class NotificationHubPushRegistrationServiceTests { [Theory] - [BitAutoData([null])] - [BitAutoData("")] - [BitAutoData(" ")] + [RepeatingPatternBitAutoData([null, "", " "])] public async Task CreateOrUpdateRegistrationAsync_PushTokenNullOrEmpty_InstallationNotCreated(string? pushToken, SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, - Guid organizationId) + Guid organizationId, Guid installationId) { await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), - identifier.ToString(), DeviceType.Android, [organizationId.ToString()]); + identifier.ToString(), DeviceType.Android, [organizationId.ToString()], installationId); sutProvider.GetDependency() .Received(0) @@ -30,13 +28,11 @@ public class NotificationHubPushRegistrationServiceTests } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroid_InstallationCreated(bool identifierNull, - bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, - Guid userId, Guid? identifier, Guid organizationId) + bool partOfOrganizationId, bool installationIdNull, + SutProvider sutProvider, Guid deviceId, Guid userId, Guid? identifier, + Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -45,7 +41,8 @@ public class NotificationHubPushRegistrationServiceTests await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.Android, - partOfOrganizationId ? [organizationId.ToString()] : []); + partOfOrganizationId ? [organizationId.ToString()] : [], + installationIdNull ? Guid.Empty : installationId); sutProvider.GetDependency() .Received(1) @@ -60,6 +57,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains("clientType:Mobile") && (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + (installationIdNull || installation.Tags.Contains($"installationId:{installationId}")) && installation.Templates.Count == 3)); await notificationHubClient .Received(1) @@ -73,6 +71,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -86,6 +85,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -99,17 +99,16 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeIOS_InstallationCreated(bool identifierNull, - bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, - Guid userId, Guid identifier, Guid organizationId) + bool partOfOrganizationId, bool installationIdNull, + SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, + Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -118,7 +117,8 @@ public class NotificationHubPushRegistrationServiceTests await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.iOS, - partOfOrganizationId ? [organizationId.ToString()] : []); + partOfOrganizationId ? [organizationId.ToString()] : [], + installationIdNull ? Guid.Empty : installationId); sutProvider.GetDependency() .Received(1) @@ -133,6 +133,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains("clientType:Mobile") && (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + (installationIdNull || installation.Tags.Contains($"installationId:{installationId}")) && installation.Templates.Count == 3)); await notificationHubClient .Received(1) @@ -146,6 +147,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -159,6 +161,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -172,17 +175,16 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroidAmazon_InstallationCreated(bool identifierNull, - bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, - Guid userId, Guid identifier, Guid organizationId) + bool partOfOrganizationId, bool installationIdNull, + SutProvider sutProvider, Guid deviceId, + Guid userId, Guid identifier, Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -191,7 +193,8 @@ public class NotificationHubPushRegistrationServiceTests await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.AndroidAmazon, - partOfOrganizationId ? [organizationId.ToString()] : []); + partOfOrganizationId ? [organizationId.ToString()] : [], + installationIdNull ? Guid.Empty : installationId); sutProvider.GetDependency() .Received(1) @@ -206,6 +209,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains("clientType:Mobile") && (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + (installationIdNull || installation.Tags.Contains($"installationId:{installationId}")) && installation.Templates.Count == 3)); await notificationHubClient .Received(1) @@ -219,6 +223,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -232,6 +237,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -245,6 +251,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); } @@ -254,7 +261,7 @@ public class NotificationHubPushRegistrationServiceTests [BitAutoData(DeviceType.MacOsDesktop)] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeNotMobile_InstallationCreated(DeviceType deviceType, SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, - Guid organizationId) + Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -262,7 +269,7 @@ public class NotificationHubPushRegistrationServiceTests var pushToken = "test push token"; await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), - identifier.ToString(), deviceType, [organizationId.ToString()]); + identifier.ToString(), deviceType, [organizationId.ToString()], installationId); sutProvider.GetDependency() .Received(1) @@ -276,6 +283,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains($"clientType:{DeviceTypes.ToClientType(deviceType)}") && installation.Tags.Contains($"deviceIdentifier:{identifier}") && installation.Tags.Contains($"organizationId:{organizationId}") && + installation.Tags.Contains($"installationId:{installationId}") && installation.Templates.Count == 0)); } diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index 22161924ea..3025197c66 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -5,6 +5,8 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Settings; using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.CurrentContextFixtures; using Bit.Core.Test.NotificationCenter.AutoFixture; @@ -14,7 +16,7 @@ using Microsoft.AspNetCore.Http; using NSubstitute; using Xunit; -namespace Bit.Core.Platform.Push.Internal.Test; +namespace Bit.Core.Test.Platform.Push.Services; [QueueClientCustomize] [SutProviderCustomize] @@ -24,20 +26,43 @@ public class AzureQueuePushNotificationServiceTests [BitAutoData] [NotificationCustomize] [CurrentContextCustomize] - public async Task PushNotificationAsync_Notification_Sent( + public async Task PushNotificationAsync_NotificationGlobal_Sent( SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext) + ICurrentContext currentContext, Guid installationId) { currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); sutProvider.GetDependency().HttpContext!.RequestServices .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; await sutProvider.Sut.PushNotificationAsync(notification); await sutProvider.GetDependency().Received(1) .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.SyncNotification, message, - new NotificationPushNotificationEquals(notification, null), + MatchMessage(PushType.Notification, message, + new NotificationPushNotificationEquals(notification, null, installationId), + deviceIdentifier.ToString()))); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(false)] + [CurrentContextCustomize] + public async Task PushNotificationAsync_NotificationNotGlobal_Sent( + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, Guid installationId) + { + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; + + await sutProvider.Sut.PushNotificationAsync(notification); + + await sutProvider.GetDependency().Received(1) + .SendMessageAsync(Arg.Is(message => + MatchMessage(PushType.Notification, message, + new NotificationPushNotificationEquals(notification, null, null), deviceIdentifier.ToString()))); } @@ -46,20 +71,44 @@ public class AzureQueuePushNotificationServiceTests [NotificationCustomize] [NotificationStatusCustomize] [CurrentContextCustomize] - public async Task PushNotificationStatusAsync_Notification_Sent( + public async Task PushNotificationStatusAsync_NotificationGlobal_Sent( SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext, NotificationStatus notificationStatus) + ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId) { currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); sutProvider.GetDependency().HttpContext!.RequestServices .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); await sutProvider.GetDependency().Received(1) .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.SyncNotificationStatus, message, - new NotificationPushNotificationEquals(notification, notificationStatus), + MatchMessage(PushType.NotificationStatus, message, + new NotificationPushNotificationEquals(notification, notificationStatus, installationId), + deviceIdentifier.ToString()))); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(false)] + [NotificationStatusCustomize] + [CurrentContextCustomize] + public async Task PushNotificationStatusAsync_NotificationNotGlobal_Sent( + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId) + { + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; + + await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus); + + await sutProvider.GetDependency().Received(1) + .SendMessageAsync(Arg.Is(message => + MatchMessage(PushType.NotificationStatus, message, + new NotificationPushNotificationEquals(notification, notificationStatus, null), deviceIdentifier.ToString()))); } @@ -73,7 +122,10 @@ public class AzureQueuePushNotificationServiceTests pushNotificationData.ContextId == contextId; } - private class NotificationPushNotificationEquals(Notification notification, NotificationStatus? notificationStatus) + private class NotificationPushNotificationEquals( + Notification notification, + NotificationStatus? notificationStatus, + Guid? installationId) : IEquatable { public bool Equals(NotificationPushNotification? other) @@ -87,6 +139,8 @@ public class AzureQueuePushNotificationServiceTests other.UserId == notification.UserId && other.OrganizationId.HasValue == notification.OrganizationId.HasValue && other.OrganizationId == notification.OrganizationId && + other.ClientType == notification.ClientType && + other.InstallationId == installationId && other.Title == notification.Title && other.Body == notification.Body && other.CreationDate == notification.CreationDate && diff --git a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs index 08dfd0a5c0..68acf7ec72 100644 --- a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs @@ -1,13 +1,15 @@ #nullable enable using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Platform.Push.Internal.Test; +namespace Bit.Core.Test.Platform.Push.Services; [SutProviderCustomize] public class MultiServicePushNotificationServiceTests @@ -75,4 +77,22 @@ public class MultiServicePushNotificationServiceTests .Received(1) .SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, clientType); } + + [Theory] + [BitAutoData([null, null])] + [BitAutoData(ClientType.All, null)] + [BitAutoData([null, "test device id"])] + [BitAutoData(ClientType.All, "test device id")] + public async Task SendPayloadToInstallationAsync_Message_Sent(ClientType? clientType, string? deviceId, + string installationId, PushType type, object payload, string identifier, + SutProvider sutProvider) + { + await sutProvider.Sut.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, + clientType); + + await sutProvider.GetDependency>() + .First() + .Received(1) + .SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType); + } } diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs index 78f60da359..07f348a5ba 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs @@ -1,10 +1,11 @@ -using Bit.Core.Settings; +using Bit.Core.Platform.Push; +using Bit.Core.Settings; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -namespace Bit.Core.Platform.Push.Internal.Test; +namespace Bit.Core.Test.Platform.Push.Services; public class NotificationsApiPushNotificationServiceTests { diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index 61d7f0a788..9ae79f7142 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -1,11 +1,12 @@ -using Bit.Core.Repositories; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Repositories; using Bit.Core.Settings; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -namespace Bit.Core.Platform.Push.Internal.Test; +namespace Bit.Core.Test.Platform.Push.Services; public class RelayPushNotificationServiceTests { diff --git a/test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs index cfd843d2eb..062b4a96a8 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs @@ -1,9 +1,10 @@ -using Bit.Core.Settings; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Settings; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; -namespace Bit.Core.Platform.Push.Internal.Test; +namespace Bit.Core.Test.Platform.Push.Services; public class RelayPushRegistrationServiceTests { diff --git a/test/Core.Test/Services/DeviceServiceTests.cs b/test/Core.Test/Services/DeviceServiceTests.cs index 98b04eb7d3..95a93cf4e8 100644 --- a/test/Core.Test/Services/DeviceServiceTests.cs +++ b/test/Core.Test/Services/DeviceServiceTests.cs @@ -7,6 +7,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -20,7 +21,7 @@ public class DeviceServiceTests [Theory] [BitAutoData] public async Task SaveAsync_IdProvided_UpdatedRevisionDateAndPushRegistration(Guid id, Guid userId, - Guid organizationId1, Guid organizationId2, + Guid organizationId1, Guid organizationId2, Guid installationId, OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) { @@ -32,7 +33,9 @@ public class DeviceServiceTests var organizationUserRepository = Substitute.For(); organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any(), Arg.Any()) .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); - var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository); + var globalSettings = Substitute.For(); + globalSettings.Installation.Id.Returns(installationId); + var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings); var device = new Device { @@ -54,13 +57,13 @@ public class DeviceServiceTests Assert.Equal(2, organizationIdsList.Count); Assert.Contains(organizationId1.ToString(), organizationIdsList); Assert.Contains(organizationId2.ToString(), organizationIdsList); - })); + }), installationId); } [Theory] [BitAutoData] public async Task SaveAsync_IdNotProvided_CreatedAndPushRegistration(Guid userId, Guid organizationId1, - Guid organizationId2, + Guid organizationId2, Guid installationId, OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) { @@ -72,7 +75,9 @@ public class DeviceServiceTests var organizationUserRepository = Substitute.For(); organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any(), Arg.Any()) .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); - var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository); + var globalSettings = Substitute.For(); + globalSettings.Installation.Id.Returns(installationId); + var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings); var device = new Device { @@ -92,7 +97,7 @@ public class DeviceServiceTests Assert.Equal(2, organizationIdsList.Count); Assert.Contains(organizationId1.ToString(), organizationIdsList); Assert.Contains(organizationId2.ToString(), organizationIdsList); - })); + }), installationId); } /// From 4bef2357d59c69eb366229f5d0e60ab7a2af1fa4 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 20 Feb 2025 16:01:48 +0100 Subject: [PATCH 09/84] [PM-18028] Enabling automatic tax for customers without country or with manual tax rates set (#5376) --- .../Implementations/UpcomingInvoiceHandler.cs | 13 +++--- .../Billing/Extensions/CustomerExtensions.cs | 16 +++++++ .../SubscriptionCreateOptionsExtensions.cs | 26 +++++++++++ .../SubscriptionUpdateOptionsExtensions.cs | 35 +++++++++++++++ .../UpcomingInvoiceOptionsExtensions.cs | 35 +++++++++++++++ .../PremiumUserBillingService.cs | 2 +- .../Implementations/SubscriberService.cs | 20 ++++++--- .../Implementations/StripePaymentService.cs | 43 +++++++++---------- 8 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 src/Core/Billing/Extensions/CustomerExtensions.cs create mode 100644 src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs create mode 100644 src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs create mode 100644 src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index c52c03b6aa..409bd0d18b 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,6 +1,7 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Extensions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -160,16 +161,16 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler private async Task TryEnableAutomaticTaxAsync(Subscription subscription) { - if (subscription.AutomaticTax.Enabled) + var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; + var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions); + + var subscriptionUpdateOptions = new SubscriptionUpdateOptions(); + + if (!subscriptionUpdateOptions.EnableAutomaticTax(customer, subscription)) { return subscription; } - var subscriptionUpdateOptions = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }; - return await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); } diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs new file mode 100644 index 0000000000..62f1a5055c --- /dev/null +++ b/src/Core/Billing/Extensions/CustomerExtensions.cs @@ -0,0 +1,16 @@ +using Bit.Core.Billing.Constants; +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class CustomerExtensions +{ + + /// + /// Determines if a Stripe customer supports automatic tax + /// + /// + /// + public static bool HasTaxLocationVerified(this Customer customer) => + customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; +} diff --git a/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs new file mode 100644 index 0000000000..d76a0553a3 --- /dev/null +++ b/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs @@ -0,0 +1,26 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class SubscriptionCreateOptionsExtensions +{ + /// + /// Attempts to enable automatic tax for given new subscription options. + /// + /// + /// The existing customer. + /// Returns true when successful, false when conditions are not met. + public static bool EnableAutomaticTax(this SubscriptionCreateOptions options, Customer customer) + { + // We might only need to check the automatic tax status. + if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + { + return false; + } + + options.DefaultTaxRates = []; + options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + + return true; + } +} diff --git a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs new file mode 100644 index 0000000000..d70af78fa8 --- /dev/null +++ b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs @@ -0,0 +1,35 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class SubscriptionUpdateOptionsExtensions +{ + /// + /// Attempts to enable automatic tax for given subscription options. + /// + /// + /// The existing customer to which the subscription belongs. + /// The existing subscription. + /// Returns true when successful, false when conditions are not met. + public static bool EnableAutomaticTax( + this SubscriptionUpdateOptions options, + Customer customer, + Subscription subscription) + { + if (subscription.AutomaticTax.Enabled) + { + return false; + } + + // We might only need to check the automatic tax status. + if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + { + return false; + } + + options.DefaultTaxRates = []; + options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + + return true; + } +} diff --git a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs new file mode 100644 index 0000000000..88df5638c9 --- /dev/null +++ b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs @@ -0,0 +1,35 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class UpcomingInvoiceOptionsExtensions +{ + /// + /// Attempts to enable automatic tax for given upcoming invoice options. + /// + /// + /// The existing customer to which the upcoming invoice belongs. + /// The existing subscription to which the upcoming invoice belongs. + /// Returns true when successful, false when conditions are not met. + public static bool EnableAutomaticTax( + this UpcomingInvoiceOptions options, + Customer customer, + Subscription subscription) + { + if (subscription != null && subscription.AutomaticTax.Enabled) + { + return false; + } + + // We might only need to check the automatic tax status. + if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + { + return false; + } + + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + options.SubscriptionDefaultTaxRates = []; + + return true; + } +} diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 6b9f32e8f9..57be92ba94 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -320,7 +320,7 @@ public class PremiumUserBillingService( { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true + Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index f4cf22ac19..b2dca19e80 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -661,11 +661,21 @@ public class SubscriberService( } } - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, - new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - }); + if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) + { + await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + + return; + + bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer) + => !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) && + (localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) && + localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; } public async Task VerifyBankAccount( diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 4813608fb5..7f2ac36216 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -177,7 +177,7 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.Customer = customer.Id; - subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + subCreateOptions.EnableAutomaticTax(customer); subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) @@ -358,10 +358,9 @@ public class StripePaymentService : IPaymentService customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions); } - var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade) - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }; + var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); + + subCreateOptions.EnableAutomaticTax(customer); var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); @@ -520,10 +519,6 @@ public class StripePaymentService : IPaymentService var customerCreateOptions = new CustomerCreateOptions { - Tax = new CustomerTaxOptions - { - ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately - }, Description = user.Name, Email = user.Email, Metadata = stripeCustomerMetadata, @@ -561,7 +556,6 @@ public class StripePaymentService : IPaymentService var subCreateOptions = new SubscriptionCreateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, Customer = customer.Id, Items = [], Metadata = new Dictionary @@ -581,10 +575,12 @@ public class StripePaymentService : IPaymentService subCreateOptions.Items.Add(new SubscriptionItemOptions { Plan = StoragePlanId, - Quantity = additionalStorageGb, + Quantity = additionalStorageGb }); } + subCreateOptions.EnableAutomaticTax(customer); + var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer, stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer); @@ -622,7 +618,10 @@ public class StripePaymentService : IPaymentService SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) }); - previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; + if (customer.HasTaxLocationVerified()) + { + previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; + } if (previewInvoice.AmountDue > 0) { @@ -680,12 +679,10 @@ public class StripePaymentService : IPaymentService Customer = customer.Id, SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, - AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = true - } }; + upcomingInvoiceOptions.EnableAutomaticTax(customer, null); + var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); if (previewInvoice.AmountDue > 0) @@ -804,11 +801,7 @@ public class StripePaymentService : IPaymentService Items = updatedItemOptions, ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations, DaysUntilDue = daysUntilDue ?? 1, - CollectionMethod = "send_invoice", - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - } + CollectionMethod = "send_invoice" }; if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing") { @@ -816,6 +809,8 @@ public class StripePaymentService : IPaymentService new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; } + subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); + if (!subscriptionUpdate.UpdateNeeded(sub)) { // No need to update subscription, quantity matches @@ -1500,11 +1495,13 @@ public class StripePaymentService : IPaymentService if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && customer.Subscriptions.Any(sub => sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled)) + !sub.AutomaticTax.Enabled) && + customer.HasTaxLocationVerified()) { var subscriptionUpdateOptions = new SubscriptionUpdateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, + DefaultTaxRates = [] }; _ = await _stripeAdapter.SubscriptionUpdateAsync( From fb74512d27e21a72c20ccf8e7a70cdad2d3db04f Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:10:10 -0500 Subject: [PATCH 10/84] refactor(TwoFactorComponentRefactor Feature Flag): [PM-8113] - Deprecate TwoFactorComponentRefactor feature flag in favor of UnauthenticatedExtensionUIRefresh feature flag. (#5120) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 91637e3893..5023c4b3fc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -129,7 +129,6 @@ public static class FeatureFlagKeys public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string VaultBulkManagementAction = "vault-bulk-management-action"; public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; - public const string TwoFactorComponentRefactor = "two-factor-component-refactor"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; From 0b6f0d9fe8968798cb847f5dfeb461def862d06d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:19:48 -0500 Subject: [PATCH 11/84] Collect Code Coverage In DB Tests (#5431) --- .github/workflows/test-database.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index a668ddb37d..81119414ff 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -146,7 +146,7 @@ jobs: # Unified MariaDB BW_TEST_DATABASES__4__TYPE: "MySql" BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true" - run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" + run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" shell: pwsh - name: Print MySQL Logs @@ -174,6 +174,9 @@ jobs: reporter: dotnet-trx fail-on-error: true + - name: Upload to codecov.io + uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 + - name: Docker Compose down if: always() working-directory: "dev" From 5bbd9054017850b86c1620e29a0e53e331b00aed Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:03:29 -0500 Subject: [PATCH 12/84] [PM-18436] Only cancel subscriptions when creating or renewing (#5423) * Only cancel subscriptions during creation or cycle renewal * Resolved possible null reference warning * Inverted conitional to reduce nesting --- .../Jobs/SubscriptionCancellationJob.cs | 4 +- .../SubscriptionUpdatedHandler.cs | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/Billing/Jobs/SubscriptionCancellationJob.cs b/src/Billing/Jobs/SubscriptionCancellationJob.cs index c46581272e..b59bb10eaf 100644 --- a/src/Billing/Jobs/SubscriptionCancellationJob.cs +++ b/src/Billing/Jobs/SubscriptionCancellationJob.cs @@ -23,9 +23,9 @@ public class SubscriptionCancellationJob( } var subscription = await stripeFacade.GetSubscription(subscriptionId); - if (subscription?.Status != "unpaid") + if (subscription?.Status != "unpaid" || + subscription.LatestInvoice?.BillingReason is not ("subscription_cycle" or "subscription_create")) { - // Subscription is no longer unpaid, skip cancellation return; } diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 10a1d1a186..4e142f8cae 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -59,7 +59,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler /// public async Task HandleAsync(Event parsedEvent) { - var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts"]); + var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice"]); var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); switch (subscription.Status) @@ -68,7 +68,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler when organizationId.HasValue: { await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); - if (subscription.Status == StripeSubscriptionStatus.Unpaid) + if (subscription.Status == StripeSubscriptionStatus.Unpaid && + subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" }) { await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value); } @@ -96,7 +97,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler { await _organizationEnableCommand.EnableAsync(organizationId.Value); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); - await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); + if (organization != null) + { + await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); + } break; } case StripeSubscriptionStatus.Active: @@ -204,23 +208,24 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId) { var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert); - - if (isResellerManagedOrgAlertEnabled) + if (!isResellerManagedOrgAlertEnabled) { - var scheduler = await _schedulerFactory.GetScheduler(); - - var job = JobBuilder.Create() - .WithIdentity($"cancel-sub-{subscriptionId}", "subscription-cancellations") - .UsingJobData("subscriptionId", subscriptionId) - .UsingJobData("organizationId", organizationId.ToString()) - .Build(); - - var trigger = TriggerBuilder.Create() - .WithIdentity($"cancel-trigger-{subscriptionId}", "subscription-cancellations") - .StartAt(DateTimeOffset.UtcNow.AddDays(7)) - .Build(); - - await scheduler.ScheduleJob(job, trigger); + return; } + + var scheduler = await _schedulerFactory.GetScheduler(); + + var job = JobBuilder.Create() + .WithIdentity($"cancel-sub-{subscriptionId}", "subscription-cancellations") + .UsingJobData("subscriptionId", subscriptionId) + .UsingJobData("organizationId", organizationId.ToString()) + .Build(); + + var trigger = TriggerBuilder.Create() + .WithIdentity($"cancel-trigger-{subscriptionId}", "subscription-cancellations") + .StartAt(DateTimeOffset.UtcNow.AddDays(7)) + .Build(); + + await scheduler.ScheduleJob(job, trigger); } } From 93e5f7d0fe569fdde1c3732cea851193a68f10ac Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:21:50 +0100 Subject: [PATCH 13/84] Incorrect Read only connection string on development self-hosted environment (#5426) --- src/Core/Settings/GlobalSettings.cs | 13 +- .../Auth/Services/AuthRequestServiceTests.cs | 11 +- .../Services/SubscriberServiceTests.cs | 5 +- .../LaunchDarklyFeatureServiceTests.cs | 13 +- test/Core.Test/Services/UserServiceTests.cs | 4 +- .../Core.Test/Settings/GlobalSettingsTests.cs | 134 ++++++++++++++++++ .../Tools/Services/SendServiceTests.cs | 6 +- 7 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 test/Core.Test/Settings/GlobalSettingsTests.cs diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index a1c7a4fac6..dbfc8543a3 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -241,7 +241,18 @@ public class GlobalSettings : IGlobalSettings public string ConnectionString { get => _connectionString; - set => _connectionString = value.Trim('"'); + set + { + // On development environment, the self-hosted overrides would not override the read-only connection string, since it is already set from the non-self-hosted connection string. + // This causes a bug, where the read-only connection string is pointing to self-hosted database. + if (!string.IsNullOrWhiteSpace(_readOnlyConnectionString) && + _readOnlyConnectionString == _connectionString) + { + _readOnlyConnectionString = null; + } + + _connectionString = value.Trim('"'); + } } public string ReadOnlyConnectionString diff --git a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs index 8feef2facc..5e99ecf171 100644 --- a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs +++ b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs @@ -19,6 +19,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; #nullable enable @@ -138,7 +139,7 @@ public class AuthRequestServiceTests sutProvider.GetDependency() .PasswordlessAuth - .Returns(new Settings.GlobalSettings.PasswordlessAuthSettings()); + .Returns(new GlobalSettings.PasswordlessAuthSettings()); var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode); @@ -513,7 +514,7 @@ public class AuthRequestServiceTests sutProvider.GetDependency() .PasswordlessAuth - .Returns(new Settings.GlobalSettings.PasswordlessAuthSettings()); + .Returns(new GlobalSettings.PasswordlessAuthSettings()); var updateModel = new AuthRequestUpdateRequestModel { @@ -582,7 +583,7 @@ public class AuthRequestServiceTests sutProvider.GetDependency() .PasswordlessAuth - .Returns(new Settings.GlobalSettings.PasswordlessAuthSettings()); + .Returns(new GlobalSettings.PasswordlessAuthSettings()); sutProvider.GetDependency() .GetByIdentifierAsync(device.Identifier, authRequest.UserId) @@ -736,7 +737,7 @@ public class AuthRequestServiceTests sutProvider.GetDependency() .PasswordlessAuth - .Returns(new Settings.GlobalSettings.PasswordlessAuthSettings()); + .Returns(new GlobalSettings.PasswordlessAuthSettings()); var updateModel = new AuthRequestUpdateRequestModel { @@ -803,7 +804,7 @@ public class AuthRequestServiceTests sutProvider.GetDependency() .PasswordlessAuth - .Returns(new Settings.GlobalSettings.PasswordlessAuthSettings()); + .Returns(new GlobalSettings.PasswordlessAuthSettings()); var updateModel = new AuthRequestUpdateRequestModel { diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 9c25ffdc55..5b7a2cc8bd 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -19,6 +19,7 @@ using Xunit; using static Bit.Core.Test.Billing.Utilities; using Address = Stripe.Address; using Customer = Stripe.Customer; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; using PaymentMethod = Stripe.PaymentMethod; using Subscription = Stripe.Subscription; @@ -1446,7 +1447,7 @@ public class SubscriberServiceTests }); sutProvider.GetDependency().BaseServiceUri - .Returns(new Settings.GlobalSettings.BaseServiceUriSettings(new Settings.GlobalSettings()) + .Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings()) { CloudRegion = "US" }); @@ -1488,7 +1489,7 @@ public class SubscriberServiceTests }); sutProvider.GetDependency().BaseServiceUri - .Returns(new Settings.GlobalSettings.BaseServiceUriSettings(new Settings.GlobalSettings()) + .Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings()) { CloudRegion = "US" }); diff --git a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs index a2c86b5a76..a173566b88 100644 --- a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs +++ b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs @@ -8,6 +8,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using LaunchDarkly.Sdk.Server.Interfaces; using NSubstitute; using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Core.Test.Services; @@ -41,7 +42,7 @@ public class LaunchDarklyFeatureServiceTests [Theory, BitAutoData] public void DefaultFeatureValue_WhenSelfHost(string key) { - var sutProvider = GetSutProvider(new Settings.GlobalSettings { SelfHosted = true }); + var sutProvider = GetSutProvider(new GlobalSettings { SelfHosted = true }); Assert.False(sutProvider.Sut.IsEnabled(key)); } @@ -49,7 +50,7 @@ public class LaunchDarklyFeatureServiceTests [Fact] public void DefaultFeatureValue_NoSdkKey() { - var sutProvider = GetSutProvider(new Settings.GlobalSettings()); + var sutProvider = GetSutProvider(new GlobalSettings()); Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey)); } @@ -57,7 +58,7 @@ public class LaunchDarklyFeatureServiceTests [Fact(Skip = "For local development")] public void FeatureValue_Boolean() { - var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } }; + var settings = new GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } }; var sutProvider = GetSutProvider(settings); @@ -67,7 +68,7 @@ public class LaunchDarklyFeatureServiceTests [Fact(Skip = "For local development")] public void FeatureValue_Int() { - var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } }; + var settings = new GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } }; var sutProvider = GetSutProvider(settings); @@ -77,7 +78,7 @@ public class LaunchDarklyFeatureServiceTests [Fact(Skip = "For local development")] public void FeatureValue_String() { - var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } }; + var settings = new GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } }; var sutProvider = GetSutProvider(settings); @@ -87,7 +88,7 @@ public class LaunchDarklyFeatureServiceTests [Fact(Skip = "For local development")] public void GetAll() { - var sutProvider = GetSutProvider(new Settings.GlobalSettings()); + var sutProvider = GetSutProvider(new GlobalSettings()); var results = sutProvider.Sut.GetAll(); diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 88c214f471..880e79500d 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -66,8 +66,8 @@ public class UserServiceTests user.EmailVerified = true; user.Email = userLicense.Email; - sutProvider.GetDependency().SelfHosted = true; - sutProvider.GetDependency().LicenseDirectory = tempDir.Directory; + sutProvider.GetDependency().SelfHosted = true; + sutProvider.GetDependency().LicenseDirectory = tempDir.Directory; sutProvider.GetDependency() .VerifyLicense(userLicense) .Returns(true); diff --git a/test/Core.Test/Settings/GlobalSettingsTests.cs b/test/Core.Test/Settings/GlobalSettingsTests.cs new file mode 100644 index 0000000000..1f5aa494bb --- /dev/null +++ b/test/Core.Test/Settings/GlobalSettingsTests.cs @@ -0,0 +1,134 @@ +using Bit.Core.Settings; +using Xunit; + +namespace Bit.Core.Test.Settings; + +public class GlobalSettingsTests +{ + public class SqlSettingsTests + { + private const string _testingConnectionString = + "Server=server;Database=database;User Id=user;Password=password;"; + + private const string _testingReadOnlyConnectionString = + "Server=server_read;Database=database_read;User Id=user_read;Password=password_read;"; + + [Fact] + public void ConnectionString_ValueInDoubleQuotes_Stripped() + { + var settings = new GlobalSettings.SqlSettings { ConnectionString = $"\"{_testingConnectionString}\"", }; + + Assert.Equal(_testingConnectionString, settings.ConnectionString); + } + + [Fact] + public void ConnectionString_ValueWithoutDoubleQuotes_TheSameValue() + { + var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString }; + + Assert.Equal(_testingConnectionString, settings.ConnectionString); + } + + [Fact] + public void ConnectionString_SetTwice_ReturnsSecondConnectionString() + { + var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString }; + + Assert.Equal(_testingConnectionString, settings.ConnectionString); + + var newConnectionString = $"{_testingConnectionString}_new"; + settings.ConnectionString = newConnectionString; + + Assert.Equal(newConnectionString, settings.ConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_ValueInDoubleQuotes_Stripped() + { + var settings = new GlobalSettings.SqlSettings + { + ReadOnlyConnectionString = $"\"{_testingReadOnlyConnectionString}\"", + }; + + Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_ValueWithoutDoubleQuotes_TheSameValue() + { + var settings = new GlobalSettings.SqlSettings + { + ReadOnlyConnectionString = _testingReadOnlyConnectionString + }; + + Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_NotSet_DefaultsToConnectionString() + { + var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString }; + + Assert.Equal(_testingConnectionString, settings.ReadOnlyConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_Set_ReturnsReadOnlyConnectionString() + { + var settings = new GlobalSettings.SqlSettings + { + ConnectionString = _testingConnectionString, + ReadOnlyConnectionString = _testingReadOnlyConnectionString + }; + + Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_SetTwice_ReturnsSecondReadOnlyConnectionString() + { + var settings = new GlobalSettings.SqlSettings + { + ConnectionString = _testingConnectionString, + ReadOnlyConnectionString = _testingReadOnlyConnectionString + }; + + Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString); + + var newReadOnlyConnectionString = $"{_testingReadOnlyConnectionString}_new"; + settings.ReadOnlyConnectionString = newReadOnlyConnectionString; + + Assert.Equal(newReadOnlyConnectionString, settings.ReadOnlyConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_NotSetAndConnectionStringSetTwice_ReturnsSecondConnectionString() + { + var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString }; + + Assert.Equal(_testingConnectionString, settings.ReadOnlyConnectionString); + + var newConnectionString = $"{_testingConnectionString}_new"; + settings.ConnectionString = newConnectionString; + + Assert.Equal(newConnectionString, settings.ReadOnlyConnectionString); + } + + [Fact] + public void ReadOnlyConnectionString_SetAndConnectionStringSetTwice_ReturnsReadOnlyConnectionString() + { + var settings = new GlobalSettings.SqlSettings + { + ConnectionString = _testingConnectionString, + ReadOnlyConnectionString = _testingReadOnlyConnectionString + }; + + Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString); + + var newConnectionString = $"{_testingConnectionString}_new"; + settings.ConnectionString = newConnectionString; + + Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString); + } + } +} diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs index 7ef6f915dd..cabb438b61 100644 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendServiceTests.cs @@ -24,6 +24,8 @@ using Microsoft.AspNetCore.Identity; using NSubstitute; using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + namespace Bit.Core.Test.Tools.Services; [SutProviderCustomize] @@ -309,7 +311,7 @@ public class SendServiceTests .CanAccessPremium(user) .Returns(true); - sutProvider.GetDependency() + sutProvider.GetDependency() .SelfHosted = true; var badRequest = await Assert.ThrowsAsync(() => @@ -342,7 +344,7 @@ public class SendServiceTests .CanAccessPremium(user) .Returns(true); - sutProvider.GetDependency() + sutProvider.GetDependency() .SelfHosted = false; var badRequest = await Assert.ThrowsAsync(() => From 2f4d5283d34ada143efde0463f996bd6c7f3224a Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 20 Feb 2025 15:08:06 -0500 Subject: [PATCH 14/84] [PM-17449] Add stored proc, EF query, and an integration test for them (#5413) --- .../IOrganizationDomainRepository.cs | 1 + .../OrganizationDomainRepository.cs | 14 ++++ .../OrganizationDomainRepository.cs | 21 +++++ ...ganizationDomain_ReadByOrganizationIds.sql | 14 ++++ .../OrganizationDomainRepositoryTests.cs | 77 +++++++++++++++++++ ...ganizationDomain_ReadByOrganizationIds.sql | 15 ++++ 6 files changed, 142 insertions(+) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql create mode 100644 util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql diff --git a/src/Core/Repositories/IOrganizationDomainRepository.cs b/src/Core/Repositories/IOrganizationDomainRepository.cs index f8b45574a2..d802fe65df 100644 --- a/src/Core/Repositories/IOrganizationDomainRepository.cs +++ b/src/Core/Repositories/IOrganizationDomainRepository.cs @@ -12,6 +12,7 @@ public interface IOrganizationDomainRepository : IRepository> GetManyByNextRunDateAsync(DateTime date); Task GetOrganizationDomainSsoDetailsAsync(string email); Task> GetVerifiedOrganizationDomainSsoDetailsAsync(string email); + Task> GetVerifiedDomainsByOrganizationIdsAsync(IEnumerable organizationIds); Task GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId); Task GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName); Task> GetExpiredOrganizationDomainsAsync(); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs index 1a7085eb18..91cbc40ff6 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs @@ -46,6 +46,20 @@ public class OrganizationDomainRepository : Repository } } + public async Task> GetVerifiedDomainsByOrganizationIdsAsync(IEnumerable organizationIds) + { + + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationDomain_ReadByOrganizationIds]", + new { OrganizationIds = organizationIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task> GetManyByNextRunDateAsync(DateTime date) { using var connection = new SqlConnection(ConnectionString); diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 50d791b81b..e7bee0cdfd 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -157,4 +157,25 @@ public class OrganizationDomainRepository : Repository 0; } + + public async Task> GetVerifiedDomainsByOrganizationIdsAsync( + IEnumerable organizationIds) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var verifiedDomains = await (from d in dbContext.OrganizationDomains + where organizationIds.Contains(d.OrganizationId) && d.VerifiedDate != null + select new OrganizationDomain + { + OrganizationId = d.OrganizationId, + DomainName = d.DomainName + }) + .AsNoTracking() + .ToListAsync(); + + return Mapper.Map>(verifiedDomains); + } + } + diff --git a/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql new file mode 100644 index 0000000000..f62544e486 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByOrganizationIds] + @OrganizationIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + + SET NOCOUNT ON + + SELECT + d.OrganizationId, + d.DomainName + FROM dbo.OrganizationDomainView AS d + WHERE d.OrganizationId IN (SELECT [Id] FROM @OrganizationIds) + AND d.VerifiedDate IS NOT NULL; +END \ No newline at end of file diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs index a1c5f9bd07..6c1ac00073 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationDomainRepositoryTests.cs @@ -306,4 +306,81 @@ public class OrganizationDomainRepositoryTests var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName); Assert.Null(expectedDomain); } + + [DatabaseTheory, DatabaseData] + public async Task GetVerifiedDomainsByOrganizationIdsAsync_ShouldVerifiedDomainsMatchesOrganizationIds( + IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + var organization1 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {guid1}", + BillingEmail = $"test+{guid1}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organization1Domain1 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain1+{guid1}@example.com", + Txt = "btw+12345" + }; + + const int arbitraryNextIteration = 1; + organization1Domain1.SetNextRunDate(arbitraryNextIteration); + organization1Domain1.SetVerifiedDate(); + + await organizationDomainRepository.CreateAsync(organization1Domain1); + + var organization1Domain2 = new OrganizationDomain + { + OrganizationId = organization1.Id, + DomainName = $"domain2+{guid1}@example.com", + Txt = "btw+12345" + }; + + organization1Domain2.SetNextRunDate(arbitraryNextIteration); + + await organizationDomainRepository.CreateAsync(organization1Domain2); + + var organization2 = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {guid2}", + BillingEmail = $"test+{guid2}@example.com", + Plan = "Test", + PrivateKey = "privatekey", + + }); + + var organization2Domain1 = new OrganizationDomain + { + OrganizationId = organization2.Id, + DomainName = $"domain+{guid2}@example.com", + Txt = "btw+12345" + }; + organization2Domain1.SetVerifiedDate(); + organization2Domain1.SetNextRunDate(arbitraryNextIteration); + + await organizationDomainRepository.CreateAsync(organization2Domain1); + + + // Act + var domains = await organizationDomainRepository.GetVerifiedDomainsByOrganizationIdsAsync(new[] { organization1.Id }); + + // Assert + var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organization1Domain1.DomainName); + Assert.NotNull(expectedDomain); + + var unverifiedDomain = domains.FirstOrDefault(domain => domain.DomainName == organization1Domain2.DomainName); + var otherOrganizationDomain = domains.FirstOrDefault(domain => domain.DomainName == organization2Domain1.DomainName); + + Assert.Null(otherOrganizationDomain); + Assert.Null(unverifiedDomain); + } } diff --git a/util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql b/util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql new file mode 100644 index 0000000000..5616aa0ac7 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-17_00_OrganizationDomain_ReadByOrganizationIds.sql @@ -0,0 +1,15 @@ + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationDomain_ReadByOrganizationIds] + @OrganizationIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + + SET NOCOUNT ON + + SELECT + d.OrganizationId, + d.DomainName + FROM dbo.OrganizationDomainView AS d + WHERE d.OrganizationId IN (SELECT [Id] FROM @OrganizationIds) + AND d.VerifiedDate IS NOT NULL; +END \ No newline at end of file From 06c96a96c543b70045c2e96423859588c1db37ce Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 20 Feb 2025 15:38:59 -0500 Subject: [PATCH 15/84] [PM-17449] Add logic to handle email updates for managed users. (#5422) --- .../Auth/Controllers/AccountsController.cs | 12 ---- .../Services/Implementations/UserService.cs | 35 +++++++++++ .../Controllers/AccountsControllerTest.cs | 59 +------------------ .../Controllers/AccountsControllerTests.cs | 30 ---------- test/Core.Test/Services/UserServiceTests.cs | 2 + 5 files changed, 38 insertions(+), 100 deletions(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 1cd9292386..f3af49e6c3 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -149,12 +149,6 @@ public class AccountsController : Controller throw new BadRequestException("MasterPasswordHash", "Invalid password."); } - // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _userService.IsManagedByAnyOrganizationAsync(user.Id)) - { - throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details."); - } await _userService.InitiateEmailChangeAsync(user, model.NewEmail); } @@ -173,12 +167,6 @@ public class AccountsController : Controller throw new BadRequestException("You cannot change your email when using Key Connector."); } - // If Account Deprovisioning is enabled, we need to check if the user is managed by any organization. - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _userService.IsManagedByAnyOrganizationAsync(user.Id)) - { - throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details."); - } var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail, model.NewMasterPasswordHash, model.Token, model.Key); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index e04290a686..47637d0f75 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -49,6 +49,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly ICipherRepository _cipherRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; @@ -81,6 +82,7 @@ public class UserService : UserManager, IUserService, IDisposable ICipherRepository cipherRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, + IOrganizationDomainRepository organizationDomainRepository, IMailService mailService, IPushNotificationService pushService, IUserStore store, @@ -127,6 +129,7 @@ public class UserService : UserManager, IUserService, IDisposable _cipherRepository = cipherRepository; _organizationUserRepository = organizationUserRepository; _organizationRepository = organizationRepository; + _organizationDomainRepository = organizationDomainRepository; _mailService = mailService; _pushService = pushService; _identityOptions = optionsAccessor?.Value ?? new IdentityOptions(); @@ -521,6 +524,13 @@ public class UserService : UserManager, IUserService, IDisposable return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } + var managedUserValidationResult = await ValidateManagedUserAsync(user, newEmail); + + if (!managedUserValidationResult.Succeeded) + { + return managedUserValidationResult; + } + if (!await base.VerifyUserTokenAsync(user, _identityOptions.Tokens.ChangeEmailTokenProvider, GetChangeEmailTokenPurpose(newEmail), token)) { @@ -586,6 +596,31 @@ public class UserService : UserManager, IUserService, IDisposable return IdentityResult.Success; } + private async Task ValidateManagedUserAsync(User user, string newEmail) + { + var managingOrganizations = await GetOrganizationsManagingUserAsync(user.Id); + + if (!managingOrganizations.Any()) + { + return IdentityResult.Success; + } + + var newDomain = CoreHelpers.GetEmailDomain(newEmail); + + var verifiedDomains = await _organizationDomainRepository.GetVerifiedDomainsByOrganizationIdsAsync(managingOrganizations.Select(org => org.Id)); + + if (verifiedDomains.Any(verifiedDomain => verifiedDomain.DomainName == newDomain)) + { + return IdentityResult.Success; + } + + return IdentityResult.Failed(new IdentityError + { + Code = "EmailDomainMismatch", + Description = "Your new email must match your organization domain." + }); + } + public async Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint, string key) { diff --git a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs index 6dd7f42c63..277f558566 100644 --- a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs +++ b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs @@ -1,6 +1,4 @@ -using System.Net; -using System.Net.Http.Headers; -using Bit.Api.Auth.Models.Request.Accounts; +using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Response; @@ -45,61 +43,6 @@ public class AccountsControllerTest : IClassFixture Assert.NotNull(content.SecurityStamp); } - [Fact] - public async Task PostEmailToken_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest() - { - var email = await SetupOrganizationManagedAccount(); - - var tokens = await _factory.LoginAsync(email); - var client = _factory.CreateClient(); - - var model = new EmailTokenRequestModel - { - NewEmail = $"{Guid.NewGuid()}@example.com", - MasterPasswordHash = "master_password_hash" - }; - - using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email-token") - { - Content = JsonContent.Create(model) - }; - message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); - var response = await client.SendAsync(message); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var content = await response.Content.ReadAsStringAsync(); - Assert.Contains("Cannot change emails for accounts owned by an organization", content); - } - - [Fact] - public async Task PostEmail_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest() - { - var email = await SetupOrganizationManagedAccount(); - - var tokens = await _factory.LoginAsync(email); - var client = _factory.CreateClient(); - - var model = new EmailRequestModel - { - NewEmail = $"{Guid.NewGuid()}@example.com", - MasterPasswordHash = "master_password_hash", - NewMasterPasswordHash = "master_password_hash", - Token = "validtoken", - Key = "key" - }; - - using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email") - { - Content = JsonContent.Create(model) - }; - message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); - var response = await client.SendAsync(message); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - var content = await response.Content.ReadAsStringAsync(); - Assert.Contains("Cannot change emails for accounts owned by an organization", content); - } - private async Task SetupOrganizationManagedAccount() { _factory.SubstituteService(featureService => diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 33b7e764d4..8bdb14bf78 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -181,22 +181,6 @@ public class AccountsControllerTests : IDisposable ); } - [Fact] - public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException() - { - var user = GenerateExampleUser(); - ConfigureUserServiceToReturnValidPrincipalFor(user); - ConfigureUserServiceToAcceptPasswordFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true); - - var result = await Assert.ThrowsAsync( - () => _sut.PostEmailToken(new EmailTokenRequestModel()) - ); - - Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message); - } - [Fact] public async Task PostEmail_ShouldChangeUserEmail() { @@ -248,20 +232,6 @@ public class AccountsControllerTests : IDisposable ); } - [Fact] - public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException() - { - var user = GenerateExampleUser(); - ConfigureUserServiceToReturnValidPrincipalFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true); - - var result = await Assert.ThrowsAsync( - () => _sut.PostEmail(new EmailRequestModel()) - ); - - Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message); - } [Fact] public async Task PostVerifyEmail_ShouldSendEmailVerification() diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 880e79500d..7f1dd37b6b 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -248,6 +248,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), + sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency>(), @@ -829,6 +830,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), + sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency>(), From f6365fa3859c0bca72ef778bf03d988bca1c5d92 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 21 Feb 2025 15:35:36 +0100 Subject: [PATCH 16/84] [PM-17593] Remove Multi-Org Enterprise feature flag (#5351) --- .../Controllers/ProvidersController.cs | 10 --- .../Views/Providers/Create.cshtml | 5 -- .../AdminConsole/Views/Providers/Edit.cshtml | 45 +++++++------- src/Core/Constants.cs | 2 +- .../Controllers/ProvidersControllerTests.cs | 62 ------------------- 5 files changed, 22 insertions(+), 102 deletions(-) diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 38e25939c4..9e3dc00cd6 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -3,7 +3,6 @@ using System.Net; using Bit.Admin.AdminConsole.Models; using Bit.Admin.Enums; using Bit.Admin.Utilities; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; @@ -133,11 +132,6 @@ public class ProvidersController : Controller [HttpGet("providers/create/multi-organization-enterprise")] public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null) { - if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)) - { - return RedirectToAction("Create"); - } - return View(new CreateMultiOrganizationEnterpriseProviderModel { OwnerEmail = ownerEmail, @@ -211,10 +205,6 @@ public class ProvidersController : Controller } var provider = model.ToProvider(); - if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)) - { - return RedirectToAction("Create"); - } await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync( provider, model.OwnerEmail, diff --git a/src/Admin/AdminConsole/Views/Providers/Create.cshtml b/src/Admin/AdminConsole/Views/Providers/Create.cshtml index 3c92075991..25574bf6b9 100644 --- a/src/Admin/AdminConsole/Views/Providers/Create.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Create.cshtml @@ -12,11 +12,6 @@ var providerTypes = Enum.GetValues() .OrderBy(x => x.GetDisplayAttribute().Order) .ToList(); - - if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)) - { - providerTypes.Remove(ProviderType.MultiOrganizationEnterprise); - } }

Create Provider

diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index be13a7c740..045109fb36 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -76,32 +76,29 @@ } case ProviderType.MultiOrganizationEnterprise: { - @if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise) - { -
-
-
- @{ - var multiOrgPlans = new List - { - PlanType.EnterpriseAnnually, - PlanType.EnterpriseMonthly - }; - } - - -
-
-
-
- - -
+
+
+
+ @{ + var multiOrgPlans = new List + { + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly + }; + } + +
- } +
+
+ + +
+
+
break; } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5023c4b3fc..ee48479d5f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -150,7 +150,7 @@ public static class FeatureFlagKeys public const string StorageReseedRefactor = "storage-reseed-refactor"; 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 NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; diff --git a/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs index be9883ba07..e84d4c0ef8 100644 --- a/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs +++ b/test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs @@ -1,11 +1,9 @@ using Bit.Admin.AdminConsole.Controllers; using Bit.Admin.AdminConsole.Models; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.Billing.Enums; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Mvc; @@ -86,9 +84,6 @@ public class ProvidersControllerTests SutProvider sutProvider) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) - .Returns(true); // Act var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); @@ -102,9 +97,6 @@ public class ProvidersControllerTests model.OwnerEmail, Arg.Is(y => y == model.Plan), model.EnterpriseSeatMinimum); - sutProvider.GetDependency() - .Received(Quantity.Exactly(1)) - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises); } [BitAutoData] @@ -129,10 +121,6 @@ public class ProvidersControllerTests providerArgument.Id = expectedProviderId; }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) - .Returns(true); - // Act var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); @@ -144,53 +132,6 @@ public class ProvidersControllerTests Assert.Null(actualResult.ControllerName); Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]); } - - [BitAutoData] - [SutProviderCustomize] - [Theory] - public async Task CreateMultiOrganizationEnterpriseAsync_ChecksFeatureFlag( - CreateMultiOrganizationEnterpriseProviderModel model, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) - .Returns(true); - - // Act - await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); - - // Assert - sutProvider.GetDependency() - .Received(Quantity.Exactly(1)) - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises); - } - - [BitAutoData] - [SutProviderCustomize] - [Theory] - public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToProviderTypeSelectionPage_WhenFeatureFlagIsDisabled( - CreateMultiOrganizationEnterpriseProviderModel model, - SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) - .Returns(false); - - // Act - var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model); - - // Assert - sutProvider.GetDependency() - .Received(Quantity.Exactly(1)) - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises); - - Assert.IsType(actual); - var actualResult = (RedirectToActionResult)actual; - Assert.Equal("Create", actualResult.ActionName); - Assert.Null(actualResult.ControllerName); - } #endregion #region CreateResellerAsync @@ -202,9 +143,6 @@ public class ProvidersControllerTests SutProvider sutProvider) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) - .Returns(true); // Act var actual = await sutProvider.Sut.CreateReseller(model); From b66f255c5c6d6e5145fd4feeff7bad199571f115 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Fri, 21 Feb 2025 10:21:31 -0500 Subject: [PATCH 17/84] Add import-logins-flow flag (#5397) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ee48479d5f..c310674bcc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -173,6 +173,7 @@ public static class FeatureFlagKeys public const string AndroidMutualTls = "mutual-tls"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; + public const string AndroidImportLoginsFlow = "import-logins-flow"; public static List GetAllKeys() { From b00f11fc43c0d5f20a8e224a8f9e3fc4dc23e60e Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:12:31 -0500 Subject: [PATCH 18/84] [PM-17645] : update email for new email multi factor tokens (#5428) * feat(newDeviceVerification) : Initial update to email * fix : email copying over extra whitespace when using keyboard short cuts * test : Fixing tests for new device verificaiton email format --- .../Auth/Controllers/TwoFactorController.cs | 7 ++- .../Handlebars/Auth/TwoFactorEmail.html.hbs | 46 ++++++++++---- .../Handlebars/Auth/TwoFactorEmail.text.hbs | 15 ++++- .../Mail/TwoFactorEmailTokenViewModel.cs | 25 ++++++++ ...=> UserVerificationEmailTokenViewModel.cs} | 2 +- src/Core/Services/IMailService.cs | 2 +- src/Core/Services/IUserService.cs | 16 ++++- .../Implementations/HandlebarsMailService.cs | 20 ++++-- .../Services/Implementations/UserService.cs | 29 +++++++-- .../NoopImplementations/NoopMailService.cs | 2 +- .../RequestValidators/DeviceValidator.cs | 10 ++- test/Core.Test/Services/UserServiceTests.cs | 62 +++++++++++++++++-- .../Endpoints/IdentityServerTwoFactorTests.cs | 14 ++++- .../IdentityServer/DeviceValidatorTests.cs | 2 +- 14 files changed, 214 insertions(+), 38 deletions(-) create mode 100644 src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs rename src/Core/Models/Mail/{EmailTokenViewModel.cs => UserVerificationEmailTokenViewModel.cs} (54%) diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index c7d39f64b0..83490f1c2f 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -288,12 +288,17 @@ public class TwoFactorController : Controller return response; } + /// + /// This endpoint is only used to set-up email two factor authentication. + /// + /// secret verification model + /// void [HttpPost("send-email")] public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model) { var user = await CheckAsync(model, false, true); model.ToUser(user); - await _userService.SendTwoFactorEmailAsync(user); + await _userService.SendTwoFactorEmailAsync(user, false); } [AllowAnonymous] diff --git a/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs index be51c4e9f3..27a222f1de 100644 --- a/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs @@ -1,14 +1,38 @@ {{#>FullHtmlLayout}} - - - - - - + + + + + + + + +
- Your two-step verification code is: {{Token}} -
- Use this code to complete logging in with Bitwarden. -
+ To finish {{EmailTotpAction}}, enter this verification code: {{Token}} +
+
+ If this was not you, take these immediate steps to secure your account in the web app: +
    +
  • Deauthorize unrecognized devices
  • +
  • Change your master password
  • +
  • Turn on two-step login
  • +
+
+
+
+
+ Account: + {{AccountEmail}} +
+ Date: + {{TheDate}} at {{TheTime}} {{TimeZone}} +
+ IP: + {{DeviceIp}} +
+ DeviceType: + {{DeviceType}} +
-{{/FullHtmlLayout}} +{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.text.hbs index c7e64e5da2..211a870d6a 100644 --- a/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.text.hbs +++ b/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.text.hbs @@ -1,5 +1,16 @@ {{#>BasicTextLayout}} -Your two-step verification code is: {{Token}} +To finish {{EmailTotpAction}}, enter this verification code: {{Token}} -Use this code to complete logging in with Bitwarden. +If this was not you, take these immediate steps to secure your account in the web app: + +Deauthorize unrecognized devices + +Change your master password + +Turn on two-step login + +Account : {{AccountEmail}} +Date : {{TheDate}} at {{TheTime}} {{TimeZone}} +IP : {{DeviceIp}} +Device Type : {{DeviceType}} {{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs b/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs new file mode 100644 index 0000000000..dbd47af35a --- /dev/null +++ b/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs @@ -0,0 +1,25 @@ +namespace Bit.Core.Models.Mail; + +/// +/// This view model is used to set-up email two factor authentication, to log in with email two factor authentication, +/// and for new device verification. +/// +public class TwoFactorEmailTokenViewModel : BaseMailModel +{ + public string Token { get; set; } + /// + /// This view model is used to also set-up email two factor authentication. We use this property to communicate + /// the purpose of the email, since it can be used for logging in and for setting up. + /// + public string EmailTotpAction { get; set; } + /// + /// When logging in with email two factor the account email may not be the same as the email used for two factor. + /// we want to show the account email in the email, so the user knows which account they are logging into. + /// + public string AccountEmail { get; set; } + public string TheDate { get; set; } + public string TheTime { get; set; } + public string TimeZone { get; set; } + public string DeviceIp { get; set; } + public string DeviceType { get; set; } +} diff --git a/src/Core/Models/Mail/EmailTokenViewModel.cs b/src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs similarity index 54% rename from src/Core/Models/Mail/EmailTokenViewModel.cs rename to src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs index 561df580e8..b8850b5f00 100644 --- a/src/Core/Models/Mail/EmailTokenViewModel.cs +++ b/src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Models.Mail; -public class EmailTokenViewModel : BaseMailModel +public class UserVerificationEmailTokenViewModel : BaseMailModel { public string Token { get; set; } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 92d05ddb7d..3492ada838 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -23,7 +23,7 @@ public interface IMailService Task SendCannotDeleteManagedAccountEmailAsync(string email); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); Task SendChangeEmailEmailAsync(string newEmailAddress, string token); - Task SendTwoFactorEmailAsync(string email, string token); + Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true); Task SendNoMasterPasswordHintEmailAsync(string email); Task SendMasterPasswordHintEmailAsync(string email, string hint); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index d1c61e4418..2ac7796547 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -21,7 +21,21 @@ public interface IUserService Task CreateUserAsync(User user); Task CreateUserAsync(User user, string masterPasswordHash); Task SendMasterPasswordHintAsync(string email); - Task SendTwoFactorEmailAsync(User user); + /// + /// Used for both email two factor and email two factor setup. + /// + /// user requesting the action + /// this controls if what verbiage is shown in the email + /// void + Task SendTwoFactorEmailAsync(User user, bool authentication = true); + /// + /// Calls the same email implementation but instead it sends the token to the account email not the + /// email set up for two-factor, since in practice they can be different. + /// + /// user attepting to login with a new device + /// void + Task SendNewDeviceVerificationEmailAsync(User user); + Task VerifyTwoFactorEmailAsync(User user, string token); Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index d18a29b13a..44be3bfdf4 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -146,7 +146,7 @@ public class HandlebarsMailService : IMailService public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token) { var message = CreateDefaultMessage("Your Email Change", newEmailAddress); - var model = new EmailTokenViewModel + var model = new UserVerificationEmailTokenViewModel { Token = token, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, @@ -158,14 +158,22 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendTwoFactorEmailAsync(string email, string token) + public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true) { - var message = CreateDefaultMessage("Your Two-step Login Verification Code", email); - var model = new EmailTokenViewModel + var message = CreateDefaultMessage("Your Bitwarden Verification Code", email); + var requestDateTime = DateTime.UtcNow; + var model = new TwoFactorEmailTokenViewModel { Token = token, + EmailTotpAction = authentication ? "logging in" : "setting up two-step login", + AccountEmail = accountEmail, + TheDate = requestDateTime.ToLongDateString(), + TheTime = requestDateTime.ToShortTimeString(), + TimeZone = _utcTimeZoneDisplay, + DeviceIp = deviceIp, + DeviceType = deviceType, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, - SiteName = _globalSettings.SiteName + SiteName = _globalSettings.SiteName, }; await AddMessageContentAsync(message, "Auth.TwoFactorEmail", model); message.MetaData.Add("SendGridBypassListManagement", true); @@ -1012,7 +1020,7 @@ public class HandlebarsMailService : IMailService public async Task SendOTPEmailAsync(string email, string token) { var message = CreateDefaultMessage("Your Bitwarden Verification Code", email); - var model = new EmailTokenViewModel + var model = new UserVerificationEmailTokenViewModel { Token = token, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 47637d0f75..2374d8f4e1 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,4 +1,6 @@ -using System.Security.Claims; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; @@ -350,7 +352,7 @@ public class UserService : UserManager, IUserService, IDisposable await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint); } - public async Task SendTwoFactorEmailAsync(User user) + public async Task SendTwoFactorEmailAsync(User user, bool authentication = true) { var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); if (provider == null || provider.MetaData == null || !provider.MetaData.ContainsKey("Email")) @@ -361,7 +363,26 @@ public class UserService : UserManager, IUserService, IDisposable var email = ((string)provider.MetaData["Email"]).ToLowerInvariant(); var token = await base.GenerateTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)); - await _mailService.SendTwoFactorEmailAsync(email, token); + + var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName() ?? "Unknown Browser"; + + await _mailService.SendTwoFactorEmailAsync( + email, user.Email, token, _currentContext.IpAddress, deviceType, authentication); + } + + public async Task SendNewDeviceVerificationEmailAsync(User user) + { + ArgumentNullException.ThrowIfNull(user); + + var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider, + "otp:" + user.Email); + + var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName() ?? "Unknown Browser"; + + await _mailService.SendTwoFactorEmailAsync( + user.Email, user.Email, token, _currentContext.IpAddress, deviceType); } public async Task VerifyTwoFactorEmailAsync(User user, string token) @@ -1519,7 +1540,7 @@ public class UserService : UserManager, IUserService, IDisposable if (await VerifySecretAsync(user, secret)) { - await SendOTPAsync(user); + await SendNewDeviceVerificationEmailAsync(user); } } diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index d6b330294d..9984f8ee90 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -87,7 +87,7 @@ public class NoopMailService : IMailService public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) => Task.CompletedTask; - public Task SendTwoFactorEmailAsync(string email, string token) + public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true) { return Task.FromResult(0); } diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index fee10e10ff..17d16f5949 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -79,7 +79,7 @@ public class DeviceValidator( BuildDeviceErrorResult(validationResult); if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired) { - await _userService.SendOTPAsync(context.User); + await _userService.SendNewDeviceVerificationEmailAsync(context.User); } return false; } @@ -163,6 +163,14 @@ public class DeviceValidator( return DeviceValidationResultType.NewDeviceVerificationRequired; } + /// + /// Sends an email whenever the user logs in from a new device. Will not send to a user who's account + /// is less than 10 minutes old. We assume an account that is less than 10 minutes old is new and does + /// not need an email stating they just logged in. + /// + /// user logging in + /// current device being approved to login + /// void private async Task SendNewDeviceLoginEmail(User user, Device requestDevice) { // Ensure that the user doesn't receive a "new device" email on the first login diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 7f1dd37b6b..3158c1595c 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -96,6 +96,9 @@ public class UserServiceTests { var email = user.Email.ToLowerInvariant(); var token = "thisisatokentocompare"; + var authentication = true; + var IpAddress = "1.1.1.1"; + var deviceType = "Android"; var userTwoFactorTokenProvider = Substitute.For>(); userTwoFactorTokenProvider @@ -105,6 +108,10 @@ public class UserServiceTests .GenerateAsync("TwoFactor", Arg.Any>(), user) .Returns(Task.FromResult(token)); + var context = sutProvider.GetDependency(); + context.DeviceType = DeviceType.Android; + context.IpAddress = IpAddress; + sutProvider.Sut.RegisterTokenProvider("Custom_Email", userTwoFactorTokenProvider); user.SetTwoFactorProviders(new Dictionary @@ -119,7 +126,7 @@ public class UserServiceTests await sutProvider.GetDependency() .Received(1) - .SendTwoFactorEmailAsync(email, token); + .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType, authentication); } [Theory, BitAutoData] @@ -160,6 +167,44 @@ public class UserServiceTests await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); } + [Theory, BitAutoData] + public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => sutProvider.Sut.SendNewDeviceVerificationEmailAsync(null)); + } + + [Theory] + [BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")] + [BitAutoData(DeviceType.Android, "Android")] + public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName, SutProvider sutProvider, User user) + { + SetupFakeTokenProvider(sutProvider, user); + var context = sutProvider.GetDependency(); + context.DeviceType = deviceType; + context.IpAddress = "1.1.1.1"; + + await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), deviceTypeName, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(SutProvider sutProvider, User user) + { + SetupFakeTokenProvider(sutProvider, user); + var context = sutProvider.GetDependency(); + context.DeviceType = null; + context.IpAddress = "1.1.1.1"; + + await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), "Unknown Browser", Arg.Any()); + } + [Theory, BitAutoData] public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider sutProvider, User user) { @@ -577,7 +622,7 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_UserNull_SendOTPAsyncNotCalled( + public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled( SutProvider sutProvider, string email, string secret) { sutProvider.GetDependency() @@ -588,11 +633,11 @@ public class UserServiceTests await sutProvider.GetDependency() .DidNotReceive() - .SendOTPEmailAsync(Arg.Any(), Arg.Any()); + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendOTPAsyncNotCalled( + public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendTwoFactorEmailAsyncNotCalled( SutProvider sutProvider, string email, string secret) { sutProvider.GetDependency() @@ -603,7 +648,7 @@ public class UserServiceTests await sutProvider.GetDependency() .DidNotReceive() - .SendOTPEmailAsync(Arg.Any(), Arg.Any()); + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -637,6 +682,10 @@ public class UserServiceTests .GetByEmailAsync(user.Email) .Returns(user); + var context = sutProvider.GetDependency(); + context.DeviceType = DeviceType.Android; + context.IpAddress = "1.1.1.1"; + // HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured var sut = RebuildSut(sutProvider); @@ -644,7 +693,8 @@ public class UserServiceTests await sutProvider.GetDependency() .Received(1) - .SendOTPEmailAsync(user.Email, Arg.Any()); + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } [Theory] diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 4e598c436d..289f321512 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -67,7 +67,12 @@ public class IdentityServerTwoFactorTests : IClassFixture(mailService => { - mailService.SendTwoFactorEmailAsync(Arg.Any(), Arg.Do(t => emailToken = t)) + mailService.SendTwoFactorEmailAsync( + Arg.Any(), + Arg.Any(), + Arg.Do(t => emailToken = t), + Arg.Any(), + Arg.Any()) .Returns(Task.CompletedTask); }); @@ -273,7 +278,12 @@ public class IdentityServerTwoFactorTests : IClassFixture(mailService => { - mailService.SendTwoFactorEmailAsync(Arg.Any(), Arg.Do(t => emailToken = t)) + mailService.SendTwoFactorEmailAsync( + Arg.Any(), + Arg.Any(), + Arg.Do(t => emailToken = t), + Arg.Any(), + Arg.Any()) .Returns(Task.CompletedTask); }); diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 6e6406f16b..fddcf2005d 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -574,7 +574,7 @@ public class DeviceValidatorTests var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert - await _userService.Received(1).SendOTPAsync(context.User); + await _userService.Received(1).SendNewDeviceVerificationEmailAsync(context.User); await _deviceService.Received(0).SaveAsync(Arg.Any()); Assert.False(result); From c1ac96814e3a6b689abd3fe5fc60f6fdd2a26330 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 21 Feb 2025 13:23:06 -0500 Subject: [PATCH 19/84] remove feature flag (#5432) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c310674bcc..879e8365fc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -105,7 +105,6 @@ public static class FeatureFlagKeys public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; 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 ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests"; From d8cf658207ba3984e2c27f26ec6e43ee5d1ed3b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:35:39 +0000 Subject: [PATCH 20/84] [deps] Auth: Update sass to v1.85.0 (#4947) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 33 ++++++++++++++------- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 33 ++++++++++++++------- src/Admin/package.json | 2 +- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index f1e23abd60..a0e7b767cc 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.2", - "sass": "1.79.5", + "sass": "1.85.0", "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" @@ -104,6 +104,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -771,6 +772,7 @@ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -964,6 +966,7 @@ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, "license": "Apache-2.0", + "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -1133,6 +1136,7 @@ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1234,9 +1238,9 @@ } }, "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "dev": true, "license": "MIT" }, @@ -1292,6 +1296,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -1302,6 +1307,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1315,6 +1321,7 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.12.0" } @@ -1430,6 +1437,7 @@ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1513,7 +1521,8 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/node-releases": { "version": "2.0.19", @@ -1601,6 +1610,7 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8.6" }, @@ -1857,15 +1867,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.79.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", - "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", - "immutable": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -1873,6 +1882,9 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-loader": { @@ -2125,6 +2137,7 @@ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index fa1fac3907..d9aefafef3 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -16,7 +16,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.2", - "sass": "1.79.5", + "sass": "1.85.0", "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index cc2693eae6..152edd6fc9 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -18,7 +18,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.2", - "sass": "1.79.5", + "sass": "1.85.0", "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" @@ -105,6 +105,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -772,6 +773,7 @@ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -965,6 +967,7 @@ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, "license": "Apache-2.0", + "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -1134,6 +1137,7 @@ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1235,9 +1239,9 @@ } }, "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "dev": true, "license": "MIT" }, @@ -1293,6 +1297,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -1303,6 +1308,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1316,6 +1322,7 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.12.0" } @@ -1431,6 +1438,7 @@ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1514,7 +1522,8 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/node-releases": { "version": "2.0.19", @@ -1602,6 +1611,7 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8.6" }, @@ -1858,15 +1868,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.79.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", - "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "dev": true, "license": "MIT", "dependencies": { - "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", - "immutable": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -1874,6 +1883,9 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-loader": { @@ -2126,6 +2138,7 @@ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/src/Admin/package.json b/src/Admin/package.json index 2a8e91f43e..7f3c8046a2 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.0", "mini-css-extract-plugin": "2.9.2", - "sass": "1.79.5", + "sass": "1.85.0", "sass-loader": "16.0.4", "webpack": "5.97.1", "webpack-cli": "5.1.4" From 5241e09c1a29e65c17308d2cdc77f788ccd8da37 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:59:37 +0100 Subject: [PATCH 21/84] PM-15882: Added RemoveUnlockWithPin policy (#5388) --- src/Core/AdminConsole/Enums/PolicyType.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index 80ab18e174..6f3bcd0102 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -15,7 +15,8 @@ public enum PolicyType : byte DisablePersonalVaultExport = 10, ActivateAutofill = 11, AutomaticAppLogIn = 12, - FreeFamiliesSponsorshipPolicy = 13 + FreeFamiliesSponsorshipPolicy = 13, + RemoveUnlockWithPin = 14, } public static class PolicyTypeExtensions @@ -41,7 +42,8 @@ public static class PolicyTypeExtensions PolicyType.DisablePersonalVaultExport => "Remove individual vault export", PolicyType.ActivateAutofill => "Active auto-fill", PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications", - PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship" + PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship", + PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN" }; } } From b0c6fc9146433c95019e51f64cc38ed1f658293d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:19:52 +1000 Subject: [PATCH 22/84] [PM-18234] Add SendPolicyRequirement (#5409) --- .../SendPolicyRequirement.cs | 54 +++++++ .../PolicyServiceCollectionExtensions.cs | 1 + .../Services/Implementations/SendService.cs | 41 +++++- .../AutoFixture/PolicyDetailsFixtures.cs | 35 +++++ .../PolicyDetailsTestExtensions.cs | 10 ++ .../SendPolicyRequirementTests.cs | 138 ++++++++++++++++++ .../Tools/AutoFixture/SendFixtures.cs | 21 ++- .../Tools/Services/SendServiceTests.cs | 90 ++++++++++++ 8 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs create mode 100644 test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs new file mode 100644 index 0000000000..c54cc98373 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs @@ -0,0 +1,54 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Disable Send and Send Options policies. +/// +public class SendPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends. + /// They may still delete existing Sends. + /// + public bool DisableSend { get; init; } + /// + /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send. + /// + public bool DisableHideEmail { get; init; } + + /// + /// Create a new SendPolicyRequirement. + /// + /// All PolicyDetails relating to the user. + /// + /// This is a for the SendPolicyRequirement. + /// + public static SendPolicyRequirement Create(IEnumerable policyDetails) + { + var filteredPolicies = policyDetails + .ExemptRoles([OrganizationUserType.Owner, OrganizationUserType.Admin]) + .ExemptStatus([OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked]) + .ExemptProviders() + .ToList(); + + var result = filteredPolicies + .GetPolicyType(PolicyType.SendOptions) + .Select(p => p.GetDataModel()) + .Aggregate( + new SendPolicyRequirement + { + // Set Disable Send requirement in the initial seed + DisableSend = filteredPolicies.GetPolicyType(PolicyType.DisableSend).Any() + }, + (result, data) => new SendPolicyRequirement + { + DisableSend = result.DisableSend, + DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail + }); + + return result; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index f7b35f2f06..7bc8a7b5a3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -32,6 +32,7 @@ public static class PolicyServiceCollectionExtensions private static void AddPolicyRequirements(this IServiceCollection services) { // Register policy requirement factories here + services.AddPolicyRequirement(SendPolicyRequirement.Create); } /// diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index 918379d7a5..bddaa93bfc 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -1,7 +1,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -26,7 +27,6 @@ public class SendService : ISendService public const string MAX_FILE_SIZE_READABLE = "500 MB"; private readonly ISendRepository _sendRepository; private readonly IUserRepository _userRepository; - private readonly IPolicyRepository _policyRepository; private readonly IPolicyService _policyService; private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; @@ -36,6 +36,9 @@ public class SendService : ISendService private readonly IReferenceEventService _referenceEventService; private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; + private const long _fileSizeLeeway = 1024L * 1024L; // 1MB public SendService( @@ -48,14 +51,14 @@ public class SendService : ISendService IPushNotificationService pushService, IReferenceEventService referenceEventService, GlobalSettings globalSettings, - IPolicyRepository policyRepository, IPolicyService policyService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { _sendRepository = sendRepository; _userRepository = userRepository; _userService = userService; - _policyRepository = policyRepository; _policyService = policyService; _organizationRepository = organizationRepository; _sendFileStorageService = sendFileStorageService; @@ -64,6 +67,8 @@ public class SendService : ISendService _referenceEventService = referenceEventService; _globalSettings = globalSettings; _currentContext = currentContext; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } public async Task SaveSendAsync(Send send) @@ -286,6 +291,12 @@ public class SendService : ISendService private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + await ValidateUserCanSaveAsync_vNext(userId, send); + return; + } + if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true)) { return; @@ -308,6 +319,26 @@ public class SendService : ISendService } } + private async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) + { + if (!userId.HasValue) + { + return; + } + + var sendPolicyRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + + if (sendPolicyRequirement.DisableSend) + { + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + if (sendPolicyRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + } + } + private async Task StorageRemainingForSendAsync(Send send) { var storageBytesRemaining = 0L; diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs new file mode 100644 index 0000000000..87ea390cb6 --- /dev/null +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.Test.AdminConsole.AutoFixture; + +internal class PolicyDetailsCustomization( + PolicyType policyType, + OrganizationUserType userType, + bool isProvider, + OrganizationUserStatusType userStatus) : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.PolicyType, policyType) + .With(o => o.OrganizationUserType, userType) + .With(o => o.IsProvider, isProvider) + .With(o => o.OrganizationUserStatus, userStatus) + .Without(o => o.PolicyData)); // avoid autogenerating invalid json data + } +} + +public class PolicyDetailsAttribute( + PolicyType policyType, + OrganizationUserType userType = OrganizationUserType.User, + bool isProvider = false, + OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) + => new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs new file mode 100644 index 0000000000..3323c9c754 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Utilities; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public static class PolicyDetailsTestExtensions +{ + public static void SetDataModel(this PolicyDetails policyDetails, T data) where T : IPolicyDataModel + => policyDetails.PolicyData = CoreHelpers.ClassToJsonData(data); +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs new file mode 100644 index 0000000000..4d7bf5db4e --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs @@ -0,0 +1,138 @@ +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Enums; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public class SendPolicyRequirementTests +{ + [Theory, AutoData] + public void DisableSend_IsFalse_IfNoDisableSendPolicies( + [PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1, + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails otherPolicy2) + { + EnableDisableHideEmail(otherPolicy2); + + var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]); + + Assert.False(actual.DisableSend); + } + + [Theory] + [InlineAutoData(OrganizationUserType.Owner, false)] + [InlineAutoData(OrganizationUserType.Admin, false)] + [InlineAutoData(OrganizationUserType.User, true)] + [InlineAutoData(OrganizationUserType.Custom, true)] + public void DisableSend_TestRoles( + OrganizationUserType userType, + bool shouldBeEnforced, + [PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails) + { + policyDetails.OrganizationUserType = userType; + + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.Equal(shouldBeEnforced, actual.DisableSend); + } + + [Theory, AutoData] + public void DisableSend_Not_EnforcedAgainstProviders( + [PolicyDetails(PolicyType.DisableSend, isProvider: true)] PolicyDetails policyDetails) + { + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.False(actual.DisableSend); + } + + [Theory] + [InlineAutoData(OrganizationUserStatusType.Confirmed, true)] + [InlineAutoData(OrganizationUserStatusType.Accepted, true)] + [InlineAutoData(OrganizationUserStatusType.Invited, false)] + [InlineAutoData(OrganizationUserStatusType.Revoked, false)] + public void DisableSend_TestStatuses( + OrganizationUserStatusType userStatus, + bool shouldBeEnforced, + [PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails) + { + policyDetails.OrganizationUserStatus = userStatus; + + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.Equal(shouldBeEnforced, actual.DisableSend); + } + + [Theory, AutoData] + public void DisableHideEmail_IsFalse_IfNoSendOptionsPolicies( + [PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1, + [PolicyDetails(PolicyType.DisableSend)] PolicyDetails otherPolicy2) + { + var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]); + + Assert.False(actual.DisableHideEmail); + } + + [Theory] + [InlineAutoData(OrganizationUserType.Owner, false)] + [InlineAutoData(OrganizationUserType.Admin, false)] + [InlineAutoData(OrganizationUserType.User, true)] + [InlineAutoData(OrganizationUserType.Custom, true)] + public void DisableHideEmail_TestRoles( + OrganizationUserType userType, + bool shouldBeEnforced, + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) + { + EnableDisableHideEmail(policyDetails); + policyDetails.OrganizationUserType = userType; + + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.Equal(shouldBeEnforced, actual.DisableHideEmail); + } + + [Theory, AutoData] + public void DisableHideEmail_Not_EnforcedAgainstProviders( + [PolicyDetails(PolicyType.SendOptions, isProvider: true)] PolicyDetails policyDetails) + { + EnableDisableHideEmail(policyDetails); + + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.False(actual.DisableHideEmail); + } + + [Theory] + [InlineAutoData(OrganizationUserStatusType.Confirmed, true)] + [InlineAutoData(OrganizationUserStatusType.Accepted, true)] + [InlineAutoData(OrganizationUserStatusType.Invited, false)] + [InlineAutoData(OrganizationUserStatusType.Revoked, false)] + public void DisableHideEmail_TestStatuses( + OrganizationUserStatusType userStatus, + bool shouldBeEnforced, + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) + { + EnableDisableHideEmail(policyDetails); + policyDetails.OrganizationUserStatus = userStatus; + + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.Equal(shouldBeEnforced, actual.DisableHideEmail); + } + + [Theory, AutoData] + public void DisableHideEmail_HandlesNullData( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) + { + policyDetails.PolicyData = null; + + var actual = SendPolicyRequirement.Create([policyDetails]); + + Assert.False(actual.DisableHideEmail); + } + + private static void EnableDisableHideEmail(PolicyDetails policyDetails) + => policyDetails.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); +} diff --git a/test/Core.Test/Tools/AutoFixture/SendFixtures.cs b/test/Core.Test/Tools/AutoFixture/SendFixtures.cs index c8005f4faf..0d58ca1671 100644 --- a/test/Core.Test/Tools/AutoFixture/SendFixtures.cs +++ b/test/Core.Test/Tools/AutoFixture/SendFixtures.cs @@ -1,4 +1,6 @@ -using AutoFixture; +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; using Bit.Core.Tools.Entities; using Bit.Test.Common.AutoFixture.Attributes; @@ -19,3 +21,20 @@ internal class UserSendCustomizeAttribute : BitCustomizeAttribute { public override ICustomization GetCustomization() => new UserSend(); } + +internal class NewUserSend : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(s => s.Id, Guid.Empty) + .Without(s => s.OrganizationId)); + } +} + +internal class NewUserSendCustomizeAttribute : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameterInfo) + => new NewUserSend(); +} + diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs index cabb438b61..ae65ee1388 100644 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendServiceTests.cs @@ -3,6 +3,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -22,6 +24,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Xunit; using GlobalSettings = Bit.Core.Settings.GlobalSettings; @@ -118,6 +121,93 @@ public class SendServiceTests await sutProvider.GetDependency().Received(1).CreateAsync(send); } + // Disable Send policy check - vNext + private void SaveSendAsync_Setup_vNext(SutProvider sutProvider, Send send, + SendPolicyRequirement sendPolicyRequirement) + { + sutProvider.GetDependency().GetAsync(send.UserId!.Value) + .Returns(sendPolicyRequirement); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Should not be called in these tests + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync( + Arg.Any(), Arg.Any()).ThrowsAsync(); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true }); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); + Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.", + exception.Message); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + + // Send Options Policy - Disable Hide Email check + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + send.HideEmail = true; + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); + Assert.Contains("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.", exception.Message); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + send.HideEmail = false; + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + + [Theory] + [BitAutoData(SendType.File)] + [BitAutoData(SendType.Text)] + public async Task SaveSendAsync_DisableHideEmail_DoesntApply_Success_vNext(SendType sendType, + SutProvider sutProvider, [NewUserSendCustomize] Send send) + { + send.Type = sendType; + SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + send.HideEmail = true; + + await sutProvider.Sut.SaveSendAsync(send); + + await sutProvider.GetDependency().Received(1).CreateAsync(send); + } + [Theory] [BitAutoData] public async Task SaveSendAsync_ExistingSend_Updates(SutProvider sutProvider, From 2b1db97d5c3438b9dd909fa06892248653437295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:40:53 +0000 Subject: [PATCH 23/84] [PM-18085] Add Manage property to UserCipherDetails (#5390) * Add Manage permission to UserCipherDetails and CipherDetails_ReadByIdUserId * Add Manage property to CipherDetails and UserCipherDetailsQuery * Add integration test for CipherRepository Manage permission rules * Update CipherDetails_ReadWithoutOrganizationsByUserId to include Manage permission * Refactor UserCipherDetailsQuery to include detailed permission and organization properties * Refactor CipherRepositoryTests to improve test organization and readability - Split large test method into smaller, focused methods - Added helper methods for creating test data and performing assertions - Improved test coverage for cipher permissions in different scenarios - Maintained existing test logic while enhancing code structure * Refactor CipherRepositoryTests to consolidate cipher permission tests - Removed redundant helper methods for permission assertions - Simplified test methods for GetCipherPermissionsForOrganizationAsync, GetManyByUserIdAsync, and GetByIdAsync - Maintained existing test coverage for cipher manage permissions - Improved code readability and reduced code duplication * Add integration test for CipherRepository group collection manage permissions - Added new test method GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules - Implemented helper method CreateCipherInOrganizationCollectionWithGroup to support group-based collection permission testing - Verified manage permissions are correctly applied based on group collection access settings * Add @Manage parameter to Cipher stored procedures - Updated CipherDetails_Create, CipherDetails_CreateWithCollections, and CipherDetails_Update stored procedures - Added @Manage parameter with comment "-- not used" - Included new stored procedure implementations in migration script - Consistent with previous work on adding Manage property to cipher details * Update UserCipherDetails functions to reorder Manage and ViewPassword columns * Reorder Manage and ViewPassword properties in cipher details queries * Bump date in migration script --- src/Core/Vault/Models/Data/CipherDetails.cs | 2 + .../Queries/UserCipherDetailsQuery.cs | 51 ++- .../Vault/Repositories/CipherRepository.cs | 1 + .../Vault/dbo/Functions/UserCipherDetails.sql | 6 + .../Cipher/CipherDetails_Create.sql | 3 +- .../CipherDetails_CreateWithCollections.sql | 5 +- .../Cipher/CipherDetails_ReadByIdUserId.sql | 3 +- ...tails_ReadWithoutOrganizationsByUserId.sql | 1 + .../Cipher/CipherDetails_Update.sql | 5 +- .../Repositories/CipherRepositoryTests.cs | 271 +++++++++++++++ .../2025-02-19_00_UserCipherDetailsManage.sql | 309 ++++++++++++++++++ 11 files changed, 645 insertions(+), 12 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-02-19_00_UserCipherDetailsManage.sql diff --git a/src/Core/Vault/Models/Data/CipherDetails.cs b/src/Core/Vault/Models/Data/CipherDetails.cs index 716b49ca4f..e0ece1efec 100644 --- a/src/Core/Vault/Models/Data/CipherDetails.cs +++ b/src/Core/Vault/Models/Data/CipherDetails.cs @@ -8,6 +8,7 @@ public class CipherDetails : CipherOrganizationDetails public bool Favorite { get; set; } public bool Edit { get; set; } public bool ViewPassword { get; set; } + public bool Manage { get; set; } public CipherDetails() { } @@ -53,6 +54,7 @@ public class CipherDetailsWithCollections : CipherDetails Favorite = cipher.Favorite; Edit = cipher.Edit; ViewPassword = cipher.ViewPassword; + Manage = cipher.Manage; CollectionIds = collectionCiphersGroupDict.TryGetValue(Id, out var value) ? value.Select(cc => cc.CollectionId) diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs index fdfb9a1bc9..507849f51b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs @@ -50,11 +50,49 @@ public class UserCipherDetailsQuery : IQuery where (cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null - select c; + select new + { + c.Id, + c.UserId, + c.OrganizationId, + c.Type, + c.Data, + c.Attachments, + c.CreationDate, + c.RevisionDate, + c.DeletedDate, + c.Favorites, + c.Folders, + Edit = cu == null ? (cg != null && cg.ReadOnly == false) : cu.ReadOnly == false, + ViewPassword = cu == null ? (cg != null && cg.HidePasswords == false) : cu.HidePasswords == false, + Manage = cu == null ? (cg != null && cg.Manage == true) : cu.Manage == true, + OrganizationUseTotp = o.UseTotp, + c.Reprompt, + c.Key + }; var query2 = from c in dbContext.Ciphers where c.UserId == _userId - select c; + select new + { + c.Id, + c.UserId, + c.OrganizationId, + c.Type, + c.Data, + c.Attachments, + c.CreationDate, + c.RevisionDate, + c.DeletedDate, + c.Favorites, + c.Folders, + Edit = true, + ViewPassword = true, + Manage = true, + OrganizationUseTotp = false, + c.Reprompt, + c.Key + }; var union = query.Union(query2).Select(c => new CipherDetails { @@ -68,11 +106,12 @@ public class UserCipherDetailsQuery : IQuery RevisionDate = c.RevisionDate, DeletedDate = c.DeletedDate, Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"), - FolderId = GetFolderId(_userId, c), - Edit = true, + FolderId = GetFolderId(_userId, new Cipher { Id = c.Id, Folders = c.Folders }), + Edit = c.Edit, Reprompt = c.Reprompt, - ViewPassword = true, - OrganizationUseTotp = false, + ViewPassword = c.ViewPassword, + Manage = c.Manage, + OrganizationUseTotp = c.OrganizationUseTotp, Key = c.Key }); return union; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 6a4ffb4b35..9c91609b1b 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -432,6 +432,7 @@ public class CipherRepository : Repository c.Id == manageCipher.Id); + Assert.NotNull(managePermission); + Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission"); + + var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id); + Assert.NotNull(nonManagePermission); + Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission"); + } + + [DatabaseTheory, DatabaseData] + public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules( + ICipherRepository cipherRepository, + IUserRepository userRepository, + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository) + { + var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository); + + var group = await groupRepository.CreateAsync(new Group + { + OrganizationId = organization.Id, + Name = "Test Group", + }); + await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id }); + + var (manageCipher, nonManageCipher) = await CreateCipherInOrganizationCollectionWithGroup( + organization, group, cipherRepository, collectionRepository, collectionCipherRepository, groupRepository); + + var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id); + Assert.Equal(2, permissions.Count); + + var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id); + Assert.NotNull(managePermission); + Assert.True(managePermission.Manage, "Collection with Group Manage=true should grant Manage permission"); + + var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id); + Assert.NotNull(nonManagePermission); + Assert.False(nonManagePermission.Manage, "Collection with Group Manage=false should not grant Manage permission"); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByUserIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules( + ICipherRepository cipherRepository, + IUserRepository userRepository, + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository); + + var manageCipher = await CreateCipherInOrganizationCollection( + organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository, + hasManagePermission: true, "Manage Collection"); + + var nonManageCipher = await CreateCipherInOrganizationCollection( + organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository, + hasManagePermission: false, "Non-Manage Collection"); + + var personalCipher = await CreatePersonalCipher(user, cipherRepository); + + var userCiphers = await cipherRepository.GetManyByUserIdAsync(user.Id); + Assert.Equal(3, userCiphers.Count); + + var managePermission = userCiphers.FirstOrDefault(c => c.Id == manageCipher.Id); + Assert.NotNull(managePermission); + Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission"); + + var nonManagePermission = userCiphers.FirstOrDefault(c => c.Id == nonManageCipher.Id); + Assert.NotNull(nonManagePermission); + Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission"); + + var personalPermission = userCiphers.FirstOrDefault(c => c.Id == personalCipher.Id); + Assert.NotNull(personalPermission); + Assert.True(personalPermission.Manage, "Personal ciphers should always have Manage permission"); + } + + [DatabaseTheory, DatabaseData] + public async Task GetByIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules( + ICipherRepository cipherRepository, + IUserRepository userRepository, + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository); + + var manageCipher = await CreateCipherInOrganizationCollection( + organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository, + hasManagePermission: true, "Manage Collection"); + + var nonManageCipher = await CreateCipherInOrganizationCollection( + organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository, + hasManagePermission: false, "Non-Manage Collection"); + + var personalCipher = await CreatePersonalCipher(user, cipherRepository); + + var manageDetails = await cipherRepository.GetByIdAsync(manageCipher.Id, user.Id); + Assert.NotNull(manageDetails); + Assert.True(manageDetails.Manage, "Collection with Manage=true should grant Manage permission"); + + var nonManageDetails = await cipherRepository.GetByIdAsync(nonManageCipher.Id, user.Id); + Assert.NotNull(nonManageDetails); + Assert.False(nonManageDetails.Manage, "Collection with Manage=false should not grant Manage permission"); + + var personalDetails = await cipherRepository.GetByIdAsync(personalCipher.Id, user.Id); + Assert.NotNull(personalDetails); + Assert.True(personalDetails.Manage, "Personal ciphers should always have Manage permission"); + } + + private async Task<(User user, Organization org, OrganizationUser orgUser)> CreateTestUserAndOrganization( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = user.Email, + Plan = "Test" + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + UserId = user.Id, + OrganizationId = organization.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }); + + return (user, organization, orgUser); + } + + private async Task CreateCipherInOrganizationCollection( + Organization organization, + OrganizationUser orgUser, + ICipherRepository cipherRepository, + ICollectionRepository collectionRepository, + ICollectionCipherRepository collectionCipherRepository, + bool hasManagePermission, + string collectionName) + { + var collection = await collectionRepository.CreateAsync(new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + }); + + var cipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id, + new List { collection.Id }); + + await collectionRepository.UpdateUsersAsync(collection.Id, new List + { + new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = hasManagePermission } + }); + + return cipher; + } + + private async Task<(Cipher manageCipher, Cipher nonManageCipher)> CreateCipherInOrganizationCollectionWithGroup( + Organization organization, + Group group, + ICipherRepository cipherRepository, + ICollectionRepository collectionRepository, + ICollectionCipherRepository collectionCipherRepository, + IGroupRepository groupRepository) + { + var manageCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Group Manage Collection", + OrganizationId = organization.Id, + }); + + var nonManageCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Group Non-Manage Collection", + OrganizationId = organization.Id, + }); + + var manageCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var nonManageCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id, + new List { manageCollection.Id }); + await collectionCipherRepository.UpdateCollectionsForAdminAsync(nonManageCipher.Id, organization.Id, + new List { nonManageCollection.Id }); + + await groupRepository.ReplaceAsync(group, + new[] + { + new CollectionAccessSelection + { + Id = manageCollection.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + new CollectionAccessSelection + { + Id = nonManageCollection.Id, + HidePasswords = false, + ReadOnly = false, + Manage = false + } + }); + + return (manageCipher, nonManageCipher); + } + + private async Task CreatePersonalCipher(User user, ICipherRepository cipherRepository) + { + return await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + UserId = user.Id, + Data = "" + }); + } } diff --git a/util/Migrator/DbScripts/2025-02-19_00_UserCipherDetailsManage.sql b/util/Migrator/DbScripts/2025-02-19_00_UserCipherDetailsManage.sql new file mode 100644 index 0000000000..c6420ff13f --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-19_00_UserCipherDetailsManage.sql @@ -0,0 +1,309 @@ +CREATE OR ALTER FUNCTION [dbo].[UserCipherDetails](@UserId UNIQUEIDENTIFIER) +RETURNS TABLE +AS RETURN +WITH [CTE] AS ( + SELECT + [Id], + [OrganizationId] + FROM + [OrganizationUser] + WHERE + [UserId] = @UserId + AND [Status] = 2 -- Confirmed +) +SELECT + C.*, + CASE + WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0 + THEN 1 + ELSE 0 + END [Edit], + CASE + WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0 + THEN 1 + ELSE 0 + END [ViewPassword], + CASE + WHEN COALESCE(CU.[Manage], CG.[Manage], 0) = 1 + THEN 1 + ELSE 0 + END [Manage], + CASE + WHEN O.[UseTotp] = 1 + THEN 1 + ELSE 0 + END [OrganizationUseTotp] +FROM + [dbo].[CipherDetails](@UserId) C +INNER JOIN + [CTE] OU ON C.[UserId] IS NULL AND C.[OrganizationId] IN (SELECT [OrganizationId] FROM [CTE]) +INNER JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] AND O.[Id] = C.[OrganizationId] AND O.[Enabled] = 1 +LEFT JOIN + [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] +LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] +LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] +LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] +LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId] +WHERE + CU.[CollectionId] IS NOT NULL + OR CG.[CollectionId] IS NOT NULL + +UNION ALL + +SELECT + *, + 1 [Edit], + 1 [ViewPassword], + 1 [Manage], + 0 [OrganizationUseTotp] +FROM + [dbo].[CipherDetails](@UserId) +WHERE + [UserId] = @UserId +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp], + MAX ([Edit]) AS [Edit], + MAX ([ViewPassword]) AS [ViewPassword], + MAX ([Manage]) AS [Manage] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Id] = @Id + GROUP BY + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp] +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadWithoutOrganizationsByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + *, + 1 [Edit], + 1 [ViewPassword], + 1 [Manage], + 0 [OrganizationUseTotp] + FROM + [dbo].[CipherDetails](@UserId) + WHERE + [UserId] = @UserId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Favorites], + [Folders], + [CreationDate], + [RevisionDate], + [DeletedDate], + [Reprompt], + [Key] + ) + VALUES + ( + @Id, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + @OrganizationId, + @Type, + @Data, + CASE WHEN @Favorite = 1 THEN CONCAT('{', @UserIdKey, ':true}') ELSE NULL END, + CASE WHEN @FolderId IS NOT NULL THEN CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') ELSE NULL END, + @CreationDate, + @RevisionDate, + @DeletedDate, + @Reprompt, + @Key + ) + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_CreateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @CollectionIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, + @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword, @Manage, + @OrganizationUseTotp, @DeletedDate, @Reprompt, @Key + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(2), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Folders] = + CASE + WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN + CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') + WHEN @FolderId IS NOT NULL THEN + JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50))) + ELSE + JSON_MODIFY([Folders], @UserIdPath, NULL) + END, + [Favorites] = + CASE + WHEN @Favorite = 1 AND [Favorites] IS NULL THEN + CONCAT('{', @UserIdKey, ':true}') + WHEN @Favorite = 1 THEN + JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT)) + ELSE + JSON_MODIFY([Favorites], @UserIdPath, NULL) + END, + [Attachments] = @Attachments, + [Reprompt] = @Reprompt, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Key] = @Key + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO From 6bc579f51ea3930444981778024abf272c26ba5c Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 24 Feb 2025 12:32:27 +0000 Subject: [PATCH 24/84] Bumped version to 2025.2.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index c797513c63..4b56322adb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.2.0 + 2025.2.1 Bit.$(MSBuildProjectName) enable From 6ca98df721aacccfe9b79e4855c110d882b74fee Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 24 Feb 2025 10:42:04 -0500 Subject: [PATCH 25/84] Ac/pm 17449/add managed user validation to email token (#5437) --- .../Auth/Controllers/AccountsController.cs | 7 ++++- src/Core/Exceptions/BadRequestException.cs | 14 +++++++++- src/Core/Services/IUserService.cs | 10 +++++++ .../Services/Implementations/UserService.cs | 4 +-- .../Controllers/AccountsControllerTests.cs | 28 ++++++++++++++----- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index f3af49e6c3..176bc183b6 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -149,6 +149,12 @@ public class AccountsController : Controller throw new BadRequestException("MasterPasswordHash", "Invalid password."); } + var managedUserValidationResult = await _userService.ValidateManagedUserDomainAsync(user, model.NewEmail); + + if (!managedUserValidationResult.Succeeded) + { + throw new BadRequestException(managedUserValidationResult.Errors); + } await _userService.InitiateEmailChangeAsync(user, model.NewEmail); } @@ -167,7 +173,6 @@ public class AccountsController : Controller throw new BadRequestException("You cannot change your email when using Key Connector."); } - var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail, model.NewMasterPasswordHash, model.Token, model.Key); if (result.Succeeded) diff --git a/src/Core/Exceptions/BadRequestException.cs b/src/Core/Exceptions/BadRequestException.cs index e7268b6c55..042f853a57 100644 --- a/src/Core/Exceptions/BadRequestException.cs +++ b/src/Core/Exceptions/BadRequestException.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Bit.Core.Exceptions; @@ -29,5 +30,16 @@ public class BadRequestException : Exception ModelState = modelState; } + public BadRequestException(IEnumerable identityErrors) + : base("The model state is invalid.") + { + ModelState = new ModelStateDictionary(); + + foreach (var error in identityErrors) + { + ModelState.AddModelError(error.Code, error.Description); + } + } + public ModelStateDictionary ModelState { get; set; } } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 2ac7796547..b6a1d1f05b 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -136,6 +136,16 @@ public interface IUserService /// Task IsManagedByAnyOrganizationAsync(Guid userId); + /// + /// Verify whether the new email domain meets the requirements for managed users. + /// + /// + /// + /// + /// IdentityResult + /// + Task ValidateManagedUserDomainAsync(User user, string newEmail); + /// /// Gets the organizations that manage the user. /// diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 2374d8f4e1..e419b832a7 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -545,7 +545,7 @@ public class UserService : UserManager, IUserService, IDisposable return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } - var managedUserValidationResult = await ValidateManagedUserAsync(user, newEmail); + var managedUserValidationResult = await ValidateManagedUserDomainAsync(user, newEmail); if (!managedUserValidationResult.Succeeded) { @@ -617,7 +617,7 @@ public class UserService : UserManager, IUserService, IDisposable return IdentityResult.Success; } - private async Task ValidateManagedUserAsync(User user, string newEmail) + public async Task ValidateManagedUserDomainAsync(User user, string newEmail) { var managingOrganizations = await GetOrganizationsManagingUserAsync(user.Id); diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 8bdb14bf78..6a9862b3d6 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -134,29 +134,43 @@ public class AccountsControllerTests : IDisposable [Fact] public async Task PostEmailToken_ShouldInitiateEmailChange() { + // Arrange var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToAcceptPasswordFor(user); - var newEmail = "example@user.com"; + const string newEmail = "example@user.com"; + _userService.ValidateManagedUserDomainAsync(user, newEmail).Returns(IdentityResult.Success); + // Act await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail }); + // Assert await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail); } [Fact] - public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldInitiateEmailChange() + public async Task PostEmailToken_WhenValidateManagedUserDomainAsyncFails_ShouldReturnError() { + // Arrange var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToAcceptPasswordFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - _userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false); - var newEmail = "example@user.com"; - await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail }); + const string newEmail = "example@user.com"; - await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail); + _userService.ValidateManagedUserDomainAsync(user, newEmail) + .Returns(IdentityResult.Failed(new IdentityError + { + Code = "TestFailure", + Description = "This is a test." + })); + + + // Act + // Assert + await Assert.ThrowsAsync( + () => _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail }) + ); } [Fact] From 0f10ca52b4886dc1afa3f624ddb5d7f342cb5c1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:45:39 -0500 Subject: [PATCH 26/84] [deps] Auth: Lock file maintenance (#5301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 204 ++++++++++---------- src/Admin/package-lock.json | 204 ++++++++++---------- 2 files changed, 214 insertions(+), 194 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index a0e7b767cc..1105d74cf1 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -98,9 +98,9 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -119,25 +119,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -156,9 +156,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -177,9 +177,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -198,9 +198,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -219,9 +219,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -240,9 +240,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -261,9 +261,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -282,9 +282,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -303,9 +303,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -345,9 +345,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -366,9 +366,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -387,9 +387,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], @@ -455,9 +455,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "license": "MIT", "dependencies": { @@ -781,9 +781,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -821,9 +821,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "dev": true, "funding": [ { @@ -975,16 +975,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.75", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", - "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", + "version": "1.5.103", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz", + "integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -1009,9 +1009,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -1114,10 +1114,20 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -1632,9 +1642,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -1652,7 +1662,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1724,9 +1734,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", "dependencies": { @@ -1765,13 +1775,13 @@ } }, "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -1949,9 +1959,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -2078,9 +2088,9 @@ } }, "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2153,9 +2163,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -2174,7 +2184,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 152edd6fc9..6f3298df5b 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -99,9 +99,9 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -120,25 +120,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -157,9 +157,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -178,9 +178,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -199,9 +199,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -220,9 +220,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -241,9 +241,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -262,9 +262,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -283,9 +283,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -346,9 +346,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -367,9 +367,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -388,9 +388,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "license": "MIT", "dependencies": { @@ -782,9 +782,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -822,9 +822,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "dev": true, "funding": [ { @@ -976,16 +976,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.75", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", - "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", + "version": "1.5.103", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz", + "integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", - "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -1010,9 +1010,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -1115,10 +1115,20 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -1633,9 +1643,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -1653,7 +1663,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1725,9 +1735,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", "dependencies": { @@ -1766,13 +1776,13 @@ } }, "node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -1950,9 +1960,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -2079,9 +2089,9 @@ } }, "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2162,9 +2172,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -2183,7 +2193,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" From d15c1faa74ff5151e02b6c4ffe0254c385783eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:57:30 +0000 Subject: [PATCH 27/84] [PM-12491] Create Organization disable command (#5348) * Add command interface and implementation for disabling organizations * Register organization disable command for dependency injection * Add unit tests for OrganizationDisableCommand * Refactor subscription handlers to use IOrganizationDisableCommand for disabling organizations * Remove DisableAsync method from IOrganizationService and its implementation in OrganizationService * Remove IOrganizationService dependency from SubscriptionDeletedHandler * Remove commented TODO for sending email to owners in OrganizationDisableCommand --- .../SubscriptionDeletedHandler.cs | 11 +-- .../SubscriptionUpdatedHandler.cs | 7 +- .../Interfaces/IOrganizationDisableCommand.cs | 14 ++++ .../OrganizationDisableCommand.cs | 33 ++++++++ .../Services/IOrganizationService.cs | 1 - .../Implementations/OrganizationService.cs | 14 ---- ...OrganizationServiceCollectionExtensions.cs | 4 + .../OrganizationDisableCommandTests.cs | 79 +++++++++++++++++++ 8 files changed, 141 insertions(+), 22 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index 26a1c30c14..8155928453 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -1,4 +1,5 @@ using Bit.Billing.Constants; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Services; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -6,20 +7,20 @@ namespace Bit.Billing.Services.Implementations; public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler { private readonly IStripeEventService _stripeEventService; - private readonly IOrganizationService _organizationService; private readonly IUserService _userService; private readonly IStripeEventUtilityService _stripeEventUtilityService; + private readonly IOrganizationDisableCommand _organizationDisableCommand; public SubscriptionDeletedHandler( IStripeEventService stripeEventService, - IOrganizationService organizationService, IUserService userService, - IStripeEventUtilityService stripeEventUtilityService) + IStripeEventUtilityService stripeEventUtilityService, + IOrganizationDisableCommand organizationDisableCommand) { _stripeEventService = stripeEventService; - _organizationService = organizationService; _userService = userService; _stripeEventUtilityService = stripeEventUtilityService; + _organizationDisableCommand = organizationDisableCommand; } /// @@ -44,7 +45,7 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler subscription.CancellationDetails.Comment != providerMigrationCancellationComment && !subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)) { - await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); } else if (userId.HasValue) { diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 4e142f8cae..35a16ae74f 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -26,6 +26,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly ISchedulerFactory _schedulerFactory; private readonly IFeatureService _featureService; private readonly IOrganizationEnableCommand _organizationEnableCommand; + private readonly IOrganizationDisableCommand _organizationDisableCommand; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -38,7 +39,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IOrganizationRepository organizationRepository, ISchedulerFactory schedulerFactory, IFeatureService featureService, - IOrganizationEnableCommand organizationEnableCommand) + IOrganizationEnableCommand organizationEnableCommand, + IOrganizationDisableCommand organizationDisableCommand) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -51,6 +53,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _schedulerFactory = schedulerFactory; _featureService = featureService; _organizationEnableCommand = organizationEnableCommand; + _organizationDisableCommand = organizationDisableCommand; } /// @@ -67,7 +70,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired when organizationId.HasValue: { - await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); if (subscription.Status == StripeSubscriptionStatus.Unpaid && subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" }) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs new file mode 100644 index 0000000000..d15e9537e6 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +/// +/// Command interface for disabling organizations. +/// +public interface IOrganizationDisableCommand +{ + /// + /// Disables an organization with an optional expiration date. + /// + /// The unique identifier of the organization to disable. + /// Optional date when the disable status should expire. + Task DisableAsync(Guid organizationId, DateTime? expirationDate); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs new file mode 100644 index 0000000000..63f80032b8 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs @@ -0,0 +1,33 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class OrganizationDisableCommand : IOrganizationDisableCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IApplicationCacheService _applicationCacheService; + + public OrganizationDisableCommand( + IOrganizationRepository organizationRepository, + IApplicationCacheService applicationCacheService) + { + _organizationRepository = organizationRepository; + _applicationCacheService = applicationCacheService; + } + + public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization is { Enabled: true }) + { + organization.Enabled = false; + organization.ExpirationDate = expirationDate; + organization.RevisionDate = DateTime.UtcNow; + + await _organizationRepository.ReplaceAsync(organization); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + } + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 683fbe9902..dacb2ab162 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -28,7 +28,6 @@ public interface IOrganizationService /// Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); - Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 284c11cc78..14cf89a246 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -686,20 +686,6 @@ public class OrganizationService : IOrganizationService } } - public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) - { - var org = await GetOrgById(organizationId); - if (org != null && org.Enabled) - { - org.Enabled = false; - org.ExpirationDate = expirationDate; - org.RevisionDate = DateTime.UtcNow; - await ReplaceAndUpdateCacheAsync(org); - - // TODO: send email to owners? - } - } - public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 7db514887c..232e04fbd0 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -55,6 +55,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationSignUpCommands(); services.AddOrganizationDeleteCommands(); services.AddOrganizationEnableCommands(); + services.AddOrganizationDisableCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); @@ -73,6 +74,9 @@ public static class OrganizationServiceCollectionExtensions private static void AddOrganizationEnableCommands(this IServiceCollection services) => services.AddScoped(); + private static void AddOrganizationDisableCommands(this IServiceCollection services) => + services.AddScoped(); + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs new file mode 100644 index 0000000000..9e77a56b93 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs @@ -0,0 +1,79 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class OrganizationDisableCommandTests +{ + [Theory, BitAutoData] + public async Task DisableAsync_WhenOrganizationEnabled_DisablesSuccessfully( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = true; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.DisableAsync(organization.Id, expirationDate); + + Assert.False(organization.Enabled); + Assert.Equal(expirationDate, organization.ExpirationDate); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task DisableAsync_WhenOrganizationNotFound_DoesNothing( + Guid organizationId, + DateTime expirationDate, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(organizationId) + .Returns((Organization)null); + + await sutProvider.Sut.DisableAsync(organizationId, expirationDate); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task DisableAsync_WhenOrganizationAlreadyDisabled_DoesNothing( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.DisableAsync(organization.Id, expirationDate); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } +} From 66feebd35833e622d56ccbf09dcff3b61e869b4f Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:32:19 +0100 Subject: [PATCH 28/84] [PM-13127]Breadcrumb event logs (#5430) * Rename the feature flag to lowercase * Rename the feature flag to epic --------- Signed-off-by: Cy Okeke --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 879e8365fc..a862978722 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -173,6 +173,7 @@ public static class FeatureFlagKeys public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; public const string AndroidImportLoginsFlow = "import-logins-flow"; + public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public static List GetAllKeys() { From 622ef902ed882ffd62ccce21e7eae73e2a71350d Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:36:12 -0500 Subject: [PATCH 29/84] [PM-18578] Don't enable automatic tax for non-taxable non-US businesses during `invoice.upcoming` (#5443) * Only enable automatic tax for US subscriptions or EU subscriptions that are taxable. * Run dotnet format --- .../Implementations/UpcomingInvoiceHandler.cs | 216 +++++++++--------- .../Billing/Extensions/CustomerExtensions.cs | 9 + 2 files changed, 113 insertions(+), 112 deletions(-) diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 409bd0d18b..5315195c59 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,6 +1,7 @@ -using Bit.Billing.Constants; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; @@ -11,94 +12,66 @@ using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; -public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler +public class UpcomingInvoiceHandler( + ILogger logger, + IMailService mailService, + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + IStripeFacade stripeFacade, + IStripeEventService stripeEventService, + IStripeEventUtilityService stripeEventUtilityService, + IUserRepository userRepository, + IValidateSponsorshipCommand validateSponsorshipCommand) + : IUpcomingInvoiceHandler { - private readonly ILogger _logger; - private readonly IStripeEventService _stripeEventService; - private readonly IUserService _userService; - private readonly IStripeFacade _stripeFacade; - private readonly IMailService _mailService; - private readonly IProviderRepository _providerRepository; - private readonly IValidateSponsorshipCommand _validateSponsorshipCommand; - private readonly IOrganizationRepository _organizationRepository; - private readonly IStripeEventUtilityService _stripeEventUtilityService; - - public UpcomingInvoiceHandler( - ILogger logger, - IStripeEventService stripeEventService, - IUserService userService, - IStripeFacade stripeFacade, - IMailService mailService, - IProviderRepository providerRepository, - IValidateSponsorshipCommand validateSponsorshipCommand, - IOrganizationRepository organizationRepository, - IStripeEventUtilityService stripeEventUtilityService) - { - _logger = logger; - _stripeEventService = stripeEventService; - _userService = userService; - _stripeFacade = stripeFacade; - _mailService = mailService; - _providerRepository = providerRepository; - _validateSponsorshipCommand = validateSponsorshipCommand; - _organizationRepository = organizationRepository; - _stripeEventUtilityService = stripeEventUtilityService; - } - - /// - /// Handles the event type from Stripe. - /// - /// - /// public async Task HandleAsync(Event parsedEvent) { - var invoice = await _stripeEventService.GetInvoice(parsedEvent); + var invoice = await stripeEventService.GetInvoice(parsedEvent); + if (string.IsNullOrEmpty(invoice.SubscriptionId)) { - _logger.LogWarning("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id); + logger.LogInformation("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id); return; } - var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); - - if (subscription == null) + var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId, new SubscriptionGetOptions { - throw new Exception( - $"Received null Subscription from Stripe for ID '{invoice.SubscriptionId}' while processing Event with ID '{parsedEvent.Id}'"); - } + Expand = ["customer.tax", "customer.tax_ids"] + }); - var updatedSubscription = await TryEnableAutomaticTaxAsync(subscription); - - var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(updatedSubscription.Metadata); - - var invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList(); + var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); if (organizationId.HasValue) { - if (_stripeEventUtilityService.IsSponsoredSubscription(updatedSubscription)) - { - var sponsorshipIsValid = - await _validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value); - if (!sponsorshipIsValid) - { - // If the sponsorship is invalid, then the subscription was updated to use the regular families plan - // price. Given that this is the case, we need the new invoice amount - subscription = await _stripeFacade.GetSubscription(subscription.Id, - new SubscriptionGetOptions { Expand = ["latest_invoice"] }); + var organization = await organizationRepository.GetByIdAsync(organizationId.Value); - invoice = subscription.LatestInvoice; - invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList(); - } - } - - var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); - - if (organization == null || !OrgPlanForInvoiceNotifications(organization)) + if (organization == null) { return; } - await SendEmails(new List { organization.BillingEmail }); + await TryEnableAutomaticTaxAsync(subscription); + + if (!HasAnnualPlan(organization)) + { + return; + } + + if (stripeEventUtilityService.IsSponsoredSubscription(subscription)) + { + var sponsorshipIsValid = await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value); + + if (!sponsorshipIsValid) + { + /* + * If the sponsorship is invalid, then the subscription was updated to use the regular families plan + * price. Given that this is the case, we need the new invoice amount + */ + invoice = await stripeFacade.GetInvoice(subscription.LatestInvoiceId); + } + } + + await SendUpcomingInvoiceEmailsAsync(new List { organization.BillingEmail }, invoice); /* * TODO: https://bitwarden.atlassian.net/browse/PM-4862 @@ -113,66 +86,85 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler } else if (userId.HasValue) { - var user = await _userService.GetUserByIdAsync(userId.Value); + var user = await userRepository.GetByIdAsync(userId.Value); - if (user?.Premium == true) + if (user == null) { - await SendEmails(new List { user.Email }); + return; + } + + await TryEnableAutomaticTaxAsync(subscription); + + if (user.Premium) + { + await SendUpcomingInvoiceEmailsAsync(new List { user.Email }, invoice); } } else if (providerId.HasValue) { - var provider = await _providerRepository.GetByIdAsync(providerId.Value); + var provider = await providerRepository.GetByIdAsync(providerId.Value); if (provider == null) { - _logger.LogError( - "Received invoice.Upcoming webhook ({EventID}) for Provider ({ProviderID}) that does not exist", - parsedEvent.Id, - providerId.Value); - return; } - await SendEmails(new List { provider.BillingEmail }); + await TryEnableAutomaticTaxAsync(subscription); + await SendUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice); } + } + + private async Task SendUpcomingInvoiceEmailsAsync(IEnumerable emails, Invoice invoice) + { + var validEmails = emails.Where(e => !string.IsNullOrEmpty(e)); + + var items = invoice.Lines.Select(i => i.Description).ToList(); + + if (invoice.NextPaymentAttempt.HasValue && invoice.AmountDue > 0) + { + await mailService.SendInvoiceUpcoming( + validEmails, + invoice.AmountDue / 100M, + invoice.NextPaymentAttempt.Value, + items, + true); + } + } + + private async Task TryEnableAutomaticTaxAsync(Subscription subscription) + { + if (subscription.AutomaticTax.Enabled || + !subscription.Customer.HasBillingLocation() || + IsNonTaxableNonUSBusinessUseSubscription(subscription)) + { + return; + } + + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + DefaultTaxRates = [], + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); return; - /* - * Sends emails to the given email addresses. - */ - async Task SendEmails(IEnumerable emails) + bool IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) { - var validEmails = emails.Where(e => !string.IsNullOrEmpty(e)); - - if (invoice.NextPaymentAttempt.HasValue && invoice.AmountDue > 0) + var familyPriceIds = new List { - await _mailService.SendInvoiceUpcoming( - validEmails, - invoice.AmountDue / 100M, - invoice.NextPaymentAttempt.Value, - invoiceLineItemDescriptions, - true); - } + // TODO: Replace with the PricingClient + StaticStore.GetPlan(PlanType.FamiliesAnnually2019).PasswordManager.StripePlanId, + StaticStore.GetPlan(PlanType.FamiliesAnnually).PasswordManager.StripePlanId + }; + + return localSubscription.Customer.Address.Country != "US" && + localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) && + !localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() && + !localSubscription.Customer.TaxIds.Any(); } } - private async Task TryEnableAutomaticTaxAsync(Subscription subscription) - { - var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] }; - var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions); - - var subscriptionUpdateOptions = new SubscriptionUpdateOptions(); - - if (!subscriptionUpdateOptions.EnableAutomaticTax(customer, subscription)) - { - return subscription; - } - - return await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); - } - - private static bool OrgPlanForInvoiceNotifications(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual; + private static bool HasAnnualPlan(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual; } diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs index 62f1a5055c..1847abb0ad 100644 --- a/src/Core/Billing/Extensions/CustomerExtensions.cs +++ b/src/Core/Billing/Extensions/CustomerExtensions.cs @@ -5,6 +5,15 @@ namespace Bit.Core.Billing.Extensions; public static class CustomerExtensions { + public static bool HasBillingLocation(this Customer customer) + => customer is + { + Address: + { + Country: not null and not "", + PostalCode: not null and not "" + } + }; /// /// Determines if a Stripe customer supports automatic tax From 492a3d64843b7c37796113ebb7228130c0d03a64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:42:40 -0500 Subject: [PATCH 30/84] [deps] Platform: Update azure azure-sdk-for-net monorepo (#4815) * [deps] Platform: Update azure azure-sdk-for-net monorepo * fix: fixing tests for package downgrade --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike Kottlowski --- src/Api/Api.csproj | 3 ++- src/Core/Core.csproj | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 6505fdab5b..44b0c7e969 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -35,7 +35,8 @@ - + + diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 860cf33298..6d681a3638 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -25,18 +25,18 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + From dd78361aa49f59a772d58e830d8c4b8e2fa148c2 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:44:35 -0500 Subject: [PATCH 31/84] Revert "[deps] Platform: Update azure azure-sdk-for-net monorepo (#4815)" (#5447) This reverts commit 492a3d64843b7c37796113ebb7228130c0d03a64. --- src/Api/Api.csproj | 3 +-- src/Core/Core.csproj | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 44b0c7e969..6505fdab5b 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -35,8 +35,7 @@ - - + diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 6d681a3638..860cf33298 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -25,18 +25,18 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + From 4a4d256fd9bdbb00e9f3bb786f785d28706e1d7b Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 26 Feb 2025 13:48:51 -0800 Subject: [PATCH 32/84] [PM-16787] Web push enablement for server (#5395) * Allow for binning of comb IDs by date and value * Introduce notification hub pool * Replace device type sharding with comb + range sharding * Fix proxy interface * Use enumerable services for multiServiceNotificationHub * Fix push interface usage * Fix push notification service dependencies * Fix push notification keys * Fixup documentation * Remove deprecated settings * Fix tests * PascalCase method names * Remove unused request model properties * Remove unused setting * Improve DateFromComb precision * Prefer readonly service enumerable * Pascal case template holes * Name TryParse methods TryParse * Apply suggestions from code review Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Include preferred push technology in config response SignalR will be the fallback, but clients should attempt web push first if offered and available to the client. * Register web push devices * Working signing and content encrypting * update to RFC-8291 and RFC-8188 * Notification hub is now working, no need to create our own * Fix body * Flip Success Check * use nifty json attribute * Remove vapid private key This is only needed to encrypt data for transmission along webpush -- it's handled by NotificationHub for us * Add web push feature flag to control config response * Update src/Core/NotificationHub/NotificationHubConnection.cs Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Update src/Core/NotificationHub/NotificationHubConnection.cs Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * fixup! Update src/Core/NotificationHub/NotificationHubConnection.cs * Move to platform ownership * Remove debugging extension * Remove unused dependencies * Set json content directly * Name web push registration data * Fix FCM type typo * Determine specific feature flag from set of flags * Fixup merged tests * Fixup tests * Code quality suggestions * Fix merged tests * Fix test --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- src/Api/Controllers/ConfigController.cs | 2 +- src/Api/Controllers/DevicesController.cs | 13 ++ src/Api/Models/Request/DeviceRequestModels.cs | 21 ++ .../Models/Response/ConfigResponseModel.cs | 32 ++- .../Push/Controllers/PushController.cs | 6 +- src/Api/Platform/Push/PushTechnologyType.cs | 11 ++ src/Core/Constants.cs | 1 + .../NotificationHub/INotificationHubPool.cs | 1 + .../NotificationHubConnection.cs | 46 ++++- .../NotificationHub/NotificationHubPool.cs | 15 +- .../NotificationHubPushRegistrationService.cs | 184 +++++++++++++----- .../NotificationHub/PushRegistrationData.cs | 50 +++++ .../Push/Services/IPushRegistrationService.cs | 4 +- .../Services/NoopPushRegistrationService.cs | 3 +- .../Services/RelayPushRegistrationService.cs | 5 +- src/Core/Services/IDeviceService.cs | 2 + .../Services/Implementations/DeviceService.cs | 19 +- src/Core/Settings/GlobalSettings.cs | 7 + src/Core/Settings/IGlobalSettings.cs | 1 + src/Core/Settings/IWebPushSettings.cs | 6 + .../Push/Controllers/PushControllerTests.cs | 11 +- .../NotificationHubConnectionTests.cs | 3 +- ...ficationHubPushRegistrationServiceTests.cs | 10 +- test/Core.Test/Services/DeviceServiceTests.cs | 9 +- .../Factories/WebApplicationFactoryBase.cs | 4 + 25 files changed, 383 insertions(+), 83 deletions(-) create mode 100644 src/Api/Platform/Push/PushTechnologyType.cs create mode 100644 src/Core/NotificationHub/PushRegistrationData.cs create mode 100644 src/Core/Settings/IWebPushSettings.cs diff --git a/src/Api/Controllers/ConfigController.cs b/src/Api/Controllers/ConfigController.cs index 7699c6b115..9f38a644c2 100644 --- a/src/Api/Controllers/ConfigController.cs +++ b/src/Api/Controllers/ConfigController.cs @@ -23,6 +23,6 @@ public class ConfigController : Controller [HttpGet("")] public ConfigResponseModel GetConfigs() { - return new ConfigResponseModel(_globalSettings, _featureService.GetAll()); + return new ConfigResponseModel(_featureService, _globalSettings); } } diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index aab898cd62..02eb2d36d5 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -186,6 +186,19 @@ public class DevicesController : Controller await _deviceService.SaveAsync(model.ToDevice(device)); } + [HttpPut("identifier/{identifier}/web-push-auth")] + [HttpPost("identifier/{identifier}/web-push-auth")] + public async Task PutWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model) + { + var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value); + if (device == null) + { + throw new NotFoundException(); + } + + await _deviceService.SaveAsync(model.ToData(), device); + } + [AllowAnonymous] [HttpPut("identifier/{identifier}/clear-token")] [HttpPost("identifier/{identifier}/clear-token")] diff --git a/src/Api/Models/Request/DeviceRequestModels.cs b/src/Api/Models/Request/DeviceRequestModels.cs index 60f17bd0ee..99465501d9 100644 --- a/src/Api/Models/Request/DeviceRequestModels.cs +++ b/src/Api/Models/Request/DeviceRequestModels.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.NotificationHub; using Bit.Core.Utilities; namespace Bit.Api.Models.Request; @@ -37,6 +38,26 @@ public class DeviceRequestModel } } +public class WebPushAuthRequestModel +{ + [Required] + public string Endpoint { get; set; } + [Required] + public string P256dh { get; set; } + [Required] + public string Auth { get; set; } + + public WebPushRegistrationData ToData() + { + return new WebPushRegistrationData + { + Endpoint = Endpoint, + P256dh = P256dh, + Auth = Auth + }; + } +} + public class DeviceTokenRequestModel { [StringLength(255)] diff --git a/src/Api/Models/Response/ConfigResponseModel.cs b/src/Api/Models/Response/ConfigResponseModel.cs index 7328f1d164..4571089295 100644 --- a/src/Api/Models/Response/ConfigResponseModel.cs +++ b/src/Api/Models/Response/ConfigResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Models.Api; +using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -11,6 +14,7 @@ public class ConfigResponseModel : ResponseModel public ServerConfigResponseModel Server { get; set; } public EnvironmentConfigResponseModel Environment { get; set; } public IDictionary FeatureStates { get; set; } + public PushSettings Push { get; set; } public ServerSettingsResponseModel Settings { get; set; } public ConfigResponseModel() : base("config") @@ -23,8 +27,9 @@ public class ConfigResponseModel : ResponseModel } public ConfigResponseModel( - IGlobalSettings globalSettings, - IDictionary featureStates) : base("config") + IFeatureService featureService, + IGlobalSettings globalSettings + ) : base("config") { Version = AssemblyHelpers.GetVersion(); GitHash = AssemblyHelpers.GetGitHash(); @@ -37,7 +42,9 @@ public class ConfigResponseModel : ResponseModel Notifications = globalSettings.BaseServiceUri.Notifications, Sso = globalSettings.BaseServiceUri.Sso }; - FeatureStates = featureStates; + FeatureStates = featureService.GetAll(); + var webPushEnabled = FeatureStates.TryGetValue(FeatureFlagKeys.WebPush, out var webPushEnabledValue) ? (bool)webPushEnabledValue : false; + Push = PushSettings.Build(webPushEnabled, globalSettings); Settings = new ServerSettingsResponseModel { DisableUserRegistration = globalSettings.DisableUserRegistration @@ -61,6 +68,23 @@ public class EnvironmentConfigResponseModel public string Sso { get; set; } } +public class PushSettings +{ + public PushTechnologyType PushTechnology { get; private init; } + public string VapidPublicKey { get; private init; } + + public static PushSettings Build(bool webPushEnabled, IGlobalSettings globalSettings) + { + var vapidPublicKey = webPushEnabled ? globalSettings.WebPush.VapidPublicKey : null; + var pushTechnology = vapidPublicKey != null ? PushTechnologyType.WebPush : PushTechnologyType.SignalR; + return new() + { + VapidPublicKey = vapidPublicKey, + PushTechnology = pushTechnology + }; + } +} + public class ServerSettingsResponseModel { public bool DisableUserRegistration { get; set; } diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index f88fa4aa9e..28641a86cf 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -1,6 +1,7 @@ using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Api; +using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -42,9 +43,8 @@ public class PushController : Controller public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model) { CheckUsage(); - await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId), - Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), - model.InstallationId); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken), Prefix(model.DeviceId), + Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), model.InstallationId); } [HttpPost("delete")] diff --git a/src/Api/Platform/Push/PushTechnologyType.cs b/src/Api/Platform/Push/PushTechnologyType.cs new file mode 100644 index 0000000000..cc89abacaa --- /dev/null +++ b/src/Api/Platform/Push/PushTechnologyType.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Enums; + +public enum PushTechnologyType +{ + [Display(Name = "SignalR")] + SignalR = 0, + [Display(Name = "WebPush")] + WebPush = 1, +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a862978722..82ceb817e3 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -172,6 +172,7 @@ public static class FeatureFlagKeys public const string AndroidMutualTls = "mutual-tls"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; + public const string WebPush = "web-push"; public const string AndroidImportLoginsFlow = "import-logins-flow"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/NotificationHub/INotificationHubPool.cs index 18bae98bc6..3981598118 100644 --- a/src/Core/NotificationHub/INotificationHubPool.cs +++ b/src/Core/NotificationHub/INotificationHubPool.cs @@ -4,6 +4,7 @@ namespace Bit.Core.NotificationHub; public interface INotificationHubPool { + NotificationHubConnection ConnectionFor(Guid comb); INotificationHubClient ClientFor(Guid comb); INotificationHubProxy AllClients { get; } } diff --git a/src/Core/NotificationHub/NotificationHubConnection.cs b/src/Core/NotificationHub/NotificationHubConnection.cs index 3a1437f70c..a68134450e 100644 --- a/src/Core/NotificationHub/NotificationHubConnection.cs +++ b/src/Core/NotificationHub/NotificationHubConnection.cs @@ -1,11 +1,20 @@ -using Bit.Core.Settings; +using System.Security.Cryptography; +using System.Text; +using System.Web; +using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Azure.NotificationHubs; -class NotificationHubConnection +namespace Bit.Core.NotificationHub; + +public class NotificationHubConnection { public string HubName { get; init; } public string ConnectionString { get; init; } + private Lazy _parsedConnectionString; + public Uri Endpoint => _parsedConnectionString.Value.Endpoint; + private string SasKey => _parsedConnectionString.Value.SharedAccessKey; + private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName; public bool EnableSendTracing { get; init; } private NotificationHubClient _hubClient; /// @@ -95,7 +104,38 @@ class NotificationHubConnection return RegistrationStartDate < queryTime; } - private NotificationHubConnection() { } + public HttpRequestMessage CreateRequest(HttpMethod method, string pathUri, params string[] queryParameters) + { + var uriBuilder = new UriBuilder(Endpoint) + { + Scheme = "https", + Path = $"{HubName}/{pathUri.TrimStart('/')}", + Query = string.Join('&', [.. queryParameters, "api-version=2015-01"]), + }; + + var result = new HttpRequestMessage(method, uriBuilder.Uri); + result.Headers.Add("Authorization", GenerateSasToken(uriBuilder.Uri)); + result.Headers.Add("TrackingId", Guid.NewGuid().ToString()); + return result; + } + + private string GenerateSasToken(Uri uri) + { + string targetUri = Uri.EscapeDataString(uri.ToString().ToLower()).ToLower(); + long expires = DateTime.UtcNow.AddMinutes(1).Ticks / TimeSpan.TicksPerSecond; + string stringToSign = targetUri + "\n" + expires; + + using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SasKey))) + { + var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign))); + return $"SharedAccessSignature sr={targetUri}&sig={HttpUtility.UrlEncode(signature)}&se={expires}&skn={SasKeyName}"; + } + } + + private NotificationHubConnection() + { + _parsedConnectionString = new(() => new NotificationHubConnectionStringBuilder(ConnectionString)); + } /// /// Creates a new NotificationHubConnection from the given settings. diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/NotificationHub/NotificationHubPool.cs index 8993ee2b8e..6b48e82f88 100644 --- a/src/Core/NotificationHub/NotificationHubPool.cs +++ b/src/Core/NotificationHub/NotificationHubPool.cs @@ -44,6 +44,18 @@ public class NotificationHubPool : INotificationHubPool /// /// Thrown when no notification hub is found for a given comb. public INotificationHubClient ClientFor(Guid comb) + { + var resolvedConnection = ConnectionFor(comb); + return resolvedConnection.HubClient; + } + + /// + /// Gets the NotificationHubConnection for the given comb ID. + /// + /// + /// + /// Thrown when no notification hub is found for a given comb. + public NotificationHubConnection ConnectionFor(Guid comb) { var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray(); if (possibleConnections.Length == 0) @@ -55,7 +67,8 @@ public class NotificationHubPool : INotificationHubPool } var resolvedConnection = possibleConnections[CoreHelpers.BinForComb(comb, possibleConnections.Length)]; _logger.LogTrace("Resolved notification hub for comb {Comb} out of {HubCount} hubs.\n{ConnectionInfo}", comb, possibleConnections.Length, resolvedConnection.LogString); - return resolvedConnection.HubClient; + return resolvedConnection; + } public INotificationHubProxy AllClients { get { return new NotificationHubClientProxy(_clients); } } diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index 9793c8198a..f44fcf91a0 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -1,82 +1,131 @@ -using Bit.Core.Enums; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Encodings.Web; +using System.Text.Json; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Azure.NotificationHubs; +using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; public class NotificationHubPushRegistrationService : IPushRegistrationService { + private static readonly JsonSerializerOptions webPushSerializationOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly INotificationHubPool _notificationHubPool; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; public NotificationHubPushRegistrationService( IInstallationDeviceRepository installationDeviceRepository, - INotificationHubPool notificationHubPool) + INotificationHubPool notificationHubPool, + IHttpClientFactory httpClientFactory, + ILogger logger) { _installationDeviceRepository = installationDeviceRepository; _notificationHubPool = notificationHubPool; + _httpClientFactory = httpClientFactory; + _logger = logger; } - public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { - if (string.IsNullOrWhiteSpace(pushToken)) - { - return; - } - + var orgIds = organizationIds.ToList(); + var clientType = DeviceTypes.ToClientType(type); var installation = new Installation { InstallationId = deviceId, - PushChannel = pushToken, + PushChannel = data.Token, + Tags = new List + { + $"userId:{userId}", + $"clientType:{clientType}" + }.Concat(orgIds.Select(organizationId => $"organizationId:{organizationId}")).ToList(), Templates = new Dictionary() }; - var clientType = DeviceTypes.ToClientType(type); - - installation.Tags = new List { $"userId:{userId}", $"clientType:{clientType}" }; - if (!string.IsNullOrWhiteSpace(identifier)) { installation.Tags.Add("deviceIdentifier:" + identifier); } - var organizationIdsList = organizationIds.ToList(); - foreach (var organizationId in organizationIdsList) - { - installation.Tags.Add($"organizationId:{organizationId}"); - } - if (installationId != Guid.Empty) { installation.Tags.Add($"installationId:{installationId}"); } - string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null; + if (data.Token != null) + { + await CreateOrUpdateMobileRegistrationAsync(installation, userId, identifier, clientType, orgIds, type, installationId); + } + else if (data.WebPush != null) + { + await CreateOrUpdateWebRegistrationAsync(data.WebPush.Value.Endpoint, data.WebPush.Value.P256dh, data.WebPush.Value.Auth, installation, userId, identifier, clientType, orgIds, installationId); + } + + if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) + { + await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); + } + } + + private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId, + string identifier, ClientType clientType, List organizationIds, DeviceType type, Guid installationId) + { + if (string.IsNullOrWhiteSpace(installation.PushChannel)) + { + return; + } + switch (type) { case DeviceType.Android: - payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}"; - messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + - "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; + installation.Templates.Add(BuildInstallationTemplate("payload", + "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("message", + "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + + "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("badgeMessage", + "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + + "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}", + userId, identifier, clientType, organizationIds, installationId)); installation.Platform = NotificationPlatform.FcmV1; break; case DeviceType.iOS: - payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," + - "\"aps\":{\"content-available\":1}}"; - messageTemplate = "{\"data\":{\"type\":\"#(type)\"}," + - "\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}"; - badgeMessageTemplate = "{\"data\":{\"type\":\"#(type)\"}," + - "\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}"; - + installation.Templates.Add(BuildInstallationTemplate("payload", + "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," + + "\"aps\":{\"content-available\":1}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("message", + "{\"data\":{\"type\":\"#(type)\"}," + + "\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}", userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("badgeMessage", + "{\"data\":{\"type\":\"#(type)\"}," + + "\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}", + userId, identifier, clientType, organizationIds, installationId)); installation.Platform = NotificationPlatform.Apns; break; case DeviceType.AndroidAmazon: - payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}"; - messageTemplate = "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}"; + installation.Templates.Add(BuildInstallationTemplate("payload", + "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("message", + "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("badgeMessage", + "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}", + userId, identifier, clientType, organizationIds, installationId)); installation.Platform = NotificationPlatform.Adm; break; @@ -84,28 +133,62 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService break; } - BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType, - organizationIdsList, installationId); - BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType, - organizationIdsList, installationId); - BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, - userId, identifier, clientType, organizationIdsList, installationId); - - await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation); - if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) - { - await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); - } + await ClientFor(GetComb(installation.InstallationId)).CreateOrUpdateInstallationAsync(installation); } - private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody, - string userId, string identifier, ClientType clientType, List organizationIds, Guid installationId) + private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId, + string identifier, ClientType clientType, List organizationIds, Guid installationId) { - if (templateBody == null) + // The Azure SDK is currently lacking support for web push registrations. + // We need to use the REST API directly. + + if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(p256dh) || string.IsNullOrWhiteSpace(auth)) { return; } + installation.Templates.Add(BuildInstallationTemplate("payload", + "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("message", + "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}", + userId, identifier, clientType, organizationIds, installationId)); + installation.Templates.Add(BuildInstallationTemplate("badgeMessage", + "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}", + userId, identifier, clientType, organizationIds, installationId)); + + var content = new + { + installationId = installation.InstallationId, + pushChannel = new + { + endpoint, + p256dh, + auth + }, + platform = "browser", + tags = installation.Tags, + templates = installation.Templates + }; + + var client = _httpClientFactory.CreateClient("NotificationHub"); + var request = ConnectionFor(GetComb(installation.InstallationId)).CreateRequest(HttpMethod.Put, $"installations/{installation.InstallationId}"); + request.Content = JsonContent.Create(content, new MediaTypeHeaderValue("application/json"), webPushSerializationOptions); + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Web push registration failed: {Response}", body); + } + else + { + _logger.LogInformation("Web push registration success: {Response}", body); + } + } + + private static KeyValuePair BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody, + string userId, string identifier, ClientType clientType, List organizationIds, Guid installationId) + { var fullTemplateId = $"template:{templateId}"; var template = new InstallationTemplate @@ -132,7 +215,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService template.Tags.Add($"installationId:{installationId}"); } - installation.Templates.Add(fullTemplateId, template); + return new KeyValuePair(fullTemplateId, template); } public async Task DeleteRegistrationAsync(string deviceId) @@ -213,6 +296,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService return _notificationHubPool.ClientFor(deviceId); } + private NotificationHubConnection ConnectionFor(Guid deviceId) + { + return _notificationHubPool.ConnectionFor(deviceId); + } + private Guid GetComb(string deviceId) { var deviceIdString = deviceId; diff --git a/src/Core/NotificationHub/PushRegistrationData.cs b/src/Core/NotificationHub/PushRegistrationData.cs new file mode 100644 index 0000000000..0cdf981ee2 --- /dev/null +++ b/src/Core/NotificationHub/PushRegistrationData.cs @@ -0,0 +1,50 @@ +namespace Bit.Core.NotificationHub; + +public struct WebPushRegistrationData : IEquatable +{ + public string Endpoint { get; init; } + public string P256dh { get; init; } + public string Auth { get; init; } + + public bool Equals(WebPushRegistrationData other) + { + return Endpoint == other.Endpoint && P256dh == other.P256dh && Auth == other.Auth; + } + + public override int GetHashCode() + { + return HashCode.Combine(Endpoint, P256dh, Auth); + } +} + +public class PushRegistrationData : IEquatable +{ + public string Token { get; set; } + public WebPushRegistrationData? WebPush { get; set; } + public PushRegistrationData(string token) + { + Token = token; + } + + public PushRegistrationData(string Endpoint, string P256dh, string Auth) : this(new WebPushRegistrationData + { + Endpoint = Endpoint, + P256dh = P256dh, + Auth = Auth + }) + { } + + public PushRegistrationData(WebPushRegistrationData webPush) + { + WebPush = webPush; + } + public bool Equals(PushRegistrationData other) + { + return Token == other.Token && WebPush.Equals(other.WebPush); + } + + public override int GetHashCode() + { + return HashCode.Combine(Token, WebPush.GetHashCode()); + } +} diff --git a/src/Core/Platform/Push/Services/IPushRegistrationService.cs b/src/Core/Platform/Push/Services/IPushRegistrationService.cs index 0f2a28700b..469cd2577b 100644 --- a/src/Core/Platform/Push/Services/IPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/IPushRegistrationService.cs @@ -1,11 +1,11 @@ using Bit.Core.Enums; +using Bit.Core.NotificationHub; namespace Bit.Core.Platform.Push; public interface IPushRegistrationService { - Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId); + Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId); Task DeleteRegistrationAsync(string deviceId); Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); diff --git a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs index ac6f8a814b..9a7674232a 100644 --- a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs @@ -1,4 +1,5 @@ using Bit.Core.Enums; +using Bit.Core.NotificationHub; namespace Bit.Core.Platform.Push.Internal; @@ -9,7 +10,7 @@ public class NoopPushRegistrationService : IPushRegistrationService return Task.FromResult(0); } - public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + public Task CreateOrUpdateRegistrationAsync(PushRegistrationData pushRegistrationData, string deviceId, string userId, string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { return Task.FromResult(0); diff --git a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs index 58a34c15c5..1a3843d05a 100644 --- a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs @@ -1,6 +1,7 @@ using Bit.Core.Enums; using Bit.Core.IdentityServer; using Bit.Core.Models.Api; +using Bit.Core.NotificationHub; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; @@ -24,14 +25,14 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi { } - public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData pushData, string deviceId, string userId, string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { var requestModel = new PushRegistrationRequestModel { DeviceId = deviceId, Identifier = identifier, - PushToken = pushToken, + PushToken = pushData.Token, Type = type, UserId = userId, OrganizationIds = organizationIds, diff --git a/src/Core/Services/IDeviceService.cs b/src/Core/Services/IDeviceService.cs index b5f3a0b8f1..cd055f8b46 100644 --- a/src/Core/Services/IDeviceService.cs +++ b/src/Core/Services/IDeviceService.cs @@ -1,10 +1,12 @@ using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Entities; +using Bit.Core.NotificationHub; namespace Bit.Core.Services; public interface IDeviceService { + Task SaveAsync(WebPushRegistrationData webPush, Device device); Task SaveAsync(Device device); Task ClearTokenAsync(Device device); Task DeactivateAsync(Device device); diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index c8b0134932..99523d8e5e 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -28,9 +29,19 @@ public class DeviceService : IDeviceService _globalSettings = globalSettings; } + public async Task SaveAsync(WebPushRegistrationData webPush, Device device) + { + await SaveAsync(new PushRegistrationData(webPush.Endpoint, webPush.P256dh, webPush.Auth), device); + } + public async Task SaveAsync(Device device) { - if (device.Id == default(Guid)) + await SaveAsync(new PushRegistrationData(device.PushToken), device); + } + + private async Task SaveAsync(PushRegistrationData data, Device device) + { + if (device.Id == default) { await _deviceRepository.CreateAsync(device); } @@ -45,9 +56,9 @@ public class DeviceService : IDeviceService OrganizationUserStatusType.Confirmed)) .Select(ou => ou.OrganizationId.ToString()); - await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(), - device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString, - _globalSettings.Installation.Id); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync(data, device.Id.ToString(), + device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString, _globalSettings.Installation.Id); + } public async Task ClearTokenAsync(Device device) diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index dbfc8543a3..6bb76eb50a 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -83,6 +83,8 @@ public class GlobalSettings : IGlobalSettings public virtual IDomainVerificationSettings DomainVerification { get; set; } = new DomainVerificationSettings(); public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings(); public virtual string DevelopmentDirectory { get; set; } + public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings(); + public virtual bool EnableEmailVerification { get; set; } public virtual string KdfDefaultHashKey { get; set; } public virtual string PricingUri { get; set; } @@ -677,4 +679,9 @@ public class GlobalSettings : IGlobalSettings public virtual IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings(); public virtual IConnectionStringSettings Cosmos { get; set; } = new ConnectionStringSettings(); } + + public class WebPushSettings : IWebPushSettings + { + public string VapidPublicKey { get; set; } + } } diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index b89df8abf5..411014ea32 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -27,5 +27,6 @@ public interface IGlobalSettings string DatabaseProvider { get; set; } GlobalSettings.SqlSettings SqlServer { get; set; } string DevelopmentDirectory { get; set; } + IWebPushSettings WebPush { get; set; } GlobalSettings.EventLoggingSettings EventLogging { get; set; } } diff --git a/src/Core/Settings/IWebPushSettings.cs b/src/Core/Settings/IWebPushSettings.cs new file mode 100644 index 0000000000..d63bec23f5 --- /dev/null +++ b/src/Core/Settings/IWebPushSettings.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Settings; + +public interface IWebPushSettings +{ + public string VapidPublicKey { get; set; } +} diff --git a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs index 70e1e83edb..b796a445ae 100644 --- a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs +++ b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs @@ -4,6 +4,7 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Api; +using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; @@ -248,7 +249,7 @@ public class PushControllerTests Assert.Equal("Not correctly configured for push relays.", exception.Message); await sutProvider.GetDependency().Received(0) - .CreateOrUpdateRegistrationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + .CreateOrUpdateRegistrationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()); } @@ -265,7 +266,7 @@ public class PushControllerTests var expectedDeviceId = $"{installationId}_{deviceId}"; var expectedOrganizationId = $"{installationId}_{organizationId}"; - await sutProvider.Sut.RegisterAsync(new PushRegistrationRequestModel + var model = new PushRegistrationRequestModel { DeviceId = deviceId.ToString(), PushToken = "test-push-token", @@ -274,10 +275,12 @@ public class PushControllerTests Identifier = identifier.ToString(), OrganizationIds = [organizationId.ToString()], InstallationId = installationId - }); + }; + + await sutProvider.Sut.RegisterAsync(model); await sutProvider.GetDependency().Received(1) - .CreateOrUpdateRegistrationAsync("test-push-token", expectedDeviceId, expectedUserId, + .CreateOrUpdateRegistrationAsync(Arg.Is(data => data.Equals(new PushRegistrationData(model.PushToken))), expectedDeviceId, expectedUserId, expectedIdentifier, DeviceType.Android, Arg.Do>(organizationIds => { var organizationIdsList = organizationIds.ToList(); diff --git a/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs b/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs index 0d7382b3cc..fc76e5c1b7 100644 --- a/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Settings; +using Bit.Core.NotificationHub; +using Bit.Core.Settings; using Bit.Core.Utilities; using Xunit; diff --git a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs index 77551f53e7..b30cd3dda8 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs @@ -19,7 +19,7 @@ public class NotificationHubPushRegistrationServiceTests SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, Guid organizationId, Guid installationId) { - await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(), identifier.ToString(), DeviceType.Android, [organizationId.ToString()], installationId); sutProvider.GetDependency() @@ -39,7 +39,7 @@ public class NotificationHubPushRegistrationServiceTests var pushToken = "test push token"; - await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.Android, partOfOrganizationId ? [organizationId.ToString()] : [], installationIdNull ? Guid.Empty : installationId); @@ -115,7 +115,7 @@ public class NotificationHubPushRegistrationServiceTests var pushToken = "test push token"; - await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.iOS, partOfOrganizationId ? [organizationId.ToString()] : [], installationIdNull ? Guid.Empty : installationId); @@ -191,7 +191,7 @@ public class NotificationHubPushRegistrationServiceTests var pushToken = "test push token"; - await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.AndroidAmazon, partOfOrganizationId ? [organizationId.ToString()] : [], installationIdNull ? Guid.Empty : installationId); @@ -268,7 +268,7 @@ public class NotificationHubPushRegistrationServiceTests var pushToken = "test push token"; - await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), + await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(), identifier.ToString(), deviceType, [organizationId.ToString()], installationId); sutProvider.GetDependency() diff --git a/test/Core.Test/Services/DeviceServiceTests.cs b/test/Core.Test/Services/DeviceServiceTests.cs index 95a93cf4e8..b454a0c04b 100644 --- a/test/Core.Test/Services/DeviceServiceTests.cs +++ b/test/Core.Test/Services/DeviceServiceTests.cs @@ -4,6 +4,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -43,13 +44,13 @@ public class DeviceServiceTests Name = "test device", Type = DeviceType.Android, UserId = userId, - PushToken = "testtoken", + PushToken = "testToken", Identifier = "testid" }; await deviceService.SaveAsync(device); Assert.True(device.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1)); - await pushRepo.Received(1).CreateOrUpdateRegistrationAsync("testtoken", id.ToString(), + await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is(v => v.Token == "testToken"), id.ToString(), userId.ToString(), "testid", DeviceType.Android, Arg.Do>(organizationIds => { @@ -84,12 +85,12 @@ public class DeviceServiceTests Name = "test device", Type = DeviceType.Android, UserId = userId, - PushToken = "testtoken", + PushToken = "testToken", Identifier = "testid" }; await deviceService.SaveAsync(device); - await pushRepo.Received(1).CreateOrUpdateRegistrationAsync("testtoken", + await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is(v => v.Token == "testToken"), Arg.Do(id => Guid.TryParse(id, out var _)), userId.ToString(), "testid", DeviceType.Android, Arg.Do>(organizationIds => { diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index 7c7f790cdc..c1089608da 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -163,6 +163,10 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // New Device Verification { "globalSettings:disableEmailNewDevice", "false" }, + + // Web push notifications + { "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" }, + { "globalSettings:launchDarkly:flagValues:web-push", "true" }, }); }); From bd66f06bd99856985e4587e6cd8094e4bb9ae392 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 26 Feb 2025 14:41:24 -0800 Subject: [PATCH 33/84] Prefer record to implementing IEquatable (#5449) --- .../NotificationHub/PushRegistrationData.cs | 23 ++----------------- .../Push/Controllers/PushControllerTests.cs | 2 +- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/Core/NotificationHub/PushRegistrationData.cs b/src/Core/NotificationHub/PushRegistrationData.cs index 0cdf981ee2..20e1cf0936 100644 --- a/src/Core/NotificationHub/PushRegistrationData.cs +++ b/src/Core/NotificationHub/PushRegistrationData.cs @@ -1,23 +1,13 @@ namespace Bit.Core.NotificationHub; -public struct WebPushRegistrationData : IEquatable +public record struct WebPushRegistrationData { public string Endpoint { get; init; } public string P256dh { get; init; } public string Auth { get; init; } - - public bool Equals(WebPushRegistrationData other) - { - return Endpoint == other.Endpoint && P256dh == other.P256dh && Auth == other.Auth; - } - - public override int GetHashCode() - { - return HashCode.Combine(Endpoint, P256dh, Auth); - } } -public class PushRegistrationData : IEquatable +public record class PushRegistrationData { public string Token { get; set; } public WebPushRegistrationData? WebPush { get; set; } @@ -38,13 +28,4 @@ public class PushRegistrationData : IEquatable { WebPush = webPush; } - public bool Equals(PushRegistrationData other) - { - return Token == other.Token && WebPush.Equals(other.WebPush); - } - - public override int GetHashCode() - { - return HashCode.Combine(Token, WebPush.GetHashCode()); - } } diff --git a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs index b796a445ae..399913a0c4 100644 --- a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs +++ b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs @@ -280,7 +280,7 @@ public class PushControllerTests await sutProvider.Sut.RegisterAsync(model); await sutProvider.GetDependency().Received(1) - .CreateOrUpdateRegistrationAsync(Arg.Is(data => data.Equals(new PushRegistrationData(model.PushToken))), expectedDeviceId, expectedUserId, + .CreateOrUpdateRegistrationAsync(Arg.Is(data => data == new PushRegistrationData(model.PushToken)), expectedDeviceId, expectedUserId, expectedIdentifier, DeviceType.Android, Arg.Do>(organizationIds => { var organizationIdsList = organizationIds.ToList(); From a2e665cb96c883e7eba7a2568b9811913615df32 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:55:46 -0500 Subject: [PATCH 34/84] [PM-16684] Integrate Pricing Service behind FF (#5276) * Remove gRPC and convert PricingClient to HttpClient wrapper * Add PlanType.GetProductTier extension Many instances of StaticStore use are just to get the ProductTierType of a PlanType, but this can be derived from the PlanType itself without having to fetch the entire plan. * Remove invocations of the StaticStore in non-Test code * Deprecate StaticStore entry points * Run dotnet format * Matt's feedback * Run dotnet format * Rui's feedback * Run dotnet format * Replacements since approval * Run dotnet format --- .../RemoveOrganizationFromProviderCommand.cs | 11 +- .../AdminConsole/Services/ProviderService.cs | 26 ++- .../Billing/ProviderBillingService.cs | 31 +-- .../Queries/Projects/MaxProjectsQuery.cs | 1 + ...oveOrganizationFromProviderCommandTests.cs | 3 + .../Services/ProviderServiceTests.cs | 7 + .../Billing/ProviderBillingServiceTests.cs | 83 +++++++ .../Controllers/OrganizationsController.cs | 17 +- .../Models/OrganizationEditModel.cs | 8 +- .../OrganizationUsersController.cs | 10 +- .../Controllers/OrganizationsController.cs | 25 +- .../OrganizationResponseModel.cs | 23 +- .../ProfileOrganizationResponseModel.cs | 5 +- ...rofileProviderOrganizationResponseModel.cs | 6 +- .../OrganizationBillingController.cs | 4 +- .../Controllers/OrganizationsController.cs | 37 +-- .../Controllers/ProviderBillingController.cs | 16 +- .../Responses/ProviderSubscriptionResponse.cs | 18 +- .../Controllers/OrganizationController.cs | 9 +- ...anizationSubscriptionUpdateRequestModel.cs | 9 +- src/Api/Controllers/PlansController.cs | 10 +- ...elfHostedOrganizationLicensesController.cs | 3 +- ...tsManagerSubscriptionUpdateRequestModel.cs | 5 +- src/Api/Models/Response/PlanResponseModel.cs | 11 +- .../Controllers/ServiceAccountsController.cs | 10 +- .../PaymentSucceededHandler.cs | 24 +- .../Implementations/ProviderEventService.cs | 17 +- .../SubscriptionUpdatedHandler.cs | 43 ++-- .../Implementations/UpcomingInvoiceHandler.cs | 26 +-- .../UpdateOrganizationUserCommand.cs | 12 +- .../CloudOrganizationSignUpCommand.cs | 6 +- .../Implementations/OrganizationService.cs | 46 ++-- src/Core/Billing/Constants/StripeConstants.cs | 1 + .../Billing/Extensions/BillingExtensions.cs | 11 + .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../Implementations/OrganizationMigrator.cs | 9 +- .../Billing/Models/ConfiguredProviderPlan.cs | 19 +- .../Billing/Models/OrganizationMetadata.cs | 15 +- .../Billing/Models/Sales/OrganizationSale.cs | 4 +- .../Billing/Models/Sales/SubscriptionSetup.cs | 4 +- src/Core/Billing/Pricing/IPricingClient.cs | 26 +++ .../JSON/FreeOrScalableDTOJsonConverter.cs | 35 +++ .../JSON/PurchasableDTOJsonConverter.cs | 40 ++++ .../Pricing/JSON/TypeReadingJsonConverter.cs | 28 +++ src/Core/Billing/Pricing/Models/FeatureDTO.cs | 9 + src/Core/Billing/Pricing/Models/PlanDTO.cs | 27 +++ .../Billing/Pricing/Models/PurchasableDTO.cs | 73 ++++++ src/Core/Billing/Pricing/PlanAdapter.cs | 153 ++++++------ src/Core/Billing/Pricing/PricingClient.cs | 88 +++++-- .../Pricing/Protos/password-manager.proto | 92 -------- .../Pricing/ServiceCollectionExtensions.cs | 21 ++ .../OrganizationBillingService.cs | 88 +++---- src/Core/Core.csproj | 12 +- .../Business/CompleteSubscriptionUpdate.cs | 23 +- .../Business/ProviderSubscriptionUpdate.cs | 7 +- .../SecretsManagerSubscriptionUpdate.cs | 12 +- src/Core/Models/Business/SubscriptionInfo.cs | 1 - .../Cloud/CloudSyncSponsorshipsCommand.cs | 4 +- .../Cloud/SetUpSponsorshipCommand.cs | 6 +- .../Cloud/ValidateSponsorshipCommand.cs | 7 +- .../CreateSponsorshipCommand.cs | 6 +- .../AddSecretsManagerSubscriptionCommand.cs | 17 +- .../UpgradeOrganizationPlanCommand.cs | 18 +- .../Implementations/StripePaymentService.cs | 22 +- src/Core/Utilities/StaticStore.cs | 6 +- .../OrganizationsControllerTests.cs | 6 +- .../OrganizationsControllerTests.cs | 6 +- .../ProviderBillingControllerTests.cs | 6 + .../ServiceAccountsControllerTests.cs | 4 + .../Services/ProviderEventServiceTests.cs | 24 +- .../CloudOrganizationSignUpCommandTests.cs | 20 +- .../Services/OrganizationServiceTests.cs | 18 +- .../OrganizationBillingServiceTests.cs | 47 +--- .../CompleteSubscriptionUpdateTests.cs | 12 +- .../SecretsManagerSubscriptionUpdateTests.cs | 55 +++-- ...dSecretsManagerSubscriptionCommandTests.cs | 10 +- ...eSecretsManagerSubscriptionCommandTests.cs | 217 +++++++++--------- .../UpgradeOrganizationPlanCommandTests.cs | 16 ++ 78 files changed, 1178 insertions(+), 712 deletions(-) create mode 100644 src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs create mode 100644 src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs create mode 100644 src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs create mode 100644 src/Core/Billing/Pricing/Models/FeatureDTO.cs create mode 100644 src/Core/Billing/Pricing/Models/PlanDTO.cs create mode 100644 src/Core/Billing/Pricing/Models/PurchasableDTO.cs delete mode 100644 src/Core/Billing/Pricing/Protos/password-manager.proto create mode 100644 src/Core/Billing/Pricing/ServiceCollectionExtensions.cs diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index ce0c0c9335..d2acdac079 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -5,12 +5,12 @@ using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Stripe; namespace Bit.Commercial.Core.AdminConsole.Providers; @@ -27,6 +27,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly IProviderBillingService _providerBillingService; private readonly ISubscriberService _subscriberService; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; + private readonly IPricingClient _pricingClient; public RemoveOrganizationFromProviderCommand( IEventService eventService, @@ -38,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv IFeatureService featureService, IProviderBillingService providerBillingService, ISubscriberService subscriberService, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IPricingClient pricingClient) { _eventService = eventService; _mailService = mailService; @@ -50,6 +52,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv _providerBillingService = providerBillingService; _subscriberService = subscriberService; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; + _pricingClient = pricingClient; } public async Task RemoveOrganizationFromProvider( @@ -110,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv Email = organization.BillingEmail }); - var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager; + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); var subscriptionCreateOptions = new SubscriptionCreateOptions { @@ -124,7 +127,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv }, OffSession = true, ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, - Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }] + Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] }; var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 864466ad45..799b57dc5a 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -50,6 +51,7 @@ public class ProviderService : IProviderService private readonly IDataProtectorTokenFactory _providerDeleteTokenDataFactory; private readonly IApplicationCacheService _applicationCacheService; private readonly IProviderBillingService _providerBillingService; + private readonly IPricingClient _pricingClient; public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, @@ -58,7 +60,7 @@ public class ProviderService : IProviderService IOrganizationRepository organizationRepository, GlobalSettings globalSettings, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, IDataProtectorTokenFactory providerDeleteTokenDataFactory, - IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService) + IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient) { _providerRepository = providerRepository; _providerUserRepository = providerUserRepository; @@ -77,6 +79,7 @@ public class ProviderService : IProviderService _providerDeleteTokenDataFactory = providerDeleteTokenDataFactory; _applicationCacheService = applicationCacheService; _providerBillingService = providerBillingService; + _pricingClient = pricingClient; } public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) @@ -452,30 +455,31 @@ public class ProviderService : IProviderService if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { - var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, - GetStripeSeatPlanId(organization.PlanType)); + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + + var subscriptionItem = await GetSubscriptionItemAsync( + organization.GatewaySubscriptionId, + plan.PasswordManager.StripeSeatPlanId); + var extractedPlanType = PlanTypeMappings(organization); + var extractedPlan = await _pricingClient.GetPlanOrThrow(extractedPlanType); + if (subscriptionItem != null) { - await UpdateSubscriptionAsync(subscriptionItem, GetStripeSeatPlanId(extractedPlanType), organization); + await UpdateSubscriptionAsync(subscriptionItem, extractedPlan.PasswordManager.StripeSeatPlanId, organization); } } await _organizationRepository.UpsertAsync(organization); } - private async Task GetSubscriptionItemAsync(string subscriptionId, string oldPlanId) + private async Task GetSubscriptionItemAsync(string subscriptionId, string oldPlanId) { var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId); return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId); } - private static string GetStripeSeatPlanId(PlanType planType) - { - return StaticStore.GetPlan(planType).PasswordManager.StripeSeatPlanId; - } - - private async Task UpdateSubscriptionAsync(Stripe.SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization) + private async Task UpdateSubscriptionAsync(SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization) { try { diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 7b10793283..b637cf37ef 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -10,6 +10,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; @@ -32,6 +33,7 @@ public class ProviderBillingService( ILogger logger, IOrganizationRepository organizationRepository, IPaymentService paymentService, + IPricingClient pricingClient, IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, @@ -77,8 +79,7 @@ public class ProviderBillingService( var managedPlanType = await GetManagedPlanTypeAsync(provider, organization); - // TODO: Replace with PricingClient - var plan = StaticStore.GetPlan(managedPlanType); + var plan = await pricingClient.GetPlanOrThrow(managedPlanType); organization.Plan = plan.Name; organization.PlanType = plan.Type; organization.MaxCollections = plan.PasswordManager.MaxCollections; @@ -154,7 +155,8 @@ public class ProviderBillingService( return; } - var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType); + var oldPlanConfiguration = await pricingClient.GetPlanOrThrow(plan.PlanType); + var newPlanConfiguration = await pricingClient.GetPlanOrThrow(command.NewPlan); plan.PlanType = command.NewPlan; await providerPlanRepository.ReplaceAsync(plan); @@ -178,7 +180,7 @@ public class ProviderBillingService( [ new SubscriptionItemOptions { - Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId, + Price = newPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId, Quantity = oldSubscriptionItem!.Quantity }, new SubscriptionItemOptions @@ -204,7 +206,7 @@ public class ProviderBillingService( throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); } organization.PlanType = command.NewPlan; - organization.Plan = StaticStore.GetPlan(command.NewPlan).Name; + organization.Plan = newPlanConfiguration.Name; await organizationRepository.ReplaceAsync(organization); } } @@ -347,7 +349,7 @@ public class ProviderBillingService( { var (organization, _) = pair; - var planName = DerivePlanName(provider, organization); + var planName = await DerivePlanName(provider, organization); var addable = new AddableOrganization( organization.Id, @@ -368,7 +370,7 @@ public class ProviderBillingService( return addable with { Disabled = requiresPurchase }; })); - string DerivePlanName(Provider localProvider, Organization localOrganization) + async Task DerivePlanName(Provider localProvider, Organization localOrganization) { if (localProvider.Type == ProviderType.Msp) { @@ -380,8 +382,7 @@ public class ProviderBillingService( }; } - // TODO: Replace with PricingClient - var plan = StaticStore.GetPlan(localOrganization.PlanType); + var plan = await pricingClient.GetPlanOrThrow(localOrganization.PlanType); return plan.Name; } } @@ -568,7 +569,7 @@ public class ProviderBillingService( foreach (var providerPlan in providerPlans) { - var plan = StaticStore.GetPlan(providerPlan.PlanType); + var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); if (!providerPlan.IsConfigured()) { @@ -652,8 +653,10 @@ public class ProviderBillingService( if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum) { - var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager - .StripeProviderPortalSeatPlanId; + var newPlan = await pricingClient.GetPlanOrThrow(newPlanConfiguration.Plan); + + var priceId = newPlan.PasswordManager.StripeProviderPortalSeatPlanId; + var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId); if (providerPlan.PurchasedSeats == 0) @@ -717,7 +720,7 @@ public class ProviderBillingService( ProviderPlan providerPlan, int newlyAssignedSeats) => async (currentlySubscribedSeats, newlySubscribedSeats) => { - var plan = StaticStore.GetPlan(providerPlan.PlanType); + var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); await paymentService.AdjustSeats( provider, @@ -741,7 +744,7 @@ public class ProviderBillingService( var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id); - var plan = StaticStore.GetPlan(planType); + var plan = await pricingClient.GetPlanOrThrow(planType); return providerOrganizations .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs index ee7bc398fe..d9a7d4a2ce 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs @@ -28,6 +28,7 @@ public class MaxProjectsQuery : IMaxProjectsQuery throw new NotFoundException(); } + // TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122 var plan = StaticStore.GetPlan(org.PlanType); if (plan?.SecretsManager == null) { diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index f45ab75046..2debd521a5 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -205,6 +206,8 @@ public class RemoveOrganizationFromProviderCommandTests var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, [], diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index 2883c9d7e3..d2d82f47de 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -550,8 +551,14 @@ public class ProviderServiceTests organization.PlanType = PlanType.EnterpriseMonthly; organization.Plan = "Enterprise (Monthly)"; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + var expectedPlanType = PlanType.EnterpriseMonthly2020; + sutProvider.GetDependency().GetPlanOrThrow(expectedPlanType) + .Returns(StaticStore.GetPlan(expectedPlanType)); + var expectedPlanId = "2020-enterprise-org-seat-monthly"; sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 3739603a2d..2fbd09a213 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; @@ -128,6 +129,9 @@ public class ProviderBillingServiceTests .GetByIdAsync(Arg.Is(p => p == providerPlanId)) .Returns(existingPlan); + sutProvider.GetDependency().GetPlanOrThrow(existingPlan.PlanType) + .Returns(StaticStore.GetPlan(existingPlan.PlanType)); + var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.ProviderSubscriptionGetAsync( Arg.Is(provider.GatewaySubscriptionId), @@ -156,6 +160,9 @@ public class ProviderBillingServiceTests var command = new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId); + sutProvider.GetDependency().GetPlanOrThrow(command.NewPlan) + .Returns(StaticStore.GetPlan(command.NewPlan)); + // Act await sutProvider.Sut.ChangePlan(command); @@ -390,6 +397,12 @@ public class ProviderBillingServiceTests } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 50 seats currently assigned with a seat minimum of 100 @@ -451,6 +464,12 @@ public class ProviderBillingServiceTests } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); @@ -515,6 +534,12 @@ public class ProviderBillingServiceTests } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); @@ -579,6 +604,12 @@ public class ProviderBillingServiceTests } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); @@ -636,6 +667,8 @@ public class ProviderBillingServiceTests } ]); + sutProvider.GetDependency().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType)); + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ new ProviderOrganizationOrganizationDetails @@ -672,6 +705,8 @@ public class ProviderBillingServiceTests } ]); + sutProvider.GetDependency().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType)); + sutProvider.GetDependency().GetManyDetailsByProviderAsync(provider.Id).Returns( [ new ProviderOrganizationOrganizationDetails @@ -856,6 +891,9 @@ public class ProviderBillingServiceTests sutProvider.GetDependency().GetByProviderId(provider.Id) .Returns(providerPlans); + sutProvider.GetDependency().GetPlanOrThrow(PlanType.EnterpriseMonthly) + .Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly)); + await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await sutProvider.GetDependency() @@ -881,6 +919,9 @@ public class ProviderBillingServiceTests sutProvider.GetDependency().GetByProviderId(provider.Id) .Returns(providerPlans); + sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly) + .Returns(StaticStore.GetPlan(PlanType.TeamsMonthly)); + await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider)); await sutProvider.GetDependency() @@ -923,6 +964,12 @@ public class ProviderBillingServiceTests } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + sutProvider.GetDependency().GetByProviderId(provider.Id) .Returns(providerPlans); @@ -968,6 +1015,12 @@ public class ProviderBillingServiceTests } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + sutProvider.GetDependency().GetByProviderId(provider.Id) .Returns(providerPlans); @@ -1066,6 +1119,12 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( @@ -1139,6 +1198,12 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( @@ -1212,6 +1277,12 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( @@ -1279,6 +1350,12 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( @@ -1352,6 +1429,12 @@ public class ProviderBillingServiceTests new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 } }; + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans); var command = new UpdateProviderSeatMinimumsCommand( diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 60a5a39612..fdb4961d9b 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; @@ -56,8 +57,8 @@ public class OrganizationsController : Controller private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IProviderBillingService _providerBillingService; - private readonly IFeatureService _featureService; private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand; + private readonly IPricingClient _pricingClient; public OrganizationsController( IOrganizationService organizationService, @@ -84,8 +85,8 @@ public class OrganizationsController : Controller IProviderOrganizationRepository providerOrganizationRepository, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IProviderBillingService providerBillingService, - IFeatureService featureService, - IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand) + IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand, + IPricingClient pricingClient) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -111,8 +112,8 @@ public class OrganizationsController : Controller _providerOrganizationRepository = providerOrganizationRepository; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _providerBillingService = providerBillingService; - _featureService = featureService; _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand; + _pricingClient = pricingClient; } [RequirePermission(Permission.Org_List_View)] @@ -212,6 +213,8 @@ public class OrganizationsController : Controller ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) : -1; + var plans = await _pricingClient.ListPlans(); + return View(new OrganizationEditModel( organization, provider, @@ -224,6 +227,7 @@ public class OrganizationsController : Controller billingHistoryInfo, billingSyncConnection, _globalSettings, + plans, secrets, projects, serviceAccounts, @@ -253,8 +257,9 @@ public class OrganizationsController : Controller UpdateOrganization(organization, model); - if (organization.UseSecretsManager && - !StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager) + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + + if (organization.UseSecretsManager && !plan.SupportsSecretsManager) { TempData["Error"] = "Plan does not support Secrets Manager"; return RedirectToAction("Edit", new { id }); diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index be191ddb8d..729b4f7990 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Models.StaticStore; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; @@ -17,6 +18,8 @@ namespace Bit.Admin.AdminConsole.Models; public class OrganizationEditModel : OrganizationViewModel { + private readonly List _plans; + public OrganizationEditModel() { } public OrganizationEditModel(Provider provider) @@ -40,6 +43,7 @@ public class OrganizationEditModel : OrganizationViewModel BillingHistoryInfo billingHistoryInfo, IEnumerable connections, GlobalSettings globalSettings, + List plans, int secrets, int projects, int serviceAccounts, @@ -96,6 +100,8 @@ public class OrganizationEditModel : OrganizationViewModel MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; SmServiceAccounts = org.SmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; + + _plans = plans; } public BillingInfo BillingInfo { get; set; } @@ -183,7 +189,7 @@ public class OrganizationEditModel : OrganizationViewModel * Add mappings for individual properties as you need them */ public object GetPlansHelper() => - StaticStore.Plans + _plans .Select(p => { var plan = new diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 265aefc4ca..5a73e57204 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -55,6 +56,7 @@ public class OrganizationUsersController : Controller private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand; private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; private readonly IFeatureService _featureService; + private readonly IPricingClient _pricingClient; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -77,7 +79,8 @@ public class OrganizationUsersController : Controller IRemoveOrganizationUserCommand removeOrganizationUserCommand, IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand, IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, - IFeatureService featureService) + IFeatureService featureService, + IPricingClient pricingClient) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -100,6 +103,7 @@ public class OrganizationUsersController : Controller _deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand; _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; _featureService = featureService; + _pricingClient = pricingClient; } [HttpGet("{id}")] @@ -648,7 +652,9 @@ public class OrganizationUsersController : Controller if (additionalSmSeatsRequired > 0) { var organization = await _organizationRepository.GetByIdAsync(orgId); - var update = new SecretsManagerSubscriptionUpdate(organization, true) + // TODO: https://bitwarden.atlassian.net/browse/PM-17000 + var plan = await _pricingClient.GetPlanOrThrow(organization!.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, true) .AdjustSeats(additionalSmSeatsRequired); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 85e8e990a6..34da3de10c 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; @@ -60,6 +61,7 @@ public class OrganizationsController : Controller private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand; + private readonly IPricingClient _pricingClient; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -81,7 +83,8 @@ public class OrganizationsController : Controller IDataProtectorTokenFactory orgDeleteTokenDataFactory, IRemoveOrganizationUserCommand removeOrganizationUserCommand, ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, - IOrganizationDeleteCommand organizationDeleteCommand) + IOrganizationDeleteCommand organizationDeleteCommand, + IPricingClient pricingClient) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -103,6 +106,7 @@ public class OrganizationsController : Controller _removeOrganizationUserCommand = removeOrganizationUserCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _organizationDeleteCommand = organizationDeleteCommand; + _pricingClient = pricingClient; } [HttpGet("{id}")] @@ -120,7 +124,8 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - return new OrganizationResponseModel(organization); + var plan = await _pricingClient.GetPlan(organization.PlanType); + return new OrganizationResponseModel(organization, plan); } [HttpGet("")] @@ -181,7 +186,8 @@ public class OrganizationsController : Controller var organizationSignup = model.ToOrganizationSignup(user); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); - return new OrganizationResponseModel(result.Organization); + var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType); + return new OrganizationResponseModel(result.Organization, plan); } [HttpPost("create-without-payment")] @@ -196,7 +202,8 @@ public class OrganizationsController : Controller var organizationSignup = model.ToOrganizationSignup(user); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); - return new OrganizationResponseModel(result.Organization); + var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType); + return new OrganizationResponseModel(result.Organization, plan); } [HttpPut("{id}")] @@ -224,7 +231,8 @@ public class OrganizationsController : Controller } await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling); - return new OrganizationResponseModel(organization); + var plan = await _pricingClient.GetPlan(organization.PlanType); + return new OrganizationResponseModel(organization, plan); } [HttpPost("{id}/storage")] @@ -358,8 +366,8 @@ public class OrganizationsController : Controller if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim) { // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types - var plan = StaticStore.GetPlan(organization.PlanType); - if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams) + var productTier = organization.PlanType.GetProductTier(); + if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams) { throw new NotFoundException(); } @@ -542,7 +550,8 @@ public class OrganizationsController : Controller } await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated); - return new OrganizationResponseModel(organization); + var plan = await _pricingClient.GetPlan(organization.PlanType); + return new OrganizationResponseModel(organization, plan); } [HttpGet("{id}/plan-type")] diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 272aaf6f9c..4dc4a4ec55 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.Utilities; using Constants = Bit.Core.Constants; @@ -11,8 +12,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations; public class OrganizationResponseModel : ResponseModel { - public OrganizationResponseModel(Organization organization, string obj = "organization") - : base(obj) + public OrganizationResponseModel( + Organization organization, + Plan plan, + string obj = "organization") : base(obj) { if (organization == null) { @@ -28,7 +31,8 @@ public class OrganizationResponseModel : ResponseModel BusinessCountry = organization.BusinessCountry; BusinessTaxNumber = organization.BusinessTaxNumber; BillingEmail = organization.BillingEmail; - Plan = new PlanResponseModel(StaticStore.GetPlan(organization.PlanType)); + // Self-Host instances only require plan information that can be derived from the Organization record. + Plan = plan != null ? new PlanResponseModel(plan) : new PlanResponseModel(organization); PlanType = organization.PlanType; Seats = organization.Seats; MaxAutoscaleSeats = organization.MaxAutoscaleSeats; @@ -110,7 +114,9 @@ public class OrganizationResponseModel : ResponseModel public class OrganizationSubscriptionResponseModel : OrganizationResponseModel { - public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription") + public OrganizationSubscriptionResponseModel( + Organization organization, + Plan plan) : base(organization, plan, "organizationSubscription") { Expiration = organization.ExpirationDate; StorageName = organization.Storage.HasValue ? @@ -119,8 +125,11 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB } - public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData) - : this(organization) + public OrganizationSubscriptionResponseModel( + Organization organization, + SubscriptionInfo subscription, + Plan plan, + bool hideSensitiveData) : this(organization, plan) { Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null; UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null; @@ -142,7 +151,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel } public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) : - this(organization) + this(organization, (Plan)null) { if (license != null) { diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index d08298de6e..3a901f11c4 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Data; @@ -37,7 +38,7 @@ public class ProfileOrganizationResponseModel : ResponseModel UsePasswordManager = organization.UsePasswordManager; UsersGetPremium = organization.UsersGetPremium; UseCustomPermissions = organization.UseCustomPermissions; - UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise; + UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise; SelfHost = organization.SelfHost; Seats = organization.Seats; MaxCollections = organization.MaxCollections; @@ -60,7 +61,7 @@ public class ProfileOrganizationResponseModel : ResponseModel FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); - ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; + ProductTierType = organization.PlanType.GetProductTier(); FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 589744c7df..d31cb5a77a 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Enums; using Bit.Core.Models.Data; -using Bit.Core.Utilities; namespace Bit.Api.AdminConsole.Models.Response; @@ -26,7 +26,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo UseResetPassword = organization.UseResetPassword; UsersGetPremium = organization.UsersGetPremium; UseCustomPermissions = organization.UseCustomPermissions; - UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise; + UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise; SelfHost = organization.SelfHost; Seats = organization.Seats; MaxCollections = organization.MaxCollections; @@ -44,7 +44,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo ProviderId = organization.ProviderId; ProviderName = organization.ProviderName; ProviderType = organization.ProviderType; - ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; + ProductTierType = organization.PlanType.GetProductTier(); LimitCollectionCreation = organization.LimitCollectionCreation; LimitCollectionDeletion = organization.LimitCollectionDeletion; LimitItemDeletion = organization.LimitItemDeletion; diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 3ceeaf3c47..2ec503281e 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -4,6 +4,7 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Repositories; @@ -21,6 +22,7 @@ public class OrganizationBillingController( IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, IPaymentService paymentService, + IPricingClient pricingClient, ISubscriberService subscriberService, IPaymentHistoryService paymentHistoryService, IUserService userService) : BaseBillingController @@ -279,7 +281,7 @@ public class OrganizationBillingController( } var organizationSignup = model.ToOrganizationSignup(user); var sale = OrganizationSale.From(organization, organizationSignup); - var plan = StaticStore.GetPlan(model.PlanType); + var plan = await pricingClient.GetPlanOrThrow(model.PlanType); sale.Organization.PlanType = plan.Type; sale.Organization.Plan = plan.Name; sale.SubscriptionSetup.SkipTrial = true; diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 7b25114a44..de14a8d798 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -45,7 +46,8 @@ public class OrganizationsController( IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand, IReferenceEventService referenceEventService, ISubscriberService subscriberService, - IOrganizationInstallationRepository organizationInstallationRepository) + IOrganizationInstallationRepository organizationInstallationRepository, + IPricingClient pricingClient) : Controller { [HttpGet("{id:guid}/subscription")] @@ -62,26 +64,28 @@ public class OrganizationsController( throw new NotFoundException(); } - if (!globalSettings.SelfHosted && organization.Gateway != null) - { - var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization); - if (subscriptionInfo == null) - { - throw new NotFoundException(); - } - - var hideSensitiveData = !await currentContext.EditSubscription(id); - - return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData); - } - if (globalSettings.SelfHosted) { var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization); return new OrganizationSubscriptionResponseModel(organization, orgLicense); } - return new OrganizationSubscriptionResponseModel(organization); + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); + + if (string.IsNullOrEmpty(organization.GatewaySubscriptionId)) + { + return new OrganizationSubscriptionResponseModel(organization, plan); + } + + var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization); + if (subscriptionInfo == null) + { + throw new NotFoundException(); + } + + var hideSensitiveData = !await currentContext.EditSubscription(id); + + return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, plan, hideSensitiveData); } [HttpGet("{id:guid}/license")] @@ -165,7 +169,8 @@ public class OrganizationsController( organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model); - var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization); + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); + var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, plan); await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate); diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index c5de63c69b..73c992040c 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -2,6 +2,7 @@ using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers; public class ProviderBillingController( ICurrentContext currentContext, ILogger logger, + IPricingClient pricingClient, IProviderBillingService providerBillingService, IProviderPlanRepository providerPlanRepository, IProviderRepository providerRepository, @@ -84,13 +86,25 @@ public class ProviderBillingController( var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan => + { + var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); + return new ConfiguredProviderPlan( + providerPlan.Id, + providerPlan.ProviderId, + plan, + providerPlan.SeatMinimum ?? 0, + providerPlan.PurchasedSeats ?? 0, + providerPlan.AllocatedSeats ?? 0); + })); + var taxInformation = GetTaxInformation(subscription.Customer); var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription); var response = ProviderSubscriptionResponse.From( subscription, - providerPlans, + configuredProviderPlans, taxInformation, subscriptionSuspension, provider); diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index 2b0592f0e3..34c3817e51 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -1,9 +1,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; -using Bit.Core.Utilities; using Stripe; namespace Bit.Api.Billing.Models.Responses; @@ -25,26 +23,24 @@ public record ProviderSubscriptionResponse( public static ProviderSubscriptionResponse From( Subscription subscription, - ICollection providerPlans, + ICollection providerPlans, TaxInformation taxInformation, SubscriptionSuspension subscriptionSuspension, Provider provider) { var providerPlanResponses = providerPlans - .Where(providerPlan => providerPlan.IsConfigured()) - .Select(ConfiguredProviderPlan.From) - .Select(configuredProviderPlan => + .Select(providerPlan => { - var plan = StaticStore.GetPlan(configuredProviderPlan.PlanType); - var cost = (configuredProviderPlan.SeatMinimum + configuredProviderPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; + var plan = providerPlan.Plan; + var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; return new ProviderPlanResponse( plan.Name, plan.Type, plan.ProductTier, - configuredProviderPlan.SeatMinimum, - configuredProviderPlan.PurchasedSeats, - configuredProviderPlan.AssignedSeats, + providerPlan.SeatMinimum, + providerPlan.PurchasedSeats, + providerPlan.AssignedSeats, cost, cadence); }); diff --git a/src/Api/Billing/Public/Controllers/OrganizationController.cs b/src/Api/Billing/Public/Controllers/OrganizationController.cs index 7fcd94acd3..b0a0537ed8 100644 --- a/src/Api/Billing/Public/Controllers/OrganizationController.cs +++ b/src/Api/Billing/Public/Controllers/OrganizationController.cs @@ -1,6 +1,7 @@ using System.Net; using Bit.Api.Billing.Public.Models; using Bit.Api.Models.Public.Response; +using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; @@ -21,19 +22,22 @@ public class OrganizationController : Controller private readonly IOrganizationRepository _organizationRepository; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly ILogger _logger; + private readonly IPricingClient _pricingClient; public OrganizationController( IOrganizationService organizationService, ICurrentContext currentContext, IOrganizationRepository organizationRepository, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, - ILogger logger) + ILogger logger, + IPricingClient pricingClient) { _organizationService = organizationService; _currentContext = currentContext; _organizationRepository = organizationRepository; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _logger = logger; + _pricingClient = pricingClient; } /// @@ -140,7 +144,8 @@ public class OrganizationController : Controller return "Organization has no access to Secrets Manager."; } - var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization); + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization, plan); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate); return string.Empty; diff --git a/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs b/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs index 781ad3ca53..5c75db5924 100644 --- a/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs +++ b/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; namespace Bit.Api.Billing.Public.Models; @@ -93,17 +94,17 @@ public class SecretsManagerSubscriptionUpdateModel set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; } } - public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) + public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan) { - var update = UpdateUpdateMaxAutoScale(organization); + var update = UpdateUpdateMaxAutoScale(organization, plan); UpdateSeats(organization, update); UpdateServiceAccounts(organization, update); return update; } - private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization) + private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization, Plan plan) { - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats, MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index c2ee494322..11b070fb66 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -1,5 +1,5 @@ using Bit.Api.Models.Response; -using Bit.Core.Utilities; +using Bit.Core.Billing.Pricing; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -7,13 +7,15 @@ namespace Bit.Api.Controllers; [Route("plans")] [Authorize("Web")] -public class PlansController : Controller +public class PlansController( + IPricingClient pricingClient) : Controller { [HttpGet("")] [AllowAnonymous] - public ListResponseModel Get() + public async Task> Get() { - var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan)); + var plans = await pricingClient.ListPlans(); + var responses = plans.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } } diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs index 783c4b71f4..ed501c41da 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -64,7 +64,8 @@ public class SelfHostedOrganizationLicensesController : Controller var result = await _organizationService.SignUpAsync(license, user, model.Key, model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); - return new OrganizationResponseModel(result.Item1); + + return new OrganizationResponseModel(result.Item1, null); } [HttpPost("{id}")] diff --git a/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs b/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs index 18bc66a0b6..6ddc1af486 100644 --- a/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; namespace Bit.Api.Models.Request.Organizations; @@ -12,9 +13,9 @@ public class SecretsManagerSubscriptionUpdateRequestModel public int ServiceAccountAdjustment { get; set; } public int? MaxAutoscaleServiceAccounts { get; set; } - public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) + public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan) { - return new SecretsManagerSubscriptionUpdate(organization, false) + return new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmSeats = MaxAutoscaleSeats, MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts diff --git a/src/Api/Models/Response/PlanResponseModel.cs b/src/Api/Models/Response/PlanResponseModel.cs index b6ca9b62d2..74bcb59661 100644 --- a/src/Api/Models/Response/PlanResponseModel.cs +++ b/src/Api/Models/Response/PlanResponseModel.cs @@ -1,4 +1,6 @@ -using Bit.Core.Billing.Enums; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Models.Api; using Bit.Core.Models.StaticStore; @@ -44,6 +46,13 @@ public class PlanResponseModel : ResponseModel PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager); } + public PlanResponseModel(Organization organization, string obj = "plan") : base(obj) + { + Type = organization.PlanType; + ProductTier = organization.PlanType.GetProductTier(); + Name = organization.Plan; + } + public PlanType Type { get; set; } public ProductTierType ProductTier { get; set; } public string Name { get; set; } diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index 8de53bc1e4..96c6c60528 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -1,6 +1,7 @@ using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -37,6 +38,7 @@ public class ServiceAccountsController : Controller private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand; private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand; + private readonly IPricingClient _pricingClient; public ServiceAccountsController( ICurrentContext currentContext, @@ -52,7 +54,8 @@ public class ServiceAccountsController : Controller ICreateServiceAccountCommand createServiceAccountCommand, IUpdateServiceAccountCommand updateServiceAccountCommand, IDeleteServiceAccountsCommand deleteServiceAccountsCommand, - IRevokeAccessTokensCommand revokeAccessTokensCommand) + IRevokeAccessTokensCommand revokeAccessTokensCommand, + IPricingClient pricingClient) { _currentContext = currentContext; _userService = userService; @@ -66,6 +69,7 @@ public class ServiceAccountsController : Controller _updateServiceAccountCommand = updateServiceAccountCommand; _deleteServiceAccountsCommand = deleteServiceAccountsCommand; _revokeAccessTokensCommand = revokeAccessTokensCommand; + _pricingClient = pricingClient; _createAccessTokenCommand = createAccessTokenCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; } @@ -124,7 +128,9 @@ public class ServiceAccountsController : Controller if (newServiceAccountSlotsRequired > 0) { var org = await _organizationRepository.GetByIdAsync(organizationId); - var update = new SecretsManagerSubscriptionUpdate(org, true) + // TODO: https://bitwarden.atlassian.net/browse/PM-17002 + var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType); + var update = new SecretsManagerSubscriptionUpdate(org, plan, true) .AdjustServiceAccounts(newServiceAccountSlotsRequired); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); } diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index 1577e77c9e..40d8c8349d 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -9,7 +10,6 @@ using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; -using Bit.Core.Utilities; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -28,6 +28,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationEnableCommand _organizationEnableCommand; + private readonly IPricingClient _pricingClient; public PaymentSucceededHandler( ILogger logger, @@ -41,7 +42,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler IStripeEventUtilityService stripeEventUtilityService, IUserService userService, IPushNotificationService pushNotificationService, - IOrganizationEnableCommand organizationEnableCommand) + IOrganizationEnableCommand organizationEnableCommand, + IPricingClient pricingClient) { _logger = logger; _stripeEventService = stripeEventService; @@ -55,6 +57,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _userService = userService; _pushNotificationService = pushNotificationService; _organizationEnableCommand = organizationEnableCommand; + _pricingClient = pricingClient; } /// @@ -96,9 +99,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler return; } - var teamsMonthly = StaticStore.GetPlan(PlanType.TeamsMonthly); + var teamsMonthly = await _pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly); - var enterpriseMonthly = StaticStore.GetPlan(PlanType.EnterpriseMonthly); + var enterpriseMonthly = await _pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly); var teamsMonthlyLineItem = subscription.Items.Data.FirstOrDefault(item => @@ -137,14 +140,21 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler } else if (organizationId.HasValue) { - if (!subscription.Items.Any(i => - StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id))) + var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); + + if (organization == null) + { + return; + } + + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + + if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id)) { return; } await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); - var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); await _referenceEventService.RaiseEventAsync( diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 548ed9f547..4e35a6c894 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,15 +1,17 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; -using Bit.Core.Utilities; +using Bit.Core.Repositories; using Stripe; namespace Bit.Billing.Services.Implementations; public class ProviderEventService( - ILogger logger, + IOrganizationRepository organizationRepository, + IPricingClient pricingClient, IProviderInvoiceItemRepository providerInvoiceItemRepository, IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, @@ -54,7 +56,14 @@ public class ProviderEventService( continue; } - var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type)); + var organization = await organizationRepository.GetByIdAsync(client.OrganizationId); + + if (organization == null) + { + return; + } + + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; @@ -76,7 +85,7 @@ public class ProviderEventService( foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0)) { - var plan = StaticStore.GetPlan(providerPlan.PlanType); + var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); var clientSeats = invoiceItems .Where(item => item.PlanName == plan.Name) diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 35a16ae74f..d2ca7fa9bf 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -2,11 +2,11 @@ using Bit.Billing.Jobs; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Quartz; using Stripe; using Event = Stripe.Event; @@ -27,6 +27,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IFeatureService _featureService; private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; + private readonly IPricingClient _pricingClient; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -40,7 +41,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler ISchedulerFactory schedulerFactory, IFeatureService featureService, IOrganizationEnableCommand organizationEnableCommand, - IOrganizationDisableCommand organizationDisableCommand) + IOrganizationDisableCommand organizationDisableCommand, + IPricingClient pricingClient) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -54,6 +56,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _featureService = featureService; _organizationEnableCommand = organizationEnableCommand; _organizationDisableCommand = organizationDisableCommand; + _pricingClient = pricingClient; } /// @@ -156,7 +159,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler /// /// /// - private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(Event parsedEvent, + private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync( + Event parsedEvent, Subscription subscription) { if (parsedEvent.Data.PreviousAttributes?.items is null) @@ -164,6 +168,22 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler return; } + var organization = subscription.Metadata.TryGetValue("organizationId", out var organizationId) + ? await _organizationRepository.GetByIdAsync(Guid.Parse(organizationId)) + : null; + + if (organization == null) + { + return; + } + + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + + if (!plan.SupportsSecretsManager) + { + return; + } + var previousSubscription = parsedEvent.Data .PreviousAttributes .ToObject() as Subscription; @@ -171,17 +191,14 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler // This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager. // If there are changes to any subscription item, Stripe sends every item in the subscription, both // changed and unchanged. - var previousSubscriptionHasSecretsManager = previousSubscription?.Items is not null && - previousSubscription.Items.Any(previousItem => - StaticStore.Plans.Any(p => - p.SecretsManager is not null && - p.SecretsManager.StripeSeatPlanId == - previousItem.Plan.Id)); + var previousSubscriptionHasSecretsManager = + previousSubscription?.Items is not null && + previousSubscription.Items.Any( + previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId); - var currentSubscriptionHasSecretsManager = subscription.Items.Any(i => - StaticStore.Plans.Any(p => - p.SecretsManager is not null && - p.SecretsManager.StripeSeatPlanId == i.Plan.Id)); + var currentSubscriptionHasSecretsManager = + subscription.Items.Any( + currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId); if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager) { diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 5315195c59..d37bf41428 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,12 +1,11 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Stripe; using Event = Stripe.Event; @@ -16,6 +15,7 @@ public class UpcomingInvoiceHandler( ILogger logger, IMailService mailService, IOrganizationRepository organizationRepository, + IPricingClient pricingClient, IProviderRepository providerRepository, IStripeFacade stripeFacade, IStripeEventService stripeEventService, @@ -52,7 +52,9 @@ public class UpcomingInvoiceHandler( await TryEnableAutomaticTaxAsync(subscription); - if (!HasAnnualPlan(organization)) + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); + + if (!plan.IsAnnual) { return; } @@ -136,7 +138,7 @@ public class UpcomingInvoiceHandler( { if (subscription.AutomaticTax.Enabled || !subscription.Customer.HasBillingLocation() || - IsNonTaxableNonUSBusinessUseSubscription(subscription)) + await IsNonTaxableNonUSBusinessUseSubscription(subscription)) { return; } @@ -150,14 +152,12 @@ public class UpcomingInvoiceHandler( return; - bool IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) + async Task IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) { - var familyPriceIds = new List - { - // TODO: Replace with the PricingClient - StaticStore.GetPlan(PlanType.FamiliesAnnually2019).PasswordManager.StripePlanId, - StaticStore.GetPlan(PlanType.FamiliesAnnually).PasswordManager.StripePlanId - }; + var familyPriceIds = (await Task.WhenAll( + pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), + pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) + .Select(plan => plan.PasswordManager.StripePlanId); return localSubscription.Customer.Address.Country != "US" && localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) && @@ -165,6 +165,4 @@ public class UpcomingInvoiceHandler( !localSubscription.Customer.TaxIds.Any(); } } - - private static bool HasAnnualPlan(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index 3dd55f9893..657cc6c54d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -24,6 +25,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand private readonly ICollectionRepository _collectionRepository; private readonly IGroupRepository _groupRepository; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; + private readonly IPricingClient _pricingClient; public UpdateOrganizationUserCommand( IEventService eventService, @@ -34,7 +36,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, ICollectionRepository collectionRepository, IGroupRepository groupRepository, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IPricingClient pricingClient) { _eventService = eventService; _organizationService = organizationService; @@ -45,6 +48,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand _collectionRepository = collectionRepository; _groupRepository = groupRepository; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; + _pricingClient = pricingClient; } /// @@ -128,8 +132,10 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1); if (additionalSmSeatsRequired > 0) { - var update = new SecretsManagerSubscriptionUpdate(organization, true) - .AdjustSeats(additionalSmSeatsRequired); + // TODO: https://bitwarden.atlassian.net/browse/PM-17012 + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, true) + .AdjustSeats(additionalSmSeatsRequired); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 94ff3c0059..57cfd1e60f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -45,11 +46,12 @@ public class CloudOrganizationSignUpCommand( IPushRegistrationService pushRegistrationService, IPushNotificationService pushNotificationService, ICollectionRepository collectionRepository, - IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand + IDeviceRepository deviceRepository, + IPricingClient pricingClient) : ICloudOrganizationSignUpCommand { public async Task SignUpOrganizationAsync(OrganizationSignup signup) { - var plan = StaticStore.GetPlan(signup.Plan); + var plan = await pricingClient.GetPlanOrThrow(signup.Plan); ValidatePasswordManagerPlan(plan, signup); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 14cf89a246..1b44eea496 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -16,6 +16,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -74,6 +75,7 @@ public class OrganizationService : IOrganizationService private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IOrganizationBillingService _organizationBillingService; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; + private readonly IPricingClient _pricingClient; public OrganizationService( IOrganizationRepository organizationRepository, @@ -108,7 +110,8 @@ public class OrganizationService : IOrganizationService IFeatureService featureService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationBillingService organizationBillingService, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IPricingClient pricingClient) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -143,6 +146,7 @@ public class OrganizationService : IOrganizationService _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _organizationBillingService = organizationBillingService; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; + _pricingClient = pricingClient; } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -210,11 +214,7 @@ public class OrganizationService : IOrganizationService throw new NotFoundException(); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); - if (plan == null) - { - throw new BadRequestException("Existing plan not found."); - } + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); if (!plan.PasswordManager.HasAdditionalStorageOption) { @@ -268,7 +268,7 @@ public class OrganizationService : IOrganizationService throw new BadRequestException($"Cannot set max seat autoscaling below current seat count."); } - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); if (plan == null) { throw new BadRequestException("Existing plan not found."); @@ -320,11 +320,7 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("No subscription found."); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); - if (plan == null) - { - throw new BadRequestException("Existing plan not found."); - } + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); if (!plan.PasswordManager.HasAdditionalSeatsOption) { @@ -442,7 +438,7 @@ public class OrganizationService : IOrganizationService public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup) { - var plan = StaticStore.GetPlan(signup.Plan); + var plan = await _pricingClient.GetPlanOrThrow(signup.Plan); ValidatePlan(plan, signup.AdditionalSeats, "Password Manager"); @@ -530,17 +526,6 @@ public class OrganizationService : IOrganizationService throw new BadRequestException(exception); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType); - if (plan is null) - { - throw new BadRequestException($"Server must be updated to support {license.Plan}."); - } - - if (license.PlanType != PlanType.Custom && plan.Disabled) - { - throw new BadRequestException($"Plan {plan.Name} is disabled."); - } - var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync(); if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey))) { @@ -882,7 +867,8 @@ public class OrganizationService : IOrganizationService var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount); if (additionalSmSeatsRequired > 0) { - smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true) + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, plan, true) .AdjustSeats(additionalSmSeatsRequired); } @@ -1008,7 +994,8 @@ public class OrganizationService : IOrganizationService if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue && currentOrganization.SmSeats.Value != initialSmSeatCount.Value) { - var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, false) + var plan = await _pricingClient.GetPlanOrThrow(currentOrganization.PlanType); + var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, plan, false) { SmSeats = initialSmSeatCount.Value }; @@ -2237,13 +2224,6 @@ public class OrganizationService : IOrganizationService public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted) { - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); - - if (plan!.Disabled) - { - throw new BadRequestException("Plan not found."); - } - organization.Id = CoreHelpers.GenerateComb(); organization.Enabled = false; organization.Status = OrganizationStatusType.Pending; diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index e3c2b7245e..0a5faae947 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -34,6 +34,7 @@ public static class StripeConstants public static class InvoiceStatus { public const string Draft = "draft"; + public const string Open = "open"; } public static class MetadataKeys diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index 39b92e95a2..f6e65861cd 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -10,6 +10,17 @@ namespace Bit.Core.Billing.Extensions; public static class BillingExtensions { + public static ProductTierType GetProductTier(this PlanType planType) + => planType switch + { + PlanType.Custom or PlanType.Free => ProductTierType.Free, + PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2019 => ProductTierType.Families, + PlanType.TeamsStarter or PlanType.TeamsStarter2023 => ProductTierType.TeamsStarter, + _ when planType.ToString().Contains("Teams") => ProductTierType.Teams, + _ when planType.ToString().Contains("Enterprise") => ProductTierType.Enterprise, + _ => throw new BillingException($"PlanType {planType} could not be matched to a ProductTierType") + }; + public static bool IsBillable(this Provider provider) => provider is { diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 9a7a4107ae..26815d7df0 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches.Implementations; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; @@ -17,7 +18,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); - // services.AddSingleton(); services.AddLicenseServices(); + services.AddPricingClient(); } } diff --git a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs index a24193f133..4d93c0119a 100644 --- a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs +++ b/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs @@ -3,11 +3,11 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using Stripe; using Plan = Bit.Core.Models.StaticStore.Plan; @@ -19,6 +19,7 @@ public class OrganizationMigrator( ILogger logger, IMigrationTrackerCache migrationTrackerCache, IOrganizationRepository organizationRepository, + IPricingClient pricingClient, IStripeAdapter stripeAdapter) : IOrganizationMigrator { private const string _cancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; @@ -137,7 +138,7 @@ public class OrganizationMigrator( logger.LogInformation("CB: Bringing organization ({OrganizationID}) under provider management", organization.Id); - var plan = StaticStore.GetPlan(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly); + var plan = await pricingClient.GetPlanOrThrow(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly); ResetOrganizationPlan(organization, plan); organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; @@ -206,7 +207,7 @@ public class OrganizationMigrator( ? StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice; - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); var items = new List { @@ -279,7 +280,7 @@ public class OrganizationMigrator( throw new Exception(); } - var plan = StaticStore.GetPlan(migrationRecord.PlanType); + var plan = await pricingClient.GetPlanOrThrow(migrationRecord.PlanType); ResetOrganizationPlan(organization, plan); organization.MaxStorageGb = migrationRecord.MaxStorageGb; diff --git a/src/Core/Billing/Models/ConfiguredProviderPlan.cs b/src/Core/Billing/Models/ConfiguredProviderPlan.cs index dadb176533..72c1ec5b07 100644 --- a/src/Core/Billing/Models/ConfiguredProviderPlan.cs +++ b/src/Core/Billing/Models/ConfiguredProviderPlan.cs @@ -1,24 +1,11 @@ -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Enums; +using Bit.Core.Models.StaticStore; namespace Bit.Core.Billing.Models; public record ConfiguredProviderPlan( Guid Id, Guid ProviderId, - PlanType PlanType, + Plan Plan, int SeatMinimum, int PurchasedSeats, - int AssignedSeats) -{ - public static ConfiguredProviderPlan From(ProviderPlan providerPlan) => - providerPlan.IsConfigured() - ? new ConfiguredProviderPlan( - providerPlan.Id, - providerPlan.ProviderId, - providerPlan.PlanType, - providerPlan.SeatMinimum.GetValueOrDefault(0), - providerPlan.PurchasedSeats.GetValueOrDefault(0), - providerPlan.AllocatedSeats.GetValueOrDefault(0)) - : null; -} + int AssignedSeats); diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 4bb9a85825..41666949bf 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -10,4 +10,17 @@ public record OrganizationMetadata( bool IsSubscriptionCanceled, DateTime? InvoiceDueDate, DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate); + DateTime? SubPeriodEndDate) +{ + public static OrganizationMetadata Default => new OrganizationMetadata( + false, + false, + false, + false, + false, + false, + false, + null, + null, + null); +} diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 43852bb320..f0ad7894e6 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -76,8 +76,6 @@ public class OrganizationSale private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade) { - var plan = Core.Utilities.StaticStore.GetPlan(upgrade.Plan); - var passwordManagerOptions = new SubscriptionSetup.PasswordManager { Seats = upgrade.AdditionalSeats, @@ -95,7 +93,7 @@ public class OrganizationSale return new SubscriptionSetup { - Plan = plan, + PlanType = upgrade.Plan, PasswordManagerOptions = passwordManagerOptions, SecretsManagerOptions = secretsManagerOptions }; diff --git a/src/Core/Billing/Models/Sales/SubscriptionSetup.cs b/src/Core/Billing/Models/Sales/SubscriptionSetup.cs index 39a80a776a..871a2920b1 100644 --- a/src/Core/Billing/Models/Sales/SubscriptionSetup.cs +++ b/src/Core/Billing/Models/Sales/SubscriptionSetup.cs @@ -1,4 +1,4 @@ -using Bit.Core.Models.StaticStore; +using Bit.Core.Billing.Enums; namespace Bit.Core.Billing.Models.Sales; @@ -6,7 +6,7 @@ namespace Bit.Core.Billing.Models.Sales; public class SubscriptionSetup { - public required Plan Plan { get; set; } + public required PlanType PlanType { get; set; } public required PasswordManager PasswordManagerOptions { get; set; } public SecretsManager? SecretsManagerOptions { get; set; } public bool SkipTrial = false; diff --git a/src/Core/Billing/Pricing/IPricingClient.cs b/src/Core/Billing/Pricing/IPricingClient.cs index 68577f1db3..bc3f142dda 100644 --- a/src/Core/Billing/Pricing/IPricingClient.cs +++ b/src/Core/Billing/Pricing/IPricingClient.cs @@ -1,5 +1,7 @@ using Bit.Core.Billing.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.StaticStore; +using Bit.Core.Utilities; #nullable enable @@ -7,6 +9,30 @@ namespace Bit.Core.Billing.Pricing; public interface IPricingClient { + /// + /// Retrieve a Bitwarden plan by its . If the feature flag 'use-pricing-service' is enabled, + /// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing . + /// + /// The type of plan to retrieve. + /// A Bitwarden record or null in the case the plan could not be found or the method was executed from a self-hosted instance. + /// Thrown when the request to the Pricing Service fails unexpectedly. Task GetPlan(PlanType planType); + + /// + /// Retrieve a Bitwarden plan by its . If the feature flag 'use-pricing-service' is enabled, + /// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing . + /// + /// The type of plan to retrieve. + /// A Bitwarden record. + /// Thrown when the for the provided could not be found or the method was executed from a self-hosted instance. + /// Thrown when the request to the Pricing Service fails unexpectedly. + Task GetPlanOrThrow(PlanType planType); + + /// + /// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled, + /// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing . + /// + /// A list of Bitwarden records or an empty list in the case the method is executed from a self-hosted instance. + /// Thrown when the request to the Pricing Service fails unexpectedly. Task> ListPlans(); } diff --git a/src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs b/src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs new file mode 100644 index 0000000000..37a8a4234d --- /dev/null +++ b/src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using Bit.Core.Billing.Pricing.Models; + +namespace Bit.Core.Billing.Pricing.JSON; + +#nullable enable + +public class FreeOrScalableDTOJsonConverter : TypeReadingJsonConverter +{ + public override FreeOrScalableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var type = ReadType(reader); + + return type switch + { + "free" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var free => new FreeOrScalableDTO(free) + }, + "scalable" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var scalable => new FreeOrScalableDTO(scalable) + }, + _ => null + }; + } + + public override void Write(Utf8JsonWriter writer, FreeOrScalableDTO value, JsonSerializerOptions options) + => value.Switch( + free => JsonSerializer.Serialize(writer, free, options), + scalable => JsonSerializer.Serialize(writer, scalable, options) + ); +} diff --git a/src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs b/src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs new file mode 100644 index 0000000000..f7ae9dc472 --- /dev/null +++ b/src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using Bit.Core.Billing.Pricing.Models; + +namespace Bit.Core.Billing.Pricing.JSON; + +#nullable enable +internal class PurchasableDTOJsonConverter : TypeReadingJsonConverter +{ + public override PurchasableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var type = ReadType(reader); + + return type switch + { + "free" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var free => new PurchasableDTO(free) + }, + "packaged" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var packaged => new PurchasableDTO(packaged) + }, + "scalable" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var scalable => new PurchasableDTO(scalable) + }, + _ => null + }; + } + + public override void Write(Utf8JsonWriter writer, PurchasableDTO value, JsonSerializerOptions options) + => value.Switch( + free => JsonSerializer.Serialize(writer, free, options), + packaged => JsonSerializer.Serialize(writer, packaged, options), + scalable => JsonSerializer.Serialize(writer, scalable, options) + ); +} diff --git a/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs b/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs new file mode 100644 index 0000000000..ef8d33304e --- /dev/null +++ b/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Bit.Core.Billing.Pricing.Models; + +namespace Bit.Core.Billing.Pricing.JSON; + +#nullable enable + +public abstract class TypeReadingJsonConverter : JsonConverter +{ + protected virtual string TypePropertyName => nameof(ScalableDTO.Type).ToLower(); + + protected string? ReadType(Utf8JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString()?.ToLower() != TypePropertyName) + { + continue; + } + + reader.Read(); + return reader.GetString(); + } + + return null; + } +} diff --git a/src/Core/Billing/Pricing/Models/FeatureDTO.cs b/src/Core/Billing/Pricing/Models/FeatureDTO.cs new file mode 100644 index 0000000000..a96ac019e3 --- /dev/null +++ b/src/Core/Billing/Pricing/Models/FeatureDTO.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Billing.Pricing.Models; + +#nullable enable + +public class FeatureDTO +{ + public string Name { get; set; } = null!; + public string LookupKey { get; set; } = null!; +} diff --git a/src/Core/Billing/Pricing/Models/PlanDTO.cs b/src/Core/Billing/Pricing/Models/PlanDTO.cs new file mode 100644 index 0000000000..4ae82b3efe --- /dev/null +++ b/src/Core/Billing/Pricing/Models/PlanDTO.cs @@ -0,0 +1,27 @@ +namespace Bit.Core.Billing.Pricing.Models; + +#nullable enable + +public class PlanDTO +{ + public string LookupKey { get; set; } = null!; + public string Name { get; set; } = null!; + public string Tier { get; set; } = null!; + public string? Cadence { get; set; } + public int? LegacyYear { get; set; } + public bool Available { get; set; } + public FeatureDTO[] Features { get; set; } = null!; + public PurchasableDTO Seats { get; set; } = null!; + public ScalableDTO? ManagedSeats { get; set; } + public ScalableDTO? Storage { get; set; } + public SecretsManagerPurchasablesDTO? SecretsManager { get; set; } + public int? TrialPeriodDays { get; set; } + public string[] CanUpgradeTo { get; set; } = null!; + public Dictionary AdditionalData { get; set; } = null!; +} + +public class SecretsManagerPurchasablesDTO +{ + public FreeOrScalableDTO Seats { get; set; } = null!; + public FreeOrScalableDTO ServiceAccounts { get; set; } = null!; +} diff --git a/src/Core/Billing/Pricing/Models/PurchasableDTO.cs b/src/Core/Billing/Pricing/Models/PurchasableDTO.cs new file mode 100644 index 0000000000..8ba1c7b731 --- /dev/null +++ b/src/Core/Billing/Pricing/Models/PurchasableDTO.cs @@ -0,0 +1,73 @@ +using System.Text.Json.Serialization; +using Bit.Core.Billing.Pricing.JSON; +using OneOf; + +namespace Bit.Core.Billing.Pricing.Models; + +#nullable enable + +[JsonConverter(typeof(PurchasableDTOJsonConverter))] +public class PurchasableDTO(OneOf input) : OneOfBase(input) +{ + public static implicit operator PurchasableDTO(FreeDTO free) => new(free); + public static implicit operator PurchasableDTO(PackagedDTO packaged) => new(packaged); + public static implicit operator PurchasableDTO(ScalableDTO scalable) => new(scalable); + + public T? FromFree(Func select, Func? fallback = null) => + IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default; + + public T? FromPackaged(Func select, Func? fallback = null) => + IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default; + + public T? FromScalable(Func select, Func? fallback = null) => + IsT2 ? select(AsT2) : fallback != null ? fallback(this) : default; + + public bool IsFree => IsT0; + public bool IsPackaged => IsT1; + public bool IsScalable => IsT2; +} + +[JsonConverter(typeof(FreeOrScalableDTOJsonConverter))] +public class FreeOrScalableDTO(OneOf input) : OneOfBase(input) +{ + public static implicit operator FreeOrScalableDTO(FreeDTO freeDTO) => new(freeDTO); + public static implicit operator FreeOrScalableDTO(ScalableDTO scalableDTO) => new(scalableDTO); + + public T? FromFree(Func select, Func? fallback = null) => + IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default; + + public T? FromScalable(Func select, Func? fallback = null) => + IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default; + + public bool IsFree => IsT0; + public bool IsScalable => IsT1; +} + +public class FreeDTO +{ + public int Quantity { get; set; } + public string Type => "free"; +} + +public class PackagedDTO +{ + public int Quantity { get; set; } + public string StripePriceId { get; set; } = null!; + public decimal Price { get; set; } + public AdditionalSeats? Additional { get; set; } + public string Type => "packaged"; + + public class AdditionalSeats + { + public string StripePriceId { get; set; } = null!; + public decimal Price { get; set; } + } +} + +public class ScalableDTO +{ + public int Provided { get; set; } + public string StripePriceId { get; set; } = null!; + public decimal Price { get; set; } + public string Type => "scalable"; +} diff --git a/src/Core/Billing/Pricing/PlanAdapter.cs b/src/Core/Billing/Pricing/PlanAdapter.cs index b2b24d4cf9..c38eb0501d 100644 --- a/src/Core/Billing/Pricing/PlanAdapter.cs +++ b/src/Core/Billing/Pricing/PlanAdapter.cs @@ -1,6 +1,6 @@ using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing.Models; using Bit.Core.Models.StaticStore; -using Proto.Billing.Pricing; #nullable enable @@ -8,15 +8,15 @@ namespace Bit.Core.Billing.Pricing; public record PlanAdapter : Plan { - public PlanAdapter(PlanResponse planResponse) + public PlanAdapter(PlanDTO plan) { - Type = ToPlanType(planResponse.LookupKey); + Type = ToPlanType(plan.LookupKey); ProductTier = ToProductTierType(Type); - Name = planResponse.Name; - IsAnnual = !string.IsNullOrEmpty(planResponse.Cadence) && planResponse.Cadence == "annually"; - NameLocalizationKey = planResponse.AdditionalData?["nameLocalizationKey"]; - DescriptionLocalizationKey = planResponse.AdditionalData?["descriptionLocalizationKey"]; - TrialPeriodDays = planResponse.TrialPeriodDays; + Name = plan.Name; + IsAnnual = plan.Cadence is "annually"; + NameLocalizationKey = plan.AdditionalData["nameLocalizationKey"]; + DescriptionLocalizationKey = plan.AdditionalData["descriptionLocalizationKey"]; + TrialPeriodDays = plan.TrialPeriodDays; HasSelfHost = HasFeature("selfHost"); HasPolicies = HasFeature("policies"); HasGroups = HasFeature("groups"); @@ -30,20 +30,20 @@ public record PlanAdapter : Plan HasScim = HasFeature("scim"); HasResetPassword = HasFeature("resetPassword"); UsersGetPremium = HasFeature("usersGetPremium"); - UpgradeSortOrder = planResponse.AdditionalData != null - ? int.Parse(planResponse.AdditionalData["upgradeSortOrder"]) + UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder) + ? int.Parse(upgradeSortOrder) : 0; - DisplaySortOrder = planResponse.AdditionalData != null - ? int.Parse(planResponse.AdditionalData["displaySortOrder"]) + DisplaySortOrder = plan.AdditionalData.TryGetValue("displaySortOrder", out var displaySortOrder) + ? int.Parse(displaySortOrder) : 0; - HasCustomPermissions = HasFeature("customPermissions"); - Disabled = !planResponse.Available; - PasswordManager = ToPasswordManagerPlanFeatures(planResponse); - SecretsManager = planResponse.SecretsManager != null ? ToSecretsManagerPlanFeatures(planResponse) : null; + Disabled = !plan.Available; + LegacyYear = plan.LegacyYear; + PasswordManager = ToPasswordManagerPlanFeatures(plan); + SecretsManager = plan.SecretsManager != null ? ToSecretsManagerPlanFeatures(plan) : null; return; - bool HasFeature(string lookupKey) => planResponse.Features.Any(feature => feature.LookupKey == lookupKey); + bool HasFeature(string lookupKey) => plan.Features.Any(feature => feature.LookupKey == lookupKey); } #region Mappings @@ -86,29 +86,25 @@ public record PlanAdapter : Plan _ => throw new BillingException() // TODO: Flesh out }; - private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanResponse planResponse) + private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanDTO plan) { - var stripePlanId = GetStripePlanId(planResponse.Seats); - var stripeSeatPlanId = GetStripeSeatPlanId(planResponse.Seats); - var stripeProviderPortalSeatPlanId = planResponse.ManagedSeats?.StripePriceId; - var basePrice = GetBasePrice(planResponse.Seats); - var seatPrice = GetSeatPrice(planResponse.Seats); - var providerPortalSeatPrice = - planResponse.ManagedSeats != null ? decimal.Parse(planResponse.ManagedSeats.Price) : 0; - var scales = planResponse.Seats.KindCase switch - { - PurchasableDTO.KindOneofCase.Scalable => true, - PurchasableDTO.KindOneofCase.Packaged => planResponse.Seats.Packaged.Additional != null, - _ => false - }; - var baseSeats = GetBaseSeats(planResponse.Seats); - var maxSeats = GetMaxSeats(planResponse.Seats); - var baseStorageGb = (short?)planResponse.Storage?.Provided; - var hasAdditionalStorageOption = planResponse.Storage != null; - var stripeStoragePlanId = planResponse.Storage?.StripePriceId; - short? maxCollections = - planResponse.AdditionalData != null && - planResponse.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null; + var stripePlanId = GetStripePlanId(plan.Seats); + var stripeSeatPlanId = GetStripeSeatPlanId(plan.Seats); + var stripeProviderPortalSeatPlanId = plan.ManagedSeats?.StripePriceId; + var basePrice = GetBasePrice(plan.Seats); + var seatPrice = GetSeatPrice(plan.Seats); + var providerPortalSeatPrice = plan.ManagedSeats?.Price ?? 0; + var scales = plan.Seats.Match( + _ => false, + packaged => packaged.Additional != null, + _ => true); + var baseSeats = GetBaseSeats(plan.Seats); + var maxSeats = GetMaxSeats(plan.Seats); + var baseStorageGb = (short?)plan.Storage?.Provided; + var hasAdditionalStorageOption = plan.Storage != null; + var additionalStoragePricePerGb = plan.Storage?.Price ?? 0; + var stripeStoragePlanId = plan.Storage?.StripePriceId; + short? maxCollections = plan.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null; return new PasswordManagerPlanFeatures { @@ -124,30 +120,29 @@ public record PlanAdapter : Plan MaxSeats = maxSeats, BaseStorageGb = baseStorageGb, HasAdditionalStorageOption = hasAdditionalStorageOption, + AdditionalStoragePricePerGb = additionalStoragePricePerGb, StripeStoragePlanId = stripeStoragePlanId, MaxCollections = maxCollections }; } - private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanResponse planResponse) + private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanDTO plan) { - var seats = planResponse.SecretsManager.Seats; - var serviceAccounts = planResponse.SecretsManager.ServiceAccounts; + var seats = plan.SecretsManager!.Seats; + var serviceAccounts = plan.SecretsManager.ServiceAccounts; var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts); - var allowServiceAccountsAutoscale = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; + var allowServiceAccountsAutoscale = serviceAccounts.IsScalable; var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts); var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts); var baseServiceAccount = GetBaseServiceAccount(serviceAccounts); - var hasAdditionalServiceAccountOption = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; + var hasAdditionalServiceAccountOption = serviceAccounts.IsScalable; var stripeSeatPlanId = GetStripeSeatPlanId(seats); - var hasAdditionalSeatsOption = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; + var hasAdditionalSeatsOption = seats.IsScalable; var seatPrice = GetSeatPrice(seats); var maxSeats = GetMaxSeats(seats); - var allowSeatAutoscale = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable; - var maxProjects = - planResponse.AdditionalData != null && - planResponse.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0; + var allowSeatAutoscale = seats.IsScalable; + var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0; return new SecretsManagerPlanFeatures { @@ -167,66 +162,54 @@ public record PlanAdapter : Plan } private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable - ? null - : decimal.Parse(freeOrScalable.Scalable.Price); + => freeOrScalable.FromScalable(x => x.Price); private static decimal GetBasePrice(PurchasableDTO purchasable) - => purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : decimal.Parse(purchasable.Packaged.Price); + => purchasable.FromPackaged(x => x.Price); private static int GetBaseSeats(PurchasableDTO purchasable) - => purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : purchasable.Packaged.Quantity; + => purchasable.FromPackaged(x => x.Quantity); private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase switch - { - FreeOrScalableDTO.KindOneofCase.Free => (short)freeOrScalable.Free.Quantity, - FreeOrScalableDTO.KindOneofCase.Scalable => (short)freeOrScalable.Scalable.Provided, - _ => 0 - }; + => freeOrScalable.Match( + free => (short)free.Quantity, + scalable => (short)scalable.Provided); private static short? GetMaxSeats(PurchasableDTO purchasable) - => purchasable.KindCase != PurchasableDTO.KindOneofCase.Free ? null : (short)purchasable.Free.Quantity; + => purchasable.Match( + free => (short)free.Quantity, + packaged => (short)packaged.Quantity, + _ => null); private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity; + => freeOrScalable.FromFree(x => (short)x.Quantity); private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity; + => freeOrScalable.FromFree(x => (short)x.Quantity); private static decimal GetSeatPrice(PurchasableDTO purchasable) - => purchasable.KindCase switch - { - PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional != null ? decimal.Parse(purchasable.Packaged.Additional.Price) : 0, - PurchasableDTO.KindOneofCase.Scalable => decimal.Parse(purchasable.Scalable.Price), - _ => 0 - }; + => purchasable.Match( + _ => 0, + packaged => packaged.Additional?.Price ?? 0, + scalable => scalable.Price); private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable - ? 0 - : decimal.Parse(freeOrScalable.Scalable.Price); + => freeOrScalable.FromScalable(x => x.Price); private static string? GetStripePlanId(PurchasableDTO purchasable) - => purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? null : purchasable.Packaged.StripePriceId; + => purchasable.FromPackaged(x => x.StripePriceId); private static string? GetStripeSeatPlanId(PurchasableDTO purchasable) - => purchasable.KindCase switch - { - PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional?.StripePriceId, - PurchasableDTO.KindOneofCase.Scalable => purchasable.Scalable.StripePriceId, - _ => null - }; + => purchasable.Match( + _ => null, + packaged => packaged.Additional?.StripePriceId, + scalable => scalable.StripePriceId); private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable - ? null - : freeOrScalable.Scalable.StripePriceId; + => freeOrScalable.FromScalable(x => x.StripePriceId); private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable) - => freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable - ? null - : freeOrScalable.Scalable.StripePriceId; + => freeOrScalable.FromScalable(x => x.StripePriceId); #endregion } diff --git a/src/Core/Billing/Pricing/PricingClient.cs b/src/Core/Billing/Pricing/PricingClient.cs index 65fc1761ad..14caa54eb4 100644 --- a/src/Core/Billing/Pricing/PricingClient.cs +++ b/src/Core/Billing/Pricing/PricingClient.cs @@ -1,12 +1,13 @@ -using Bit.Core.Billing.Enums; -using Bit.Core.Models.StaticStore; +using System.Net; +using System.Net.Http.Json; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing.Models; +using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; -using Google.Protobuf.WellKnownTypes; -using Grpc.Core; -using Grpc.Net.Client; -using Proto.Billing.Pricing; +using Microsoft.Extensions.Logging; +using Plan = Bit.Core.Models.StaticStore.Plan; #nullable enable @@ -14,10 +15,17 @@ namespace Bit.Core.Billing.Pricing; public class PricingClient( IFeatureService featureService, - GlobalSettings globalSettings) : IPricingClient + GlobalSettings globalSettings, + HttpClient httpClient, + ILogger logger) : IPricingClient { public async Task GetPlan(PlanType planType) { + if (globalSettings.SelfHosted) + { + return null; + } + var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); if (!usePricingService) @@ -25,30 +33,55 @@ public class PricingClient( return StaticStore.GetPlan(planType); } - using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri); - var client = new PasswordManager.PasswordManagerClient(channel); + var lookupKey = GetLookupKey(planType); - var lookupKey = ToLookupKey(planType); - if (string.IsNullOrEmpty(lookupKey)) + if (lookupKey == null) { + logger.LogError("Could not find Pricing Service lookup key for PlanType {PlanType}", planType); return null; } - try - { - var response = - await client.GetPlanByLookupKeyAsync(new GetPlanByLookupKeyRequest { LookupKey = lookupKey }); + var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}"); - return new PlanAdapter(response); - } - catch (RpcException rpcException) when (rpcException.StatusCode == StatusCode.NotFound) + if (response.IsSuccessStatusCode) { + var plan = await response.Content.ReadFromJsonAsync(); + if (plan == null) + { + throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); + } + return new PlanAdapter(plan); + } + + if (response.StatusCode == HttpStatusCode.NotFound) + { + logger.LogError("Pricing Service plan for PlanType {PlanType} was not found", planType); return null; } + + throw new BillingException( + message: $"Request to the Pricing Service failed with status code {response.StatusCode}"); + } + + public async Task GetPlanOrThrow(PlanType planType) + { + var plan = await GetPlan(planType); + + if (plan == null) + { + throw new NotFoundException(); + } + + return plan; } public async Task> ListPlans() { + if (globalSettings.SelfHosted) + { + return []; + } + var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); if (!usePricingService) @@ -56,14 +89,23 @@ public class PricingClient( return StaticStore.Plans.ToList(); } - using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri); - var client = new PasswordManager.PasswordManagerClient(channel); + var response = await httpClient.GetAsync("plans"); - var response = await client.ListPlansAsync(new Empty()); - return response.Plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList(); + if (response.IsSuccessStatusCode) + { + var plans = await response.Content.ReadFromJsonAsync>(); + if (plans == null) + { + throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); + } + return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList(); + } + + throw new BillingException( + message: $"Request to the Pricing Service failed with status {response.StatusCode}"); } - private static string? ToLookupKey(PlanType planType) + private static string? GetLookupKey(PlanType planType) => planType switch { PlanType.EnterpriseAnnually => "enterprise-annually", diff --git a/src/Core/Billing/Pricing/Protos/password-manager.proto b/src/Core/Billing/Pricing/Protos/password-manager.proto deleted file mode 100644 index 69a4c51bd1..0000000000 --- a/src/Core/Billing/Pricing/Protos/password-manager.proto +++ /dev/null @@ -1,92 +0,0 @@ -syntax = "proto3"; - -option csharp_namespace = "Proto.Billing.Pricing"; - -package plans; - -import "google/protobuf/empty.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/wrappers.proto"; - -service PasswordManager { - rpc GetPlanByLookupKey (GetPlanByLookupKeyRequest) returns (PlanResponse); - rpc ListPlans (google.protobuf.Empty) returns (ListPlansResponse); -} - -// Requests -message GetPlanByLookupKeyRequest { - string lookupKey = 1; -} - -// Responses -message PlanResponse { - string name = 1; - string lookupKey = 2; - string tier = 4; - optional string cadence = 6; - optional google.protobuf.Int32Value legacyYear = 8; - bool available = 9; - repeated FeatureDTO features = 10; - PurchasableDTO seats = 11; - optional ScalableDTO managedSeats = 12; - optional ScalableDTO storage = 13; - optional SecretsManagerPurchasablesDTO secretsManager = 14; - optional google.protobuf.Int32Value trialPeriodDays = 15; - repeated string canUpgradeTo = 16; - map additionalData = 17; -} - -message ListPlansResponse { - repeated PlanResponse plans = 1; -} - -// DTOs -message FeatureDTO { - string name = 1; - string lookupKey = 2; -} - -message FreeDTO { - int32 quantity = 2; - string type = 4; -} - -message PackagedDTO { - message AdditionalSeats { - string stripePriceId = 1; - string price = 2; - } - - int32 quantity = 2; - string stripePriceId = 3; - string price = 4; - optional AdditionalSeats additional = 5; - string type = 6; -} - -message ScalableDTO { - int32 provided = 2; - string stripePriceId = 6; - string price = 7; - string type = 9; -} - -message PurchasableDTO { - oneof kind { - FreeDTO free = 1; - PackagedDTO packaged = 2; - ScalableDTO scalable = 3; - } -} - -message FreeOrScalableDTO { - oneof kind { - FreeDTO free = 1; - ScalableDTO scalable = 2; - } -} - -message SecretsManagerPurchasablesDTO { - FreeOrScalableDTO seats = 1; - FreeOrScalableDTO serviceAccounts = 2; -} diff --git a/src/Core/Billing/Pricing/ServiceCollectionExtensions.cs b/src/Core/Billing/Pricing/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..465a12de14 --- /dev/null +++ b/src/Core/Billing/Pricing/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Bit.Core.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Billing.Pricing; + +public static class ServiceCollectionExtensions +{ + public static void AddPricingClient(this IServiceCollection services) + { + services.AddHttpClient((serviceProvider, httpClient) => + { + var globalSettings = serviceProvider.GetRequiredService(); + if (string.IsNullOrEmpty(globalSettings.PricingUri)) + { + return; + } + httpClient.BaseAddress = new Uri(globalSettings.PricingUri); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + }); + } +} diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 57a4b1781b..8b773f1cef 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -3,12 +3,12 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Utilities; using Braintree; using Microsoft.Extensions.Logging; using Stripe; @@ -26,6 +26,7 @@ public class OrganizationBillingService( IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, + IPricingClient pricingClient, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, @@ -63,13 +64,22 @@ public class OrganizationBillingService( return null; } - var isEligibleForSelfHost = IsEligibleForSelfHost(organization); + if (globalSettings.SelfHosted) + { + return OrganizationMetadata.Default; + } + + var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization); + var isManaged = organization.Status == OrganizationStatusType.Managed; if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { - return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false, - false, false, false, false, null, null, null); + return OrganizationMetadata.Default with + { + IsEligibleForSelfHost = isEligibleForSelfHost, + IsManaged = isManaged + }; } var customer = await subscriberService.GetCustomer(organization, @@ -77,18 +87,21 @@ public class OrganizationBillingService( var subscription = await subscriberService.GetSubscription(organization); - var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); - var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription); - var isSubscriptionCanceled = IsSubscriptionCanceled(subscription); - var hasSubscription = true; - var openInvoice = await HasOpenInvoiceAsync(subscription); - var hasOpenInvoice = openInvoice.HasOpenInvoice; - var invoiceDueDate = openInvoice.DueDate; - var invoiceCreatedDate = openInvoice.CreatedDate; - var subPeriodEndDate = subscription?.CurrentPeriodEnd; + var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); - return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, - isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, isSubscriptionCanceled, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate); + var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()); + + return new OrganizationMetadata( + isEligibleForSelfHost, + isManaged, + isOnSecretsManagerStandalone, + subscription.Status == StripeConstants.SubscriptionStatus.Unpaid, + true, + invoice?.Status == StripeConstants.InvoiceStatus.Open, + subscription.Status == StripeConstants.SubscriptionStatus.Canceled, + invoice?.DueDate, + invoice?.Created, + subscription.CurrentPeriodEnd); } public async Task UpdatePaymentMethod( @@ -299,7 +312,7 @@ public class OrganizationBillingService( Customer customer, SubscriptionSetup subscriptionSetup) { - var plan = subscriptionSetup.Plan; + var plan = await pricingClient.GetPlanOrThrow(subscriptionSetup.PlanType); var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions; @@ -385,15 +398,17 @@ public class OrganizationBillingService( return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); } - private static bool IsEligibleForSelfHost( + private async Task IsEligibleForSelfHostAsync( Organization organization) { - var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); + var plans = await pricingClient.ListPlans(); + + var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); return eligibleSelfHostPlans.Contains(organization.PlanType); } - private static bool IsOnSecretsManagerStandalone( + private async Task IsOnSecretsManagerStandalone( Organization organization, Customer? customer, Subscription? subscription) @@ -403,7 +418,7 @@ public class OrganizationBillingService( return false; } - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); if (!plan.SupportsSecretsManager) { @@ -424,38 +439,5 @@ public class OrganizationBillingService( return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); } - private static bool IsSubscriptionUnpaid(Subscription subscription) - { - if (subscription == null) - { - return false; - } - - return subscription.Status == "unpaid"; - } - - private async Task<(bool HasOpenInvoice, DateTime? CreatedDate, DateTime? DueDate)> HasOpenInvoiceAsync(Subscription subscription) - { - if (subscription?.LatestInvoiceId == null) - { - return (false, null, null); - } - - var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()); - - return invoice?.Status == "open" - ? (true, invoice.Created, invoice.DueDate) - : (false, null, null); - } - - private static bool IsSubscriptionCanceled(Subscription subscription) - { - if (subscription == null) - { - return false; - } - - return subscription.Status == "canceled"; - } #endregion } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 860cf33298..ff5e929f18 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -27,12 +27,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -78,11 +72,7 @@ - - - - - + diff --git a/src/Core/Models/Business/CompleteSubscriptionUpdate.cs b/src/Core/Models/Business/CompleteSubscriptionUpdate.cs index aa1c92dc2e..1d983404af 100644 --- a/src/Core/Models/Business/CompleteSubscriptionUpdate.cs +++ b/src/Core/Models/Business/CompleteSubscriptionUpdate.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Exceptions; using Stripe; +using Plan = Bit.Core.Models.StaticStore.Plan; namespace Bit.Core.Models.Business; @@ -9,7 +10,7 @@ namespace Bit.Core.Models.Business; /// public class SubscriptionData { - public StaticStore.Plan Plan { get; init; } + public Plan Plan { get; init; } public int PurchasedPasswordManagerSeats { get; init; } public bool SubscribedToSecretsManager { get; set; } public int? PurchasedSecretsManagerSeats { get; init; } @@ -38,22 +39,24 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate /// in the case of an error. /// /// The to upgrade. + /// The organization's plan. /// The updates you want to apply to the organization's subscription. public CompleteSubscriptionUpdate( Organization organization, + Plan plan, SubscriptionData updatedSubscription) { - _currentSubscription = GetSubscriptionDataFor(organization); + _currentSubscription = GetSubscriptionDataFor(organization, plan); _updatedSubscription = updatedSubscription; } - protected override List PlanIds => new() - { + protected override List PlanIds => + [ GetPasswordManagerPlanId(_updatedSubscription.Plan), _updatedSubscription.Plan.SecretsManager.StripeSeatPlanId, _updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId, _updatedSubscription.Plan.PasswordManager.StripeStoragePlanId - }; + ]; /// /// Generates the necessary to revert an 's @@ -94,7 +97,7 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate */ /// /// Checks whether the updates provided in the 's constructor - /// are actually different than the organization's current . + /// are actually different from the organization's current . /// /// The organization's . public override bool UpdateNeeded(Subscription subscription) @@ -278,11 +281,8 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate }; } - private static SubscriptionData GetSubscriptionDataFor(Organization organization) - { - var plan = Utilities.StaticStore.GetPlan(organization.PlanType); - - return new SubscriptionData + private static SubscriptionData GetSubscriptionDataFor(Organization organization, Plan plan) + => new() { Plan = plan, PurchasedPasswordManagerSeats = organization.Seats.HasValue @@ -299,5 +299,4 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate ? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) : 0 }; - } } diff --git a/src/Core/Models/Business/ProviderSubscriptionUpdate.cs b/src/Core/Models/Business/ProviderSubscriptionUpdate.cs index d66013ad14..1fd833ca1f 100644 --- a/src/Core/Models/Business/ProviderSubscriptionUpdate.cs +++ b/src/Core/Models/Business/ProviderSubscriptionUpdate.cs @@ -2,6 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Stripe; +using Plan = Bit.Core.Models.StaticStore.Plan; namespace Bit.Core.Models.Business; @@ -14,18 +15,16 @@ public class ProviderSubscriptionUpdate : SubscriptionUpdate protected override List PlanIds => [_planId]; public ProviderSubscriptionUpdate( - PlanType planType, + Plan plan, int previouslyPurchasedSeats, int newlyPurchasedSeats) { - if (!planType.SupportsConsolidatedBilling()) + if (!plan.Type.SupportsConsolidatedBilling()) { throw new BillingException( message: $"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing"); } - var plan = Utilities.StaticStore.GetPlan(planType); - _planId = plan.PasswordManager.StripeProviderPortalSeatPlanId; _previouslyPurchasedSeats = previouslyPurchasedSeats; _newlyPurchasedSeats = newlyPurchasedSeats; diff --git a/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs b/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs index 9a4fcac034..d85925db34 100644 --- a/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs +++ b/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs @@ -7,6 +7,7 @@ namespace Bit.Core.Models.Business; public class SecretsManagerSubscriptionUpdate { public Organization Organization { get; } + public Plan Plan { get; } /// /// The total seats the organization will have after the update, including any base seats included in the plan @@ -49,21 +50,16 @@ public class SecretsManagerSubscriptionUpdate public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats; public bool MaxAutoscaleSmServiceAccountsChanged => MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts; - public Plan Plan => Utilities.StaticStore.GetPlan(Organization.PlanType); public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats; public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue && MaxAutoscaleSmServiceAccounts.HasValue && SmServiceAccounts == MaxAutoscaleSmServiceAccounts; - public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling) + public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling) { - if (organization == null) - { - throw new NotFoundException("Organization is not found."); - } - - Organization = organization; + Organization = organization ?? throw new NotFoundException("Organization is not found."); + Plan = plan; if (!Plan.SupportsSecretsManager) { diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index 5294097613..c9c3b31c7f 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -82,7 +82,6 @@ public class SubscriptionInfo } public bool AddonSubscriptionItem { get; set; } - public string ProductId { get; set; } public string Name { get; set; } public decimal Amount { get; set; } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs index f817ef7d2e..2756f8930b 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -54,8 +55,9 @@ public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand foreach (var selfHostedSponsorship in sponsorshipsData) { var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType; + var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier(); if (requiredSponsoringProductType == null - || StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value) + || sponsoringOrgProductTier != requiredSponsoringProductType.Value) { continue; // prevent unsupported sponsorships } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs index e8d43fd6a9..a54106481c 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -50,9 +51,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand // Check org to sponsor's product type var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType; + var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier(); + if (requiredSponsoredProductType == null || - sponsoredOrganization == null || - StaticStore.GetPlan(sponsoredOrganization.PlanType).ProductTier != requiredSponsoredProductType.Value) + sponsoredOrganizationProductTier != requiredSponsoredProductType.Value) { throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs index 214786c0ae..a7423b067e 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; @@ -103,8 +104,6 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo return false; } - var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); - if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization)) { _logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is disabled for more than 3 months.", sponsoringOrganization.Id); @@ -113,7 +112,9 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo return false; } - if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgPlan.ProductTier) + var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier(); + + if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgProductTier) { _logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is not on the required product type.", sponsoringOrganization.Id); await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship); diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index a00dae2a9d..ac65d3b897 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -31,9 +32,10 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand } var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType; + var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier(); + if (requiredSponsoringProductType == null || - sponsoringOrg == null || - StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value) + sponsoringOrgProductTier != requiredSponsoringProductType.Value) { throw new BadRequestException("Specified Organization cannot sponsor other organizations."); } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs index 08cd09e5c3..a0ce7c03b9 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs @@ -2,11 +2,11 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Services; -using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -15,22 +15,25 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti private readonly IPaymentService _paymentService; private readonly IOrganizationService _organizationService; private readonly IProviderRepository _providerRepository; + private readonly IPricingClient _pricingClient; public AddSecretsManagerSubscriptionCommand( IPaymentService paymentService, IOrganizationService organizationService, - IProviderRepository providerRepository) + IProviderRepository providerRepository, + IPricingClient pricingClient) { _paymentService = paymentService; _organizationService = organizationService; _providerRepository = providerRepository; + _pricingClient = pricingClient; } public async Task SignUpAsync(Organization organization, int additionalSmSeats, int additionalServiceAccounts) { await ValidateOrganization(organization); - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts); _organizationService.ValidateSecretsManagerPlan(plan, signup); @@ -73,7 +76,13 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti throw new BadRequestException("Organization already uses Secrets Manager."); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType && p.SupportsSecretsManager); + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + + if (!plan.SupportsSecretsManager) + { + throw new BadRequestException("Organization's plan does not support Secrets Manager."); + } + if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.ProductTier != ProductTierType.Free) { throw new BadRequestException("No payment method found."); diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 19af8121e7..09b766e885 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -6,6 +6,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; @@ -18,7 +19,6 @@ using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; -using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -38,6 +38,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand private readonly IOrganizationService _organizationService; private readonly IFeatureService _featureService; private readonly IOrganizationBillingService _organizationBillingService; + private readonly IPricingClient _pricingClient; public UpgradeOrganizationPlanCommand( IOrganizationUserRepository organizationUserRepository, @@ -53,7 +54,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand IOrganizationRepository organizationRepository, IOrganizationService organizationService, IFeatureService featureService, - IOrganizationBillingService organizationBillingService) + IOrganizationBillingService organizationBillingService, + IPricingClient pricingClient) { _organizationUserRepository = organizationUserRepository; _collectionRepository = collectionRepository; @@ -69,6 +71,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand _organizationService = organizationService; _featureService = featureService; _organizationBillingService = organizationBillingService; + _pricingClient = pricingClient; } public async Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) @@ -84,14 +87,11 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand throw new BadRequestException("Your account has no payment method available."); } - var existingPlan = StaticStore.GetPlan(organization.PlanType); - if (existingPlan == null) - { - throw new BadRequestException("Existing plan not found."); - } + var existingPlan = await _pricingClient.GetPlanOrThrow(organization.PlanType); - var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); - if (newPlan == null) + var newPlan = await _pricingClient.GetPlanOrThrow(upgrade.Plan); + + if (newPlan.Disabled) { throw new BadRequestException("Plan not found."); } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 7f2ac36216..1a8fe2085d 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -7,6 +7,7 @@ using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.Models.Api.Requests.Organizations; using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -37,6 +38,7 @@ public class StripePaymentService : IPaymentService private readonly IFeatureService _featureService; private readonly ITaxService _taxService; private readonly ISubscriberService _subscriberService; + private readonly IPricingClient _pricingClient; public StripePaymentService( ITransactionRepository transactionRepository, @@ -46,7 +48,8 @@ public class StripePaymentService : IPaymentService IGlobalSettings globalSettings, IFeatureService featureService, ITaxService taxService, - ISubscriberService subscriberService) + ISubscriberService subscriberService, + IPricingClient pricingClient) { _transactionRepository = transactionRepository; _logger = logger; @@ -56,6 +59,7 @@ public class StripePaymentService : IPaymentService _featureService = featureService; _taxService = taxService; _subscriberService = subscriberService; + _pricingClient = pricingClient; } public async Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, @@ -297,7 +301,7 @@ public class StripePaymentService : IPaymentService OrganizationSponsorship sponsorship, bool applySponsorship) { - var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); + var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType); var sponsoredPlan = sponsorship?.PlanSponsorshipType != null ? Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : null; @@ -887,18 +891,21 @@ public class StripePaymentService : IPaymentService return paymentIntentClientSecret; } - public Task AdjustSubscription( + public async Task AdjustSubscription( Organization organization, StaticStore.Plan updatedPlan, int newlyPurchasedPasswordManagerSeats, bool subscribedToSecretsManager, int? newlyPurchasedSecretsManagerSeats, int? newlyPurchasedAdditionalSecretsManagerServiceAccounts, - int newlyPurchasedAdditionalStorage) => - FinalizeSubscriptionChangeAsync( + int newlyPurchasedAdditionalStorage) + { + var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + return await FinalizeSubscriptionChangeAsync( organization, new CompleteSubscriptionUpdate( organization, + plan, new SubscriptionData { Plan = updatedPlan, @@ -909,6 +916,7 @@ public class StripePaymentService : IPaymentService newlyPurchasedAdditionalSecretsManagerServiceAccounts, PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage }), true); + } public Task AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) => FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats)); @@ -921,7 +929,7 @@ public class StripePaymentService : IPaymentService => FinalizeSubscriptionChangeAsync( provider, new ProviderSubscriptionUpdate( - plan.Type, + plan, currentlySubscribedSeats, newlySubscribedSeats)); @@ -1957,7 +1965,7 @@ public class StripePaymentService : IPaymentService string gatewayCustomerId, string gatewaySubscriptionId) { - var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan); + var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan); var options = new InvoiceCreatePreviewOptions { diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 78fcd0d99f..24e9ccd7bd 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -137,6 +138,7 @@ public static class StaticStore } public static IDictionary> GlobalDomains { get; set; } + [Obsolete("Use PricingClient.ListPlans to retrieve all plans.")] public static IEnumerable Plans { get; } public static IEnumerable SponsoredPlans { get; set; } = new[] { @@ -147,10 +149,11 @@ public static class StaticStore SponsoringProductTierType = ProductTierType.Enterprise, StripePlanId = "2021-family-for-enterprise-annually", UsersCanSponsor = (OrganizationUserOrganizationDetails org) => - GetPlan(org.PlanType).ProductTier == ProductTierType.Enterprise, + org.PlanType.GetProductTier() == ProductTierType.Enterprise, } }; + [Obsolete("Use PricingClient.GetPlan to retrieve a plan.")] public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType); public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => @@ -167,6 +170,7 @@ public static class StaticStore /// public static bool IsAddonSubscriptionItem(string stripePlanId) { + // TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-16844 return Plans.Any(p => p.PasswordManager.StripeStoragePlanId == stripePlanId || (p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId)); diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index b739469c78..b0906ddc43 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -17,6 +17,7 @@ using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -54,6 +55,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand; + private readonly IPricingClient _pricingClient; private readonly OrganizationsController _sut; public OrganizationsControllerTests() @@ -78,6 +80,7 @@ public class OrganizationsControllerTests : IDisposable _removeOrganizationUserCommand = Substitute.For(); _cloudOrganizationSignUpCommand = Substitute.For(); _organizationDeleteCommand = Substitute.For(); + _pricingClient = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -99,7 +102,8 @@ public class OrganizationsControllerTests : IDisposable _orgDeleteTokenDataFactory, _removeOrganizationUserCommand, _cloudOrganizationSignUpCommand, - _organizationDeleteCommand); + _organizationDeleteCommand, + _pricingClient); } public void Dispose() diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index 483d962830..16e32870ad 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -10,6 +10,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -49,6 +50,7 @@ public class OrganizationsControllerTests : IDisposable private readonly ISubscriberService _subscriberService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IOrganizationInstallationRepository _organizationInstallationRepository; + private readonly IPricingClient _pricingClient; private readonly OrganizationsController _sut; @@ -73,6 +75,7 @@ public class OrganizationsControllerTests : IDisposable _subscriberService = Substitute.For(); _removeOrganizationUserCommand = Substitute.For(); _organizationInstallationRepository = Substitute.For(); + _pricingClient = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -89,7 +92,8 @@ public class OrganizationsControllerTests : IDisposable _addSecretsManagerSubscriptionCommand, _referenceEventService, _subscriberService, - _organizationInstallationRepository); + _organizationInstallationRepository, + _pricingClient); } public void Dispose() diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 644303c873..df84f74d11 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -331,6 +332,11 @@ public class ProviderBillingControllerTests sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + foreach (var providerPlan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType)); + } + var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); Assert.IsType>(result); diff --git a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 731494a846..8147b81240 100644 --- a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -2,6 +2,7 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -15,6 +16,7 @@ using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -119,6 +121,8 @@ public class ServiceAccountsControllerTests { ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + await sutProvider.Sut.CreateAsync(organization.Id, data); await sutProvider.GetDependency().Received(1) diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index 31f4ec8969..e080dd8288 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -1,14 +1,16 @@ using Bit.Billing.Services; using Bit.Billing.Services.Implementations; using Bit.Billing.Test.Utilities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; +using Bit.Core.Repositories; using Bit.Core.Utilities; -using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; @@ -17,6 +19,12 @@ namespace Bit.Billing.Test.Services; public class ProviderEventServiceTests { + private readonly IOrganizationRepository _organizationRepository = + Substitute.For(); + + private readonly IPricingClient _pricingClient = + Substitute.For(); + private readonly IProviderInvoiceItemRepository _providerInvoiceItemRepository = Substitute.For(); @@ -37,7 +45,8 @@ public class ProviderEventServiceTests public ProviderEventServiceTests() { _providerEventService = new ProviderEventService( - Substitute.For>(), + _organizationRepository, + _pricingClient, _providerInvoiceItemRepository, _providerOrganizationRepository, _providerPlanRepository, @@ -147,6 +156,12 @@ public class ProviderEventServiceTests _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId).Returns(clients); + _organizationRepository.GetByIdAsync(client1Id) + .Returns(new Organization { PlanType = PlanType.TeamsMonthly }); + + _organizationRepository.GetByIdAsync(client2Id) + .Returns(new Organization { PlanType = PlanType.EnterpriseMonthly }); + var providerPlans = new List { new () @@ -169,6 +184,11 @@ public class ProviderEventServiceTests } }; + foreach (var providerPlan in providerPlans) + { + _pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType)); + } + _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans); // Act diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index 859c74f3d0..544c97d166 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -38,6 +39,8 @@ public class CloudICloudOrganizationSignUpCommandTests signup.IsFromSecretsManagerTrial = false; signup.IsFromProvider = false; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); await sutProvider.GetDependency().Received(1).CreateAsync( @@ -66,7 +69,7 @@ public class CloudICloudOrganizationSignUpCommandTests sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken && sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry && sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode && - sale.SubscriptionSetup.Plan == plan && + sale.SubscriptionSetup.PlanType == plan.Type && sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats && sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb && sale.SubscriptionSetup.SecretsManagerOptions == null)); @@ -84,6 +87,8 @@ public class CloudICloudOrganizationSignUpCommandTests signup.UseSecretsManager = false; signup.IsFromProvider = false; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + // Extract orgUserId when created Guid? orgUserId = null; await sutProvider.GetDependency() @@ -128,6 +133,7 @@ public class CloudICloudOrganizationSignUpCommandTests signup.IsFromSecretsManagerTrial = false; signup.IsFromProvider = false; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); var result = await sutProvider.Sut.SignUpOrganizationAsync(signup); @@ -157,7 +163,7 @@ public class CloudICloudOrganizationSignUpCommandTests sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken && sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry && sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode && - sale.SubscriptionSetup.Plan == plan && + sale.SubscriptionSetup.PlanType == plan.Type && sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats && sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb && sale.SubscriptionSetup.SecretsManagerOptions.Seats == signup.AdditionalSmSeats && @@ -177,6 +183,8 @@ public class CloudICloudOrganizationSignUpCommandTests signup.PremiumAccessAddon = false; signup.IsFromProvider = true; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("Organizations with a Managed Service Provider do not support Secrets Manager.", exception.Message); } @@ -195,6 +203,8 @@ public class CloudICloudOrganizationSignUpCommandTests signup.AdditionalStorageGb = 0; signup.IsFromProvider = false; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); @@ -213,6 +223,8 @@ public class CloudICloudOrganizationSignUpCommandTests signup.AdditionalServiceAccounts = 10; signup.IsFromProvider = false; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats", exception.Message); @@ -231,6 +243,8 @@ public class CloudICloudOrganizationSignUpCommandTests signup.AdditionalServiceAccounts = -10; signup.IsFromProvider = false; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpOrganizationAsync(signup)); Assert.Contains("You can't subtract Machine Accounts!", exception.Message); @@ -249,6 +263,8 @@ public class CloudICloudOrganizationSignUpCommandTests Owner = new User { Id = Guid.NewGuid() } }; + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(StaticStore.GetPlan(signup.Plan)); + sutProvider.GetDependency() .GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id) .Returns(1); diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 52dd39e182..4c42fdfeb9 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -10,6 +10,7 @@ using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -203,10 +204,12 @@ public class OrganizationServiceTests { signup.Plan = PlanType.TeamsMonthly; - var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup); - var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(plan); + + var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup); + await sutProvider.GetDependency().Received(1).CreateAsync(Arg.Is(org => org.Id == organization.Id && org.Name == signup.Name && @@ -894,6 +897,8 @@ OrganizationUserInvite invite, SutProvider sutProvider) SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites); await sutProvider.GetDependency().Received(1) @@ -933,6 +938,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) sutProvider.GetDependency().RaiseEventAsync(default) .ThrowsForAnyArgs(); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites)); @@ -1338,6 +1346,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) organization.MaxAutoscaleSeats = currentMaxAutoscaleSeats; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, maxAutoscaleSeats)); @@ -1360,6 +1371,9 @@ OrganizationUserInvite invite, SutProvider sutProvider) organization.Seats = 100; organization.SmSeats = 100; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null)); diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 2e1782f5c8..1f15c5f7fd 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -1,8 +1,10 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -15,43 +17,6 @@ namespace Bit.Core.Test.Billing.Services; public class OrganizationBillingServiceTests { #region GetMetadata - [Theory, BitAutoData] - public async Task GetMetadata_OrganizationNull_ReturnsNull( - Guid organizationId, - SutProvider sutProvider) - { - var metadata = await sutProvider.Sut.GetMetadata(organizationId); - - Assert.Null(metadata); - } - - [Theory, BitAutoData] - public async Task GetMetadata_CustomerNull_ReturnsNull( - Guid organizationId, - Organization organization, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); - - var metadata = await sutProvider.Sut.GetMetadata(organizationId); - - Assert.False(metadata.IsOnSecretsManagerStandalone); - } - - [Theory, BitAutoData] - public async Task GetMetadata_SubscriptionNull_ReturnsNull( - Guid organizationId, - Organization organization, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); - - sutProvider.GetDependency().GetCustomer(organization).Returns(new Customer()); - - var metadata = await sutProvider.Sut.GetMetadata(organizationId); - - Assert.False(metadata.IsOnSecretsManagerStandalone); - } [Theory, BitAutoData] public async Task GetMetadata_Succeeds( @@ -61,6 +26,11 @@ public class OrganizationBillingServiceTests { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(organization); + sutProvider.GetDependency().ListPlans().Returns(StaticStore.Plans.ToList()); + + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + var subscriberService = sutProvider.GetDependency(); subscriberService @@ -99,7 +69,8 @@ public class OrganizationBillingServiceTests var metadata = await sutProvider.Sut.GetMetadata(organizationId); - Assert.True(metadata.IsOnSecretsManagerStandalone); + Assert.True(metadata!.IsOnSecretsManagerStandalone); } + #endregion } diff --git a/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs index ceb4735684..dee805033a 100644 --- a/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs @@ -43,7 +43,7 @@ public class CompleteSubscriptionUpdateTests PurchasedPasswordManagerSeats = 20 }; - var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); + var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsStarterPlan, updatedSubscriptionData); var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription); @@ -114,7 +114,7 @@ public class CompleteSubscriptionUpdateTests PurchasedAdditionalStorage = 10 }; - var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); + var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData); var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription); @@ -221,7 +221,7 @@ public class CompleteSubscriptionUpdateTests PurchasedAdditionalStorage = 10 }; - var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); + var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData); var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription); @@ -302,7 +302,7 @@ public class CompleteSubscriptionUpdateTests PurchasedPasswordManagerSeats = 20 }; - var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); + var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsStarterPlan, updatedSubscriptionData); var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription); @@ -372,7 +372,7 @@ public class CompleteSubscriptionUpdateTests PurchasedAdditionalStorage = 10 }; - var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); + var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData); var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription); @@ -478,7 +478,7 @@ public class CompleteSubscriptionUpdateTests PurchasedAdditionalStorage = 10 }; - var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData); + var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData); var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription); diff --git a/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs index faf20eb6dc..6a411363a0 100644 --- a/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs +++ b/test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs @@ -2,7 +2,9 @@ using Bit.Core.Billing.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -11,19 +13,40 @@ namespace Bit.Core.Test.Models.Business; [SecretsManagerOrganizationCustomize] public class SecretsManagerSubscriptionUpdateTests { + private static TheoryData ToPlanTheory(List types) + { + var theoryData = new TheoryData(); + var plans = types.Select(StaticStore.GetPlan).ToArray(); + theoryData.AddRange(plans); + return theoryData; + } + + public static TheoryData NonSmPlans => + ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019]); + + public static TheoryData SmPlans => ToPlanTheory([ + PlanType.EnterpriseAnnually2019, + PlanType.EnterpriseAnnually, + PlanType.TeamsMonthly2019, + PlanType.TeamsAnnually2020, + PlanType.TeamsMonthly, + PlanType.TeamsAnnually2019, + PlanType.TeamsAnnually2020, + PlanType.TeamsAnnually, + PlanType.TeamsStarter + ]); + [Theory] - [BitAutoData(PlanType.Custom)] - [BitAutoData(PlanType.FamiliesAnnually)] - [BitAutoData(PlanType.FamiliesAnnually2019)] + [BitMemberAutoData(nameof(NonSmPlans))] public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException( - PlanType planType, + Plan plan, Organization organization) { // Arrange - organization.PlanType = planType; + organization.PlanType = plan.Type; // Act - var exception = Assert.Throws(() => new SecretsManagerSubscriptionUpdate(organization, false)); + var exception = Assert.Throws(() => new SecretsManagerSubscriptionUpdate(organization, plan, false)); // Assert Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase); @@ -31,28 +54,16 @@ public class SecretsManagerSubscriptionUpdateTests } [Theory] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] + [BitMemberAutoData(nameof(SmPlans))] public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException( - PlanType planType, + Plan plan, Organization organization) { // Arrange - organization.PlanType = planType; + organization.PlanType = plan.Type; // Act - var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, false)); + var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, plan, false)); // Assert Assert.Null(ex); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs index 8dcfb198b6..02ae40798b 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; @@ -41,7 +42,8 @@ public class AddSecretsManagerSubscriptionCommandTests { organization.PlanType = planType; - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); + var plan = StaticStore.GetPlan(organization.PlanType); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(plan); await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts); @@ -85,6 +87,8 @@ public class AddSecretsManagerSubscriptionCommandTests { organization.GatewayCustomerId = null; organization.PlanType = PlanType.EnterpriseAnnually; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts)); Assert.Contains("No payment method found.", exception.Message); @@ -101,6 +105,8 @@ public class AddSecretsManagerSubscriptionCommandTests { organization.GatewaySubscriptionId = null; organization.PlanType = PlanType.EnterpriseAnnually; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts)); Assert.Contains("No subscription found.", exception.Message); @@ -132,6 +138,8 @@ public class AddSecretsManagerSubscriptionCommandTests organization.UseSecretsManager = false; provider.Type = ProviderType.Msp; sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(provider); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SignUpAsync(organization, 10, 10)); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs index 546ea7770c..50f51da7d0 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs @@ -21,26 +21,48 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate; [SecretsManagerOrganizationCustomize] public class UpdateSecretsManagerSubscriptionCommandTests { + private static TheoryData ToPlanTheory(List types) + { + var theoryData = new TheoryData(); + var plans = types.Select(StaticStore.GetPlan).ToArray(); + theoryData.AddRange(plans); + return theoryData; + } + + public static TheoryData AllTeamsAndEnterprise + => ToPlanTheory([ + PlanType.EnterpriseAnnually2019, + PlanType.EnterpriseAnnually2020, + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly2019, + PlanType.EnterpriseMonthly2020, + PlanType.EnterpriseMonthly, + PlanType.TeamsMonthly2019, + PlanType.TeamsMonthly2020, + PlanType.TeamsMonthly, + PlanType.TeamsAnnually2019, + PlanType.TeamsAnnually2020, + PlanType.TeamsAnnually, + PlanType.TeamsStarter + ]); + + public static TheoryData CurrentTeamsAndEnterprise + => ToPlanTheory([ + PlanType.EnterpriseAnnually, + PlanType.EnterpriseMonthly, + PlanType.TeamsMonthly, + PlanType.TeamsAnnually, + PlanType.TeamsStarter + ]); + [Theory] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] + [BitMemberAutoData(nameof(AllTeamsAndEnterprise))] public async Task UpdateSubscriptionAsync_UpdateEverything_ValidInput_Passes( - PlanType planType, + Plan plan, Organization organization, SutProvider sutProvider) { - organization.PlanType = planType; + organization.PlanType = plan.Type; organization.Seats = 400; organization.SmSeats = 10; organization.MaxAutoscaleSmSeats = 20; @@ -52,7 +74,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests var updateMaxAutoscaleSmSeats = 16; var updateMaxAutoscaleSmServiceAccounts = 301; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = updateSmSeats, SmServiceAccounts = updateSmServiceAccounts, @@ -62,7 +84,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests await sutProvider.Sut.UpdateSubscriptionAsync(update); - var plan = StaticStore.GetPlan(organization.PlanType); await sutProvider.GetDependency().Received(1) .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase); await sutProvider.GetDependency().Received(1) @@ -83,17 +104,13 @@ public class UpdateSecretsManagerSubscriptionCommandTests } [Theory] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] + [BitMemberAutoData(nameof(CurrentTeamsAndEnterprise))] public async Task UpdateSubscriptionAsync_ValidInput_WithNullMaxAutoscale_Passes( - PlanType planType, + Plan plan, Organization organization, SutProvider sutProvider) { - organization.PlanType = planType; + organization.PlanType = plan.Type; organization.Seats = 20; const int updateSmSeats = 15; @@ -102,7 +119,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests // Ensure that SmSeats is different from the original organization.SmSeats organization.SmSeats = updateSmSeats + 5; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = updateSmSeats, MaxAutoscaleSmSeats = null, @@ -112,7 +129,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests await sutProvider.Sut.UpdateSubscriptionAsync(update); - var plan = StaticStore.GetPlan(organization.PlanType); await sutProvider.GetDependency().Received(1) .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase); await sutProvider.GetDependency().Received(1) @@ -141,7 +157,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling).AdjustSeats(2); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, autoscaling).AdjustSeats(2); sutProvider.GetDependency().SelfHosted.Returns(true); @@ -156,8 +173,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider, Organization organization) { + var plan = StaticStore.GetPlan(organization.PlanType); + organization.UseSecretsManager = false; - var update = new SecretsManagerSubscriptionUpdate(organization, false); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -167,27 +186,16 @@ public class UpdateSecretsManagerSubscriptionCommandTests } [Theory] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] + [BitMemberAutoData(nameof(AllTeamsAndEnterprise))] public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewayCustomerId_ThrowsException( - PlanType planType, + Plan plan, Organization organization, SutProvider sutProvider) { - organization.PlanType = planType; + organization.PlanType = plan.Type; organization.GatewayCustomerId = null; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); + + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("No payment method found.", exception.Message); @@ -195,27 +203,15 @@ public class UpdateSecretsManagerSubscriptionCommandTests } [Theory] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] + [BitMemberAutoData(nameof(AllTeamsAndEnterprise))] public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewaySubscriptionId_ThrowsException( - PlanType planType, + Plan plan, Organization organization, SutProvider sutProvider) { - organization.PlanType = planType; + organization.PlanType = plan.Type; organization.GatewaySubscriptionId = null; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("No subscription found.", exception.Message); @@ -223,24 +219,12 @@ public class UpdateSecretsManagerSubscriptionCommandTests } [Theory] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] - public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(PlanType planType, Guid organizationId, + [BitMemberAutoData(nameof(AllTeamsAndEnterprise))] + public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success( + Plan plan, + Guid organizationId, SutProvider sutProvider) { - var plan = StaticStore.GetPlan(planType); - var organizationSeats = plan.SecretsManager.BaseSeats + 10; var organizationMaxAutoscaleSeats = 20; var organizationServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10; @@ -249,7 +233,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests var organization = new Organization { Id = organizationId, - PlanType = planType, + PlanType = plan.Type, GatewayCustomerId = "1", GatewaySubscriptionId = "2", UseSecretsManager = true, @@ -263,7 +247,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment; var expectedSmServiceAccountsExcludingBase = expectedSmServiceAccounts - plan.SecretsManager.BaseServiceAccount; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(10); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(10); await sutProvider.Sut.UpdateSubscriptionAsync(update); @@ -290,8 +274,9 @@ public class UpdateSecretsManagerSubscriptionCommandTests // Make sure Password Manager seats is greater or equal to Secrets Manager seats organization.Seats = seatCount; + var plan = StaticStore.GetPlan(organization.PlanType); - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = seatCount, MaxAutoscaleSmSeats = seatCount @@ -310,7 +295,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.SmSeats = null; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -325,7 +311,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(-2); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(-2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message); @@ -340,7 +327,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.PlanType = planType; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("You have reached the maximum number of Secrets Manager seats (2) for this plan", @@ -357,7 +345,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmSeats = 9; organization.MaxAutoscaleSmSeats = 10; - var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(2); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message); @@ -370,7 +359,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = organization.SmSeats + 10, MaxAutoscaleSmSeats = organization.SmSeats + 5 @@ -388,7 +378,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = 0, }; @@ -407,7 +398,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.SmSeats = 8; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = 7, }; @@ -425,7 +417,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = 300, MaxAutoscaleSmServiceAccounts = 300 @@ -444,7 +437,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.SmServiceAccounts = null; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Organization has no machine accounts limit, no need to adjust machine accounts", exception.Message); @@ -457,7 +451,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(-2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Cannot use autoscaling to subtract machine accounts.", exception.Message); @@ -472,7 +467,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { organization.PlanType = planType; - var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("You have reached the maximum number of machine accounts (3) for this plan", @@ -489,7 +485,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmServiceAccounts = 9; organization.MaxAutoscaleSmServiceAccounts = 10; - var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2); + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Secrets Manager machine account limit has been reached.", exception.Message); @@ -508,7 +505,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmServiceAccounts = smServiceAccount - 5; organization.MaxAutoscaleSmServiceAccounts = 2 * smServiceAccount; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = smServiceAccount, MaxAutoscaleSmServiceAccounts = maxAutoscaleSmServiceAccounts @@ -530,7 +528,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmServiceAccounts = newSmServiceAccounts - 10; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = newSmServiceAccounts, }; @@ -542,28 +541,16 @@ public class UpdateSecretsManagerSubscriptionCommandTests } [Theory] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] + [BitMemberAutoData(nameof(AllTeamsAndEnterprise))] public async Task UpdateSmServiceAccounts_WhenCurrentServiceAccountsIsGreaterThanNew_ThrowsBadRequestException( - PlanType planType, + Plan plan, Organization organization, SutProvider sutProvider) { var currentServiceAccounts = 301; - organization.PlanType = planType; + organization.PlanType = plan.Type; organization.SmServiceAccounts = currentServiceAccounts; - var update = new SecretsManagerSubscriptionUpdate(organization, false) { SmServiceAccounts = 201 }; + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = 201 }; sutProvider.GetDependency() .GetServiceAccountCountByOrganizationIdAsync(organization.Id) @@ -586,7 +573,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.SmSeats = smSeats - 1; organization.MaxAutoscaleSmSeats = smSeats * 2; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmSeats = smSeats, MaxAutoscaleSmSeats = maxAutoscaleSmSeats @@ -606,7 +594,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests { organization.PlanType = planType; organization.SmSeats = 2; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmSeats = 3 }; @@ -625,7 +614,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests { organization.PlanType = planType; organization.SmSeats = 2; - var update = new SecretsManagerSubscriptionUpdate(organization, false) + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmSeats = 2 }; @@ -645,7 +635,8 @@ public class UpdateSecretsManagerSubscriptionCommandTests organization.PlanType = planType; organization.SmServiceAccounts = 3; - var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 }; + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmServiceAccounts = 3 }; var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Your plan does not allow machine accounts autoscaling.", exception.Message); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 2965a2f03d..8bcee1e8c6 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -43,6 +44,7 @@ public class UpgradeOrganizationPlanCommandTests SutProvider sutProvider) { upgrade.Plan = organization.PlanType; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); @@ -58,6 +60,7 @@ public class UpgradeOrganizationPlanCommandTests upgrade.AdditionalSmSeats = 10; upgrade.AdditionalServiceAccounts = 10; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade)); Assert.Contains("already on this plan", exception.Message); @@ -69,9 +72,11 @@ public class UpgradeOrganizationPlanCommandTests SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); upgrade.AdditionalSmSeats = 10; upgrade.AdditionalSeats = 10; upgrade.Plan = PlanType.TeamsAnnually; + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); await sutProvider.GetDependency().Received(1).ReplaceAndUpdateCacheAsync(organization); } @@ -92,6 +97,8 @@ public class UpgradeOrganizationPlanCommandTests organization.PlanType = PlanType.FamiliesAnnually; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + organizationUpgrade.AdditionalSeats = 30; organizationUpgrade.UseSecretsManager = true; organizationUpgrade.AdditionalSmSeats = 20; @@ -99,6 +106,8 @@ public class UpgradeOrganizationPlanCommandTests organizationUpgrade.AdditionalStorageGb = 3; organizationUpgrade.Plan = planType; + sutProvider.GetDependency().GetPlanOrThrow(organizationUpgrade.Plan).Returns(StaticStore.GetPlan(organizationUpgrade.Plan)); + await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade); await sutProvider.GetDependency().Received(1).AdjustSubscription( organization, @@ -120,7 +129,10 @@ public class UpgradeOrganizationPlanCommandTests public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade, SutProvider sutProvider) { + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); + upgrade.Plan = planType; + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); var plan = StaticStore.GetPlan(upgrade.Plan); @@ -155,8 +167,10 @@ public class UpgradeOrganizationPlanCommandTests upgrade.AdditionalSeats = 15; upgrade.AdditionalSmSeats = 1; upgrade.AdditionalServiceAccounts = 0; + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); organization.SmSeats = 2; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() @@ -181,9 +195,11 @@ public class UpgradeOrganizationPlanCommandTests upgrade.AdditionalSeats = 15; upgrade.AdditionalSmSeats = 1; upgrade.AdditionalServiceAccounts = 0; + sutProvider.GetDependency().GetPlanOrThrow(upgrade.Plan).Returns(StaticStore.GetPlan(upgrade.Plan)); organization.SmSeats = 1; organization.SmServiceAccounts = currentServiceAccounts; + sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() From 1267332b5b589104058de2338818ac9b68db0df9 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 27 Feb 2025 08:34:42 -0600 Subject: [PATCH 35/84] [PM-14406] Security Task Notifications (#5344) * initial commit of `CipherOrganizationPermission_GetManyByUserId` * create queries to get all of the security tasks that are actionable by a user - A task is "actionable" when the user has manage permissions for that cipher * rename query * return the user's email from the query as well * Add email notification for at-risk passwords - Added email layouts for security tasks * add push notification for security tasks * update entity framework to match stored procedure plus testing * update date of migration and remove orderby * add push service to security task controller * rename `SyncSecurityTasksCreated` to `SyncNotification` * remove duplicate return * remove unused directive * remove unneeded new notification type * use `createNotificationCommand` to alert all platforms * return the cipher id that is associated with the security task and store the security task id on the notification entry * Add `TaskId` to the output model of `GetUserSecurityTasksByCipherIdsAsync` * move notification logic to command * use TaskId from `_getSecurityTasksNotificationDetailsQuery` * add service * only push last notification for each user * formatting * refactor `CreateNotificationCommand` parameter to `sendPush` * flip boolean in test * update interface to match usage * do not push any of the security related notifications to the user * add `PendingSecurityTasks` push type * add push notification for pending security tasks --- .../Controllers/SecurityTaskController.cs | 8 +- src/Core/Enums/PushType.cs | 4 +- .../Handlebars/Layouts/SecurityTasks.html.hbs | 61 ++++++ .../Handlebars/Layouts/SecurityTasks.text.hbs | 12 ++ .../SecurityTasksNotification.html.hbs | 28 +++ .../SecurityTasksNotification.text.hbs | 8 + .../Mail/SecurityTaskNotificationViewModel.cs | 12 ++ .../Commands/CreateNotificationCommand.cs | 7 +- .../Interfaces/ICreateNotificationCommand.cs | 2 +- .../NotificationHubPushNotificationService.cs | 5 + .../AzureQueuePushNotificationService.cs | 5 + .../Push/Services/IPushNotificationService.cs | 1 + .../MultiServicePushNotificationService.cs | 6 + .../Services/NoopPushNotificationService.cs | 5 + ...NotificationsApiPushNotificationService.cs | 5 + .../Services/RelayPushNotificationService.cs | 5 + src/Core/Services/IMailService.cs | 3 +- .../Implementations/HandlebarsMailService.cs | 24 ++- .../NoopImplementations/NoopMailService.cs | 7 +- .../CreateManyTaskNotificationsCommand.cs | 82 ++++++++ .../ICreateManyTaskNotificationsCommand.cs | 13 ++ .../Vault/Models/Data/UserCipherForTask.cs | 23 +++ .../Models/Data/UserSecurityTaskCipher.cs | 27 +++ .../Models/Data/UserSecurityTasksCount.cs | 22 +++ ...etSecurityTasksNotificationDetailsQuery.cs | 33 ++++ ...etSecurityTasksNotificationDetailsQuery.cs | 16 ++ .../Vault/Repositories/ICipherRepository.cs | 8 + .../Vault/VaultServiceCollectionExtensions.cs | 2 + .../Vault/Repositories/CipherRepository.cs | 22 +++ .../Vault/Repositories/CipherRepository.cs | 45 +++++ .../UserSecurityTasksByCipherIdsQuery.cs | 71 +++++++ .../UserSecurityTasks_GetManyByCipherIds.sql | 67 +++++++ .../Commands/CreateNotificationCommandTest.cs | 15 ++ .../Repositories/CipherRepositoryTests.cs | 179 ++++++++++++++++++ ...0_UserSecurityTasks_GetManyByCipherIds.sql | 68 +++++++ 35 files changed, 893 insertions(+), 8 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs create mode 100644 src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs create mode 100644 src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs create mode 100644 src/Core/Vault/Commands/Interfaces/ICreateManyTaskNotificationsCommand.cs create mode 100644 src/Core/Vault/Models/Data/UserCipherForTask.cs create mode 100644 src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs create mode 100644 src/Core/Vault/Models/Data/UserSecurityTasksCount.cs create mode 100644 src/Core/Vault/Queries/GetSecurityTasksNotificationDetailsQuery.cs create mode 100644 src/Core/Vault/Queries/IGetSecurityTasksNotificationDetailsQuery.cs create mode 100644 src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs create mode 100644 src/Sql/Vault/dbo/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql create mode 100644 util/Migrator/DbScripts/2025-02-11_00_UserSecurityTasks_GetManyByCipherIds.sql diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 88b7aed9c6..2693d60825 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -22,19 +22,22 @@ public class SecurityTaskController : Controller private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery; private readonly ICreateManyTasksCommand _createManyTasksCommand; + private readonly ICreateManyTaskNotificationsCommand _createManyTaskNotificationsCommand; public SecurityTaskController( IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, IMarkTaskAsCompleteCommand markTaskAsCompleteCommand, IGetTasksForOrganizationQuery getTasksForOrganizationQuery, - ICreateManyTasksCommand createManyTasksCommand) + ICreateManyTasksCommand createManyTasksCommand, + ICreateManyTaskNotificationsCommand createManyTaskNotificationsCommand) { _userService = userService; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; _markTaskAsCompleteCommand = markTaskAsCompleteCommand; _getTasksForOrganizationQuery = getTasksForOrganizationQuery; _createManyTasksCommand = createManyTasksCommand; + _createManyTaskNotificationsCommand = createManyTaskNotificationsCommand; } /// @@ -87,6 +90,9 @@ public class SecurityTaskController : Controller [FromBody] BulkCreateSecurityTasksRequestModel model) { var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks); + + await _createManyTaskNotificationsCommand.CreateAsync(orgId, securityTasks); + var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); return new ListResponseModel(response); } diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs index 6d0dd9393c..96a1192478 100644 --- a/src/Core/Enums/PushType.cs +++ b/src/Core/Enums/PushType.cs @@ -29,5 +29,7 @@ public enum PushType : byte SyncOrganizationCollectionSettingChanged = 19, Notification = 20, - NotificationStatus = 21 + NotificationStatus = 21, + + PendingSecurityTasks = 22 } diff --git a/src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.html.hbs new file mode 100644 index 0000000000..930d39eeee --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.html.hbs @@ -0,0 +1,61 @@ +{{#>FullUpdatedHtmlLayout}} + + + + + +
+ + + + +
+ {{OrgName}} has identified {{TaskCount}} critical login{{#if TaskCountPlural}}s{{/if}} that require{{#unless + TaskCountPlural}}s{{/unless}} a + password change +
+
+ +
+ +{{>@partial-block}} + + + + + + +
+ + + + + +
+{{/FullUpdatedHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.text.hbs b/src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.text.hbs new file mode 100644 index 0000000000..f9befac46c --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.text.hbs @@ -0,0 +1,12 @@ +{{#>FullTextLayout}} +{{OrgName}} has identified {{TaskCount}} critical login{{#if TaskCountPlural}}s{{/if}} that require{{#unless +TaskCountPlural}}s{{/unless}} a +password change + +{{>@partial-block}} + +We’re here for you! +If you have any questions, search the Bitwarden Help site or contact us. +- https://bitwarden.com/help/ +- https://bitwarden.com/contact/ +{{/FullTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs new file mode 100644 index 0000000000..039806f44b --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs @@ -0,0 +1,28 @@ +{{#>SecurityTasksHtmlLayout}} + + + + + + + +
+ Keep you and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a + data breach. +
+ Launch the Bitwarden extension to review your at-risk passwords. +
+ + + + +
+ + Review at-risk passwords + +
+{{/SecurityTasksHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs new file mode 100644 index 0000000000..ba8650ad10 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs @@ -0,0 +1,8 @@ +{{#>SecurityTasksHtmlLayout}} +Keep you and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a data +breach. + +Launch the Bitwarden extension to review your at-risk passwords. + +Review at-risk passwords ({{{ReviewPasswordsUrl}}}) +{{/SecurityTasksHtmlLayout}} diff --git a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs new file mode 100644 index 0000000000..7f93ac2439 --- /dev/null +++ b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Mail; + +public class SecurityTaskNotificationViewModel : BaseMailModel +{ + public string OrgName { get; set; } + + public int TaskCount { get; set; } + + public bool TaskCountPlural => TaskCount != 1; + + public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt"; +} diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs index 3fddafcdc7..e6eec3f4a8 100644 --- a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs +++ b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs @@ -28,7 +28,7 @@ public class CreateNotificationCommand : ICreateNotificationCommand _pushNotificationService = pushNotificationService; } - public async Task CreateAsync(Notification notification) + public async Task CreateAsync(Notification notification, bool sendPush = true) { notification.CreationDate = notification.RevisionDate = DateTime.UtcNow; @@ -37,7 +37,10 @@ public class CreateNotificationCommand : ICreateNotificationCommand var newNotification = await _notificationRepository.CreateAsync(notification); - await _pushNotificationService.PushNotificationAsync(newNotification); + if (sendPush) + { + await _pushNotificationService.PushNotificationAsync(newNotification); + } return newNotification; } diff --git a/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs index a3b4d894e6..cacd69c8ad 100644 --- a/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs +++ b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs @@ -5,5 +5,5 @@ namespace Bit.Core.NotificationCenter.Commands.Interfaces; public interface ICreateNotificationCommand { - Task CreateAsync(Notification notification); + Task CreateAsync(Notification notification, bool sendPush = true); } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index 6bc5b0db6b..a28b21f465 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -329,6 +329,11 @@ public class NotificationHubPushNotificationService : IPushNotificationService GetContextIdentifier(excludeCurrentContext), clientType: clientType); } + public async Task PushPendingSecurityTasksAsync(Guid userId) + { + await PushUserAsync(userId, PushType.PendingSecurityTasks); + } + public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index f88c0641c5..e61dd15f0d 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -219,6 +219,11 @@ public class AzureQueuePushNotificationService : IPushNotificationService await SendMessageAsync(PushType.NotificationStatus, message, true); } + public async Task PushPendingSecurityTasksAsync(Guid userId) + { + await PushUserAsync(userId, PushType.PendingSecurityTasks); + } + private async Task PushSendAsync(Send send, PushType type) { if (send.UserId.HasValue) diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index d0f18cd8ac..60f3c35089 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -38,4 +38,5 @@ public interface IPushNotificationService string? deviceId = null, ClientType? clientType = null); Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null); + Task PushPendingSecurityTasksAsync(Guid userId); } diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs index 1f88f5dcc6..490b690a3b 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs @@ -179,6 +179,12 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.FromResult(0); } + public Task PushPendingSecurityTasksAsync(Guid userId) + { + PushToServices((s) => s.PushPendingSecurityTasksAsync(userId)); + return Task.CompletedTask; + } + private void PushToServices(Func pushFunc) { if (!_services.Any()) diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs index e005f9d7af..6e7278cf94 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NoopPushNotificationService.cs @@ -121,4 +121,9 @@ public class NoopPushNotificationService : IPushNotificationService { return Task.FromResult(0); } + + public Task PushPendingSecurityTasksAsync(Guid userId) + { + return Task.FromResult(0); + } } diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index 2833c43985..53a0de9a27 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -232,6 +232,11 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService await SendMessageAsync(PushType.NotificationStatus, message, true); } + public async Task PushPendingSecurityTasksAsync(Guid userId) + { + await PushUserAsync(userId, PushType.PendingSecurityTasks); + } + private async Task PushSendAsync(Send send, PushType type) { if (send.UserId.HasValue) diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index d111efa2a8..53f5835322 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -300,6 +300,11 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti false ); + public async Task PushPendingSecurityTasksAsync(Guid userId) + { + await PushUserAsync(userId, PushType.PendingSecurityTasks); + } + private async Task SendPayloadToInstallationAsync(PushType type, object payload, bool excludeCurrentContext, ClientType? clientType = null) { diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 3492ada838..b0b884eb3e 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -5,6 +5,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; +using Bit.Core.Vault.Models.Data; namespace Bit.Core.Services; @@ -98,5 +99,5 @@ public interface IMailService string organizationName); Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList); Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable adminEmails, Guid organizationId, string email, string userName); + Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons); } - diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 44be3bfdf4..c598a9d432 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -15,6 +15,7 @@ using Bit.Core.Models.Mail.Provider; using Bit.Core.SecretsManager.Models.Mail; using Bit.Core.Settings; using Bit.Core.Utilities; +using Bit.Core.Vault.Models.Data; using HandlebarsDotNet; namespace Bit.Core.Services; @@ -654,6 +655,10 @@ public class HandlebarsMailService : IMailService Handlebars.RegisterTemplate("TitleContactUsHtmlLayout", titleContactUsHtmlLayoutSource); var titleContactUsTextLayoutSource = await ReadSourceAsync("Layouts.TitleContactUs.text"); Handlebars.RegisterTemplate("TitleContactUsTextLayout", titleContactUsTextLayoutSource); + var securityTasksHtmlLayoutSource = await ReadSourceAsync("Layouts.SecurityTasks.html"); + Handlebars.RegisterTemplate("SecurityTasksHtmlLayout", securityTasksHtmlLayoutSource); + var securityTasksTextLayoutSource = await ReadSourceAsync("Layouts.SecurityTasks.text"); + Handlebars.RegisterTemplate("SecurityTasksTextLayout", securityTasksTextLayoutSource); Handlebars.RegisterHelper("date", (writer, context, parameters) => { @@ -1196,9 +1201,26 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons) + { + MailQueueMessage CreateMessage(UserSecurityTasksCount notification) + { + var message = CreateDefaultMessage($"{orgName} has identified {notification.TaskCount} at-risk password{(notification.TaskCount.Equals(1) ? "" : "s")}", notification.Email); + var model = new SecurityTaskNotificationViewModel + { + OrgName = orgName, + TaskCount = notification.TaskCount, + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + }; + message.Category = "SecurityTasksNotification"; + return new MailQueueMessage(message, "SecurityTasksNotification", model); + } + var messageModels = securityTaskNotificaitons.Select(CreateMessage); + await EnqueueMailAsync(messageModels.ToList()); + } + private static string GetUserIdentifier(string email, string userName) { return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false); } } - diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 9984f8ee90..5fba545903 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -5,6 +5,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; +using Bit.Core.Vault.Models.Data; namespace Bit.Core.Services; @@ -322,5 +323,9 @@ public class NoopMailService : IMailService { return Task.FromResult(0); } -} + public Task SendBulkSecurityTaskNotificationsAsync(string orgName, IEnumerable securityTaskNotificaitons) + { + return Task.FromResult(0); + } +} diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs new file mode 100644 index 0000000000..58b5f65e0f --- /dev/null +++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs @@ -0,0 +1,82 @@ +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; + +public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCommand +{ + private readonly IGetSecurityTasksNotificationDetailsQuery _getSecurityTasksNotificationDetailsQuery; + private readonly IOrganizationRepository _organizationRepository; + private readonly IMailService _mailService; + private readonly ICreateNotificationCommand _createNotificationCommand; + private readonly IPushNotificationService _pushNotificationService; + + public CreateManyTaskNotificationsCommand( + IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery, + IOrganizationRepository organizationRepository, + IMailService mailService, + ICreateNotificationCommand createNotificationCommand, + IPushNotificationService pushNotificationService) + { + _getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery; + _organizationRepository = organizationRepository; + _mailService = mailService; + _createNotificationCommand = createNotificationCommand; + _pushNotificationService = pushNotificationService; + } + + public async Task CreateAsync(Guid orgId, IEnumerable securityTasks) + { + var securityTaskCiphers = await _getSecurityTasksNotificationDetailsQuery.GetNotificationDetailsByManyIds(orgId, securityTasks); + + // Get the number of tasks for each user + var userTaskCount = securityTaskCiphers.GroupBy(x => x.UserId).Select(x => new UserSecurityTasksCount + { + UserId = x.Key, + Email = x.First().Email, + TaskCount = x.Count() + }).ToList(); + + var organization = await _organizationRepository.GetByIdAsync(orgId); + + await _mailService.SendBulkSecurityTaskNotificationsAsync(organization.Name, userTaskCount); + + // Break securityTaskCiphers into separate lists by user Id + var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var userId in securityTaskCiphersByUser.Keys) + { + // Get the security tasks by the user Id + var userSecurityTaskCiphers = securityTaskCiphersByUser[userId]; + + // Process each user's security task ciphers + for (int i = 0; i < userSecurityTaskCiphers.Count; i++) + { + var userSecurityTaskCipher = userSecurityTaskCiphers[i]; + + // Create a notification for the user with the associated task + var notification = new Notification + { + UserId = userSecurityTaskCipher.UserId, + OrganizationId = orgId, + Priority = Priority.Informational, + ClientType = ClientType.Browser, + TaskId = userSecurityTaskCipher.TaskId + }; + + await _createNotificationCommand.CreateAsync(notification, false); + } + + // Notify the user that they have pending security tasks + await _pushNotificationService.PushPendingSecurityTasksAsync(userId); + } + } +} diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTaskNotificationsCommand.cs new file mode 100644 index 0000000000..465d9c6fee --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTaskNotificationsCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.Vault.Entities; + +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface ICreateManyTaskNotificationsCommand +{ + /// + /// Creates email and push notifications for the given security tasks. + /// + /// The organization Id + /// All applicable security tasks + Task CreateAsync(Guid organizationId, IEnumerable securityTasks); +} diff --git a/src/Core/Vault/Models/Data/UserCipherForTask.cs b/src/Core/Vault/Models/Data/UserCipherForTask.cs new file mode 100644 index 0000000000..3ddaa141b1 --- /dev/null +++ b/src/Core/Vault/Models/Data/UserCipherForTask.cs @@ -0,0 +1,23 @@ +namespace Bit.Core.Vault.Models.Data; + +/// +/// Minimal data model that represents a User and the associated cipher for a security task. +/// Only to be used for query responses. For full data model, . +/// +public class UserCipherForTask +{ + /// + /// The user's Id. + /// + public Guid UserId { get; set; } + + /// + /// The user's email. + /// + public string Email { get; set; } + + /// + /// The cipher Id of the security task. + /// + public Guid CipherId { get; set; } +} diff --git a/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs b/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs new file mode 100644 index 0000000000..20e59ec4f7 --- /dev/null +++ b/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs @@ -0,0 +1,27 @@ +namespace Bit.Core.Vault.Models.Data; + +/// +/// Data model that represents a User and the associated cipher for a security task. +/// +public class UserSecurityTaskCipher +{ + /// + /// The user's Id. + /// + public Guid UserId { get; set; } + + /// + /// The user's email. + /// + public string Email { get; set; } + + /// + /// The cipher Id of the security task. + /// + public Guid CipherId { get; set; } + + /// + /// The Id of the security task. + /// + public Guid TaskId { get; set; } +} diff --git a/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs b/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs new file mode 100644 index 0000000000..c8d2707db6 --- /dev/null +++ b/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.Vault.Models.Data; + +/// +/// Data model that represents a User and the amount of actionable security tasks. +/// +public class UserSecurityTasksCount +{ + /// + /// The user's Id. + /// + public Guid UserId { get; set; } + + /// + /// The user's email. + /// + public string Email { get; set; } + + /// + /// The number of actionable security tasks for the respective users. + /// + public int TaskCount { get; set; } +} diff --git a/src/Core/Vault/Queries/GetSecurityTasksNotificationDetailsQuery.cs b/src/Core/Vault/Queries/GetSecurityTasksNotificationDetailsQuery.cs new file mode 100644 index 0000000000..00104f1919 --- /dev/null +++ b/src/Core/Vault/Queries/GetSecurityTasksNotificationDetailsQuery.cs @@ -0,0 +1,33 @@ +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; + +namespace Bit.Core.Vault.Queries; + +public class GetSecurityTasksNotificationDetailsQuery : IGetSecurityTasksNotificationDetailsQuery +{ + private readonly ICurrentContext _currentContext; + private readonly ICipherRepository _cipherRepository; + + public GetSecurityTasksNotificationDetailsQuery(ICurrentContext currentContext, ICipherRepository cipherRepository) + { + _currentContext = currentContext; + _cipherRepository = cipherRepository; + } + + public async Task> GetNotificationDetailsByManyIds(Guid organizationId, IEnumerable tasks) + { + var org = _currentContext.GetOrganization(organizationId); + + if (org == null) + { + throw new NotFoundException(); + } + + var userSecurityTaskCiphers = await _cipherRepository.GetUserSecurityTasksByCipherIdsAsync(organizationId, tasks); + + return userSecurityTaskCiphers; + } +} diff --git a/src/Core/Vault/Queries/IGetSecurityTasksNotificationDetailsQuery.cs b/src/Core/Vault/Queries/IGetSecurityTasksNotificationDetailsQuery.cs new file mode 100644 index 0000000000..df81765817 --- /dev/null +++ b/src/Core/Vault/Queries/IGetSecurityTasksNotificationDetailsQuery.cs @@ -0,0 +1,16 @@ +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Vault.Queries; + +public interface IGetSecurityTasksNotificationDetailsQuery +{ + /// + /// Retrieves all users within the given organization that are applicable to the given security tasks. + /// + /// + /// + /// A dictionary of UserIds and the corresponding amount of security tasks applicable to them. + /// + public Task> GetNotificationDetailsByManyIds(Guid organizationId, IEnumerable tasks); +} diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 2950cb99c2..b094b42044 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -4,6 +4,7 @@ using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; + namespace Bit.Core.Vault.Repositories; public interface ICipherRepository : IRepository @@ -49,6 +50,13 @@ public interface ICipherRepository : IRepository Task> GetCipherPermissionsForOrganizationAsync(Guid organizationId, Guid userId); + /// + /// Returns the users and the cipher ids for security tawsks that are applicable to them. + /// + /// Security tasks are actionable when a user has manage access to the associated cipher. + /// + Task> GetUserSecurityTasksByCipherIdsAsync(Guid organizationId, IEnumerable tasks); + /// /// Updates encrypted data for ciphers during a key rotation /// diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index fcb9259135..1f361cb613 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -21,6 +21,8 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); } } diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index b8304fbbb0..b85f1991f7 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -323,6 +323,28 @@ public class CipherRepository : Repository, ICipherRepository } } + public async Task> GetUserSecurityTasksByCipherIdsAsync( + Guid organizationId, IEnumerable tasks) + { + var cipherIds = tasks.Where(t => t.CipherId.HasValue).Select(t => t.CipherId.Value).Distinct().ToList(); + using (var connection = new SqlConnection(ConnectionString)) + { + + var results = await connection.QueryAsync( + $"[{Schema}].[UserSecurityTasks_GetManyByCipherIds]", + new { OrganizationId = organizationId, CipherIds = cipherIds.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.Select(r => new UserSecurityTaskCipher + { + UserId = r.UserId, + Email = r.Email, + CipherId = r.CipherId, + TaskId = tasks.First(t => t.CipherId == r.CipherId).Id + }).ToList(); + } + } + /// public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( Guid userId, IEnumerable ciphers) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 9c91609b1b..e4930cb795 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -348,6 +348,51 @@ public class CipherRepository : Repository> GetUserSecurityTasksByCipherIdsAsync(Guid organizationId, IEnumerable tasks) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var cipherIds = tasks.Where(t => t.CipherId.HasValue).Select(t => t.CipherId.Value); + var dbContext = GetDatabaseContext(scope); + var query = new UserSecurityTasksByCipherIdsQuery(organizationId, cipherIds).Run(dbContext); + + ICollection userTaskCiphers; + + // SQLite does not support the GROUP BY clause + if (dbContext.Database.IsSqlite()) + { + userTaskCiphers = (await query.ToListAsync()) + .GroupBy(c => new { c.UserId, c.Email, c.CipherId }) + .Select(g => new UserSecurityTaskCipher + { + UserId = g.Key.UserId, + Email = g.Key.Email, + CipherId = g.Key.CipherId, + }).ToList(); + } + else + { + var groupByQuery = from p in query + group p by new { p.UserId, p.Email, p.CipherId } + into g + select new UserSecurityTaskCipher + { + UserId = g.Key.UserId, + CipherId = g.Key.CipherId, + Email = g.Key.Email, + }; + userTaskCiphers = await groupByQuery.ToListAsync(); + } + + foreach (var userTaskCipher in userTaskCiphers) + { + userTaskCipher.TaskId = tasks.First(t => t.CipherId == userTaskCipher.CipherId).Id; + } + + return userTaskCiphers; + } + } + public async Task GetByIdAsync(Guid id, Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs new file mode 100644 index 0000000000..c36c0d87c4 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs @@ -0,0 +1,71 @@ +using Bit.Core.Vault.Models.Data; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; + +public class UserSecurityTasksByCipherIdsQuery : IQuery +{ + private readonly Guid _organizationId; + private readonly IEnumerable _cipherIds; + + public UserSecurityTasksByCipherIdsQuery(Guid organizationId, IEnumerable cipherIds) + { + _organizationId = organizationId; + _cipherIds = cipherIds; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var baseCiphers = + from c in dbContext.Ciphers + where _cipherIds.Contains(c.Id) + join o in dbContext.Organizations + on c.OrganizationId equals o.Id + where o.Id == _organizationId && o.Enabled + select c; + + var userPermissions = + from c in baseCiphers + join cc in dbContext.CollectionCiphers + on c.Id equals cc.CipherId + join cu in dbContext.CollectionUsers + on cc.CollectionId equals cu.CollectionId + join ou in dbContext.OrganizationUsers + on cu.OrganizationUserId equals ou.Id + where ou.OrganizationId == _organizationId + && cu.Manage == true + select new { ou.UserId, c.Id }; + + var groupPermissions = + from c in baseCiphers + join cc in dbContext.CollectionCiphers + on c.Id equals cc.CipherId + join cg in dbContext.CollectionGroups + on cc.CollectionId equals cg.CollectionId + join gu in dbContext.GroupUsers + on cg.GroupId equals gu.GroupId + join ou in dbContext.OrganizationUsers + on gu.OrganizationUserId equals ou.Id + where ou.OrganizationId == _organizationId + && cg.Manage == true + && !userPermissions.Any(up => up.Id == c.Id && up.UserId == ou.UserId) + select new { ou.UserId, c.Id }; + + return userPermissions.Union(groupPermissions) + .Join( + dbContext.Users, + p => p.UserId, + u => u.Id, + (p, u) => new { p.UserId, p.Id, u.Email } + ) + .GroupBy(x => new { x.UserId, x.Email, x.Id }) + .Select(g => new UserCipherForTask + { + UserId = (Guid)g.Key.UserId, + Email = g.Key.Email, + CipherId = g.Key.Id + }) + .OrderByDescending(x => x.Email); + } +} diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql new file mode 100644 index 0000000000..be39ee9eb6 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql @@ -0,0 +1,67 @@ +CREATE PROCEDURE [dbo].[UserSecurityTasks_GetManyByCipherIds] + @OrganizationId UNIQUEIDENTIFIER, + @CipherIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + ;WITH BaseCiphers AS ( + SELECT C.[Id], C.[OrganizationId] + FROM [dbo].[Cipher] C + INNER JOIN @CipherIds CI ON C.[Id] = CI.[Id] + INNER JOIN [dbo].[Organization] O ON + O.[Id] = C.[OrganizationId] + AND O.[Id] = @OrganizationId + AND O.[Enabled] = 1 + ), + UserPermissions AS ( + SELECT DISTINCT + CC.[CipherId], + OU.[UserId], + COALESCE(CU.[Manage], 0) as [Manage] + FROM [dbo].[CollectionCipher] CC + INNER JOIN [dbo].[CollectionUser] CU ON + CU.[CollectionId] = CC.[CollectionId] + INNER JOIN [dbo].[OrganizationUser] OU ON + CU.[OrganizationUserId] = OU.[Id] + AND OU.[OrganizationId] = @OrganizationId + WHERE COALESCE(CU.[Manage], 0) = 1 + ), + GroupPermissions AS ( + SELECT DISTINCT + CC.[CipherId], + OU.[UserId], + COALESCE(CG.[Manage], 0) as [Manage] + FROM [dbo].[CollectionCipher] CC + INNER JOIN [dbo].[CollectionGroup] CG ON + CG.[CollectionId] = CC.[CollectionId] + INNER JOIN [dbo].[GroupUser] GU ON + GU.[GroupId] = CG.[GroupId] + INNER JOIN [dbo].[OrganizationUser] OU ON + GU.[OrganizationUserId] = OU.[Id] + AND OU.[OrganizationId] = @OrganizationId + WHERE COALESCE(CG.[Manage], 0) = 1 + AND NOT EXISTS ( + SELECT 1 + FROM UserPermissions UP + WHERE UP.[CipherId] = CC.[CipherId] + AND UP.[UserId] = OU.[UserId] + ) + ), + CombinedPermissions AS ( + SELECT CipherId, UserId, [Manage] + FROM UserPermissions + UNION + SELECT CipherId, UserId, [Manage] + FROM GroupPermissions + ) + SELECT + P.[UserId], + U.[Email], + C.[Id] as CipherId + FROM BaseCiphers C + INNER JOIN CombinedPermissions P ON P.CipherId = C.[Id] + INNER JOIN [dbo].[User] U ON U.[Id] = P.[UserId] + WHERE P.[Manage] = 1 + ORDER BY U.[Email], C.[Id] +END diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs index 3256f2f9cb..3c67cceb2e 100644 --- a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs @@ -69,4 +69,19 @@ public class CreateNotificationCommandTest .Received(0) .PushNotificationStatusAsync(Arg.Any(), Arg.Any()); } + + [Theory] + [BitAutoData] + public async Task CreateAsync_Authorized_NotificationPushSkipped( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification, true); + + var newNotification = await sutProvider.Sut.CreateAsync(notification, false); + + await sutProvider.GetDependency() + .Received(0) + .PushNotificationAsync(newNotification); + } } diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index b64a1ded76..6f02740cf5 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -704,4 +704,183 @@ public class CipherRepositoryTests Data = "" }); } + + [DatabaseTheory, DatabaseData] + public async Task GetUserSecurityTasksByCipherIdsAsync_Works( + ICipherRepository cipherRepository, + IUserRepository userRepository, + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository + ) + { + // Users + var user1 = await userRepository.CreateAsync(new User + { + Name = "Test User 1", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var user2 = await userRepository.CreateAsync(new User + { + Name = "Test User 2", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + // Organization + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = user1.Email, + Plan = "Test" + }); + + // Org Users + var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + UserId = user1.Id, + OrganizationId = organization.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }); + + var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + UserId = user2.Id, + OrganizationId = organization.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + }); + + // A group that will be assigned Edit permissions to any collections + var editGroup = await groupRepository.CreateAsync(new Group + { + OrganizationId = organization.Id, + Name = "Edit Group", + }); + await groupRepository.UpdateUsersAsync(editGroup.Id, new[] { orgUser1.Id }); + + // Add collections to Org + var manageCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Manage Collection", + OrganizationId = organization.Id + }); + + // Use a 2nd collection to differentiate between the two users + var manageCollection2 = await collectionRepository.CreateAsync(new Collection + { + Name = "Manage Collection 2", + OrganizationId = organization.Id + }); + var viewOnlyCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "View Only Collection", + OrganizationId = organization.Id + }); + + // Ciphers + var manageCipher1 = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var manageCipher2 = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var viewOnlyCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher1.Id, organization.Id, + new List { manageCollection.Id }); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher2.Id, organization.Id, + new List { manageCollection2.Id }); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id, + new List { viewOnlyCollection.Id }); + + await collectionRepository.UpdateUsersAsync(manageCollection.Id, new List + { + new() + { + Id = orgUser1.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + new() + { + Id = orgUser2.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + } + }); + + // Only add second user to the second manage collection + await collectionRepository.UpdateUsersAsync(manageCollection2.Id, new List + { + new() + { + Id = orgUser2.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + }); + + await collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id, new List + { + new() + { + Id = orgUser1.Id, + HidePasswords = false, + ReadOnly = false, + Manage = false + } + }); + + var securityTasks = new List + { + new SecurityTask { CipherId = manageCipher1.Id, Id = Guid.NewGuid() }, + new SecurityTask { CipherId = manageCipher2.Id, Id = Guid.NewGuid() }, + new SecurityTask { CipherId = viewOnlyCipher.Id, Id = Guid.NewGuid() } + }; + + var userSecurityTaskCiphers = await cipherRepository.GetUserSecurityTasksByCipherIdsAsync(organization.Id, securityTasks); + + Assert.NotEmpty(userSecurityTaskCiphers); + Assert.Equal(3, userSecurityTaskCiphers.Count); + + var user1TaskCiphers = userSecurityTaskCiphers.Where(t => t.UserId == user1.Id); + Assert.Single(user1TaskCiphers); + Assert.Equal(user1.Email, user1TaskCiphers.First().Email); + Assert.Equal(user1.Id, user1TaskCiphers.First().UserId); + Assert.Equal(manageCipher1.Id, user1TaskCiphers.First().CipherId); + + var user2TaskCiphers = userSecurityTaskCiphers.Where(t => t.UserId == user2.Id); + Assert.NotNull(user2TaskCiphers); + Assert.Equal(2, user2TaskCiphers.Count()); + Assert.Equal(user2.Email, user2TaskCiphers.Last().Email); + Assert.Equal(user2.Id, user2TaskCiphers.Last().UserId); + Assert.Contains(user2TaskCiphers, t => t.CipherId == manageCipher1.Id && t.TaskId == securityTasks[0].Id); + Assert.Contains(user2TaskCiphers, t => t.CipherId == manageCipher2.Id && t.TaskId == securityTasks[1].Id); + } } diff --git a/util/Migrator/DbScripts/2025-02-11_00_UserSecurityTasks_GetManyByCipherIds.sql b/util/Migrator/DbScripts/2025-02-11_00_UserSecurityTasks_GetManyByCipherIds.sql new file mode 100644 index 0000000000..6d16f77161 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-11_00_UserSecurityTasks_GetManyByCipherIds.sql @@ -0,0 +1,68 @@ +CREATE OR ALTER PROCEDURE [dbo].[UserSecurityTasks_GetManyByCipherIds] + @OrganizationId UNIQUEIDENTIFIER, + @CipherIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + ;WITH BaseCiphers AS ( + SELECT C.[Id], C.[OrganizationId] + FROM [dbo].[Cipher] C + INNER JOIN @CipherIds CI ON C.[Id] = CI.[Id] + INNER JOIN [dbo].[Organization] O ON + O.[Id] = C.[OrganizationId] + AND O.[Id] = @OrganizationId + AND O.[Enabled] = 1 + ), + UserPermissions AS ( + SELECT DISTINCT + CC.[CipherId], + OU.[UserId], + COALESCE(CU.[Manage], 0) as [Manage] + FROM [dbo].[CollectionCipher] CC + INNER JOIN [dbo].[CollectionUser] CU ON + CU.[CollectionId] = CC.[CollectionId] + INNER JOIN [dbo].[OrganizationUser] OU ON + CU.[OrganizationUserId] = OU.[Id] + AND OU.[OrganizationId] = @OrganizationId + WHERE COALESCE(CU.[Manage], 0) = 1 + ), + GroupPermissions AS ( + SELECT DISTINCT + CC.[CipherId], + OU.[UserId], + COALESCE(CG.[Manage], 0) as [Manage] + FROM [dbo].[CollectionCipher] CC + INNER JOIN [dbo].[CollectionGroup] CG ON + CG.[CollectionId] = CC.[CollectionId] + INNER JOIN [dbo].[GroupUser] GU ON + GU.[GroupId] = CG.[GroupId] + INNER JOIN [dbo].[OrganizationUser] OU ON + GU.[OrganizationUserId] = OU.[Id] + AND OU.[OrganizationId] = @OrganizationId + WHERE COALESCE(CG.[Manage], 0) = 1 + AND NOT EXISTS ( + SELECT 1 + FROM UserPermissions UP + WHERE UP.[CipherId] = CC.[CipherId] + AND UP.[UserId] = OU.[UserId] + ) + ), + CombinedPermissions AS ( + SELECT CipherId, UserId, [Manage] + FROM UserPermissions + UNION + SELECT CipherId, UserId, [Manage] + FROM GroupPermissions + ) + SELECT + P.[UserId], + U.[Email], + C.[Id] as CipherId + FROM BaseCiphers C + INNER JOIN CombinedPermissions P ON P.CipherId = C.[Id] + INNER JOIN [dbo].[User] U ON U.[Id] = P.[UserId] + WHERE P.[Manage] = 1 + ORDER BY U.[Email], C.[Id] +END +GO From 4c5bf495f31f42036d492b088535b28590037aa1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:54:28 -0500 Subject: [PATCH 36/84] [deps] Auth: Update Duende.IdentityServer to 7.1.0 (#5293) * [deps] Auth: Update Duende.IdentityServer to 7.1.0 * fix(identity): fixing name space for Identity 7.1.0 update * fix: formatting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike Kottlowski --- bitwarden_license/src/Scim/Startup.cs | 2 +- .../src/Scim/Utilities/ApiKeyAuthenticationHandler.cs | 2 +- bitwarden_license/src/Sso/Controllers/AccountController.cs | 2 +- .../src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs | 2 +- src/Api/Startup.cs | 2 +- src/Core/Core.csproj | 2 +- src/Core/Services/Implementations/LicensingService.cs | 2 +- src/Core/Utilities/CoreHelpers.cs | 2 +- src/Events/Startup.cs | 2 +- src/Identity/Controllers/SsoController.cs | 2 +- src/Identity/IdentityServer/ApiResources.cs | 2 +- src/Identity/IdentityServer/ClientStore.cs | 2 +- .../RequestValidators/CustomTokenRequestValidator.cs | 2 +- src/Notifications/Startup.cs | 2 +- src/Notifications/SubjectUserIdProvider.cs | 2 +- src/SharedWeb/Utilities/ServiceCollectionExtensions.cs | 2 +- test/Core.Test/Utilities/CoreHelpersTests.cs | 2 +- .../Endpoints/IdentityServerSsoTests.cs | 2 +- .../Endpoints/IdentityServerTwoFactorTests.cs | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index 3fac669eda..edbbf34aea 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -8,7 +8,7 @@ using Bit.Core.Utilities; using Bit.Scim.Context; using Bit.Scim.Utilities; using Bit.SharedWeb.Utilities; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.Extensions.DependencyInjection.Extensions; using Stripe; diff --git a/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs b/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs index 4e7e7ceb7a..6ebffb73cd 100644 --- a/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs +++ b/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs @@ -3,7 +3,7 @@ using System.Text.Encodings.Web; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Scim.Context; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index f41d2d3c65..ada6b20c29 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -19,10 +19,10 @@ using Bit.Core.Tokens; using Bit.Core.Utilities; using Bit.Sso.Models; using Bit.Sso.Utilities; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Services; using Duende.IdentityServer.Stores; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; diff --git a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs index 8bde8f84a1..804a323109 100644 --- a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs +++ b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs @@ -7,9 +7,9 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Sso.Models; using Bit.Sso.Utilities; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Infrastructure; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Options; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index a341257259..5849bfb634 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -5,7 +5,7 @@ using Bit.Core.Settings; using AspNetCoreRateLimit; using Stripe; using Bit.Core.Utilities; -using IdentityModel; +using Duende.IdentityModel; using System.Globalization; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Models.Request; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ff5e929f18..8a8de3d77d 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -52,7 +52,7 @@ - + diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index dd603b4b63..8ecd337a16 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -12,7 +12,7 @@ using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index af985914c6..d7fe51cfb6 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -18,7 +18,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Identity; using Bit.Core.Settings; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.DataProtection; using MimeKit; diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 57af285b03..a9be60ce8a 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -6,7 +6,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; -using IdentityModel; +using Duende.IdentityModel; namespace Bit.Events; diff --git a/src/Identity/Controllers/SsoController.cs b/src/Identity/Controllers/SsoController.cs index f3dc301a61..d377573c7e 100644 --- a/src/Identity/Controllers/SsoController.cs +++ b/src/Identity/Controllers/SsoController.cs @@ -5,9 +5,9 @@ using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Repositories; using Bit.Identity.Models; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Services; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index f969d67908..364cbf8619 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -1,7 +1,7 @@ using Bit.Core.Identity; using Bit.Core.IdentityServer; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer; diff --git a/src/Identity/IdentityServer/ClientStore.cs b/src/Identity/IdentityServer/ClientStore.cs index c204e364ce..23942e6cd2 100644 --- a/src/Identity/IdentityServer/ClientStore.cs +++ b/src/Identity/IdentityServer/ClientStore.cs @@ -12,9 +12,9 @@ using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; +using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; -using IdentityModel; namespace Bit.Identity.IdentityServer; diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 597d5257e2..a7c6449ff6 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -11,10 +11,10 @@ using Bit.Core.Platform.Installations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Duende.IdentityModel; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using HandlebarsDotNet; -using IdentityModel; using Microsoft.AspNetCore.Identity; #nullable enable diff --git a/src/Notifications/Startup.cs b/src/Notifications/Startup.cs index 440808b78b..c939d0d2fd 100644 --- a/src/Notifications/Startup.cs +++ b/src/Notifications/Startup.cs @@ -3,7 +3,7 @@ using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.SignalR; using Microsoft.IdentityModel.Logging; diff --git a/src/Notifications/SubjectUserIdProvider.cs b/src/Notifications/SubjectUserIdProvider.cs index 261394d06c..6f8e15cc3c 100644 --- a/src/Notifications/SubjectUserIdProvider.cs +++ b/src/Notifications/SubjectUserIdProvider.cs @@ -1,4 +1,4 @@ -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 85bd014c91..144ea1f036 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -50,7 +50,7 @@ using Bit.Core.Vault.Services; using Bit.Infrastructure.Dapper; using Bit.Infrastructure.EntityFramework; using DnsClient; -using IdentityModel; +using Duende.IdentityModel; using LaunchDarkly.Sdk.Server; using LaunchDarkly.Sdk.Server.Interfaces; using Microsoft.AspNetCore.Authentication.Cookies; diff --git a/test/Core.Test/Utilities/CoreHelpersTests.cs b/test/Core.Test/Utilities/CoreHelpersTests.cs index 264a55b6ee..d006df536b 100644 --- a/test/Core.Test/Utilities/CoreHelpersTests.cs +++ b/test/Core.Test/Utilities/CoreHelpersTests.cs @@ -9,7 +9,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.DataProtection; using Xunit; diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index 0189032c24..602d5cfe48 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -16,9 +16,9 @@ using Bit.Core.Utilities; using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.Helpers; +using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; -using IdentityModel; using Microsoft.EntityFrameworkCore; using NSubstitute; using Xunit; diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 289f321512..6f0ef20295 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -15,9 +15,9 @@ using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; -using IdentityModel; using LinqToDB; using NSubstitute; using Xunit; From 546b5a08498c7d5bf2398f871c8efbb0430a2f18 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 27 Feb 2025 10:21:01 -0500 Subject: [PATCH 37/84] [pm-17804] Fix deferred execution issue in EF CreateManyAsync (#5425) * Add failing repository tests * test * clean up comments --------- Co-authored-by: Thomas Rittson --- .../OrganizationUserRepository.cs | 1 + .../OrganizationUserRepositoryTests.cs | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index ef6460df0e..0165360099 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -46,6 +46,7 @@ public class OrganizationUserRepository : Repository> CreateManyAsync(IEnumerable organizationUsers) { + organizationUsers = organizationUsers.ToList(); if (!organizationUsers.Any()) { return new List(); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index e82be49173..092ab95a14 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Xunit; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; @@ -354,4 +355,73 @@ public class OrganizationUserRepositoryTests Assert.Single(responseModel); Assert.Equal(orgUser1.Id, responseModel.Single().Id); } + + [DatabaseTheory, DatabaseData] + public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + var user3 = await userRepository.CreateTestUserAsync("user3"); + List users = [user1, user2, user3]; + + var org = await organizationRepository.CreateAsync(new Organization + { + Name = $"test-{Guid.NewGuid()}", + BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULl + }); + + var orgUsers = users.Select(u => new OrganizationUser + { + OrganizationId = org.Id, + UserId = u.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner + }); + + var createdOrgUserIds = await organizationUserRepository.CreateManyAsync(orgUsers); + + var readOrgUsers = await organizationUserRepository.GetManyByOrganizationAsync(org.Id, null); + var readOrgUserIds = readOrgUsers.Select(ou => ou.Id); + + Assert.Equal(createdOrgUserIds.ToHashSet(), readOrgUserIds.ToHashSet()); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateManyAsync_WithId_Works(IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + var user3 = await userRepository.CreateTestUserAsync("user3"); + List users = [user1, user2, user3]; + + var org = await organizationRepository.CreateAsync(new Organization + { + Name = $"test-{Guid.NewGuid()}", + BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULl + }); + + var orgUsers = users.Select(u => new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), // generate ID ahead of time + OrganizationId = org.Id, + UserId = u.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner + }); + + var createdOrgUserIds = await organizationUserRepository.CreateManyAsync(orgUsers); + + var readOrgUsers = await organizationUserRepository.GetManyByOrganizationAsync(org.Id, null); + var readOrgUserIds = readOrgUsers.Select(ou => ou.Id); + + Assert.Equal(createdOrgUserIds.ToHashSet(), readOrgUserIds.ToHashSet()); + } } From 3533f82d0ff8da19b6b3587ae3fafea585c08c71 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Thu, 27 Feb 2025 09:53:26 -0600 Subject: [PATCH 38/84] Tools/import cipher controller tests (#5436) * initial commit * PM-17954 adding unit tests for ImportCiphersController.PostImport --- .../ImportCiphersControllerTests.cs | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs diff --git a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs new file mode 100644 index 0000000000..c07f9791a3 --- /dev/null +++ b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs @@ -0,0 +1,363 @@ +using System.Security.Claims; +using AutoFixture; +using Bit.Api.Models.Request; +using Bit.Api.Tools.Controllers; +using Bit.Api.Tools.Models.Request.Accounts; +using Bit.Api.Tools.Models.Request.Organizations; +using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Api.Vault.Models.Request; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Tools.ImportFeatures.Interfaces; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + +namespace Bit.Api.Test.Tools.Controllers; + +[ControllerCustomize(typeof(ImportCiphersController))] +[SutProviderCustomize] +public class ImportCiphersControllerTests +{ + + /************************* + * PostImport - Individual + *************************/ + [Theory, BitAutoData] + public async Task PostImportIndividual_ImportCiphersRequestModel_BadRequestException(SutProvider sutProvider, IFixture fixture) + { + // Arrange + sutProvider.GetDependency() + .SelfHosted = false; + var ciphers = fixture.CreateMany(7001).ToArray(); + var model = new ImportCiphersRequestModel + { + Ciphers = ciphers, + FolderRelationships = null, + Folders = null + }; + + // Act + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.PostImport(model)); + + // Assert + Assert.Equal("You cannot import this much data at once.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PostImportIndividual_ImportCiphersRequestModel_Success(User user, + IFixture fixture, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .SelfHosted = false; + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + var request = fixture.Build() + .With(x => x.Ciphers, fixture.Build() + .With(c => c.OrganizationId, Guid.NewGuid().ToString()) + .With(c => c.FolderId, Guid.NewGuid().ToString()) + .CreateMany(1).ToArray()) + .Create(); + + // Act + await sutProvider.Sut.PostImport(request); + + // Assert + await sutProvider.GetDependency() + .Received() + .ImportIntoIndividualVaultAsync( + Arg.Any>(), + Arg.Any>(), + Arg.Any>>() + ); + } + + /**************************** + * PostImport - Organization + ****************************/ + + [Theory, BitAutoData] + public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_BadRequestException(SutProvider sutProvider, IFixture fixture) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + globalSettings.SelfHosted = false; + globalSettings.ImportCiphersLimitation = new GlobalSettings.ImportCiphersLimitationSettings() + { // limits are set in appsettings.json, making values small for test to run faster. + CiphersLimit = 200, + CollectionsLimit = 400, + CollectionRelationshipsLimit = 20 + }; + + var ciphers = fixture.CreateMany(201).ToArray(); + var model = new ImportOrganizationCiphersRequestModel + { + Collections = null, + Ciphers = ciphers, + CollectionRelationships = null + }; + + // Act + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.PostImport(Arg.Any(), model)); + + // Assert + Assert.Equal("You cannot import this much data at once.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_Succeeds( + SutProvider sutProvider, + IFixture fixture, + User user) + { + // Arrange + var orgId = "AD89E6F8-4E84-4CFE-A978-256CC0DBF974"; + var orgIdGuid = Guid.Parse(orgId); + var existingCollections = fixture.CreateMany(2).ToArray(); + + sutProvider.GetDependency().SelfHosted = false; + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + var request = fixture.Build() + .With(x => x.Ciphers, fixture.Build() + .With(c => c.OrganizationId, Guid.NewGuid().ToString()) + .With(c => c.FolderId, Guid.NewGuid().ToString()) + .CreateMany(1).ToArray()) + .With(y => y.Collections, fixture.Build() + .With(c => c.Id, orgIdGuid) + .CreateMany(1).ToArray()) + .Create(); + + // AccessImportExport permission setup + sutProvider.GetDependency() + .AccessImportExport(Arg.Any()) + .Returns(false); + + // BulkCollectionOperations.ImportCiphers permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ImportCiphers))) + .Returns(AuthorizationResult.Success()); + + // BulkCollectionOperations.Create permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.Create))) + .Returns(AuthorizationResult.Success()); + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgIdGuid) + .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList()); + + // Act + await sutProvider.Sut.PostImport(orgId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ImportIntoOrganizationalVaultAsync( + Arg.Any>(), + Arg.Any>(), + Arg.Any>>(), + Arg.Any()); + } + + [Theory, BitAutoData] + public async Task PostImportOrganization_WithAccessImportExport_Succeeds( + SutProvider sutProvider, + IFixture fixture, + User user) + { + // Arrange + var orgId = "AD89E6F8-4E84-4CFE-A978-256CC0DBF974"; + var orgIdGuid = Guid.Parse(orgId); + var existingCollections = fixture.CreateMany(2).ToArray(); + + sutProvider.GetDependency().SelfHosted = false; + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + var request = fixture.Build() + .With(x => x.Ciphers, fixture.Build() + .With(c => c.OrganizationId, Guid.NewGuid().ToString()) + .With(c => c.FolderId, Guid.NewGuid().ToString()) + .CreateMany(1).ToArray()) + .With(y => y.Collections, fixture.Build() + .With(c => c.Id, orgIdGuid) + .CreateMany(1).ToArray()) + .Create(); + + // AccessImportExport permission setup + sutProvider.GetDependency() + .AccessImportExport(Arg.Any()) + .Returns(false); + + // BulkCollectionOperations.ImportCiphers permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.ImportCiphers))) + .Returns(AuthorizationResult.Success()); + + // BulkCollectionOperations.Create permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => reqs.Contains(BulkCollectionOperations.Create))) + .Returns(AuthorizationResult.Success()); + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgIdGuid) + .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList()); + + // Act + await sutProvider.Sut.PostImport(orgId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ImportIntoOrganizationalVaultAsync( + Arg.Any>(), + Arg.Any>(), + Arg.Any>>(), + Arg.Any()); + } + + [Theory, BitAutoData] + public async Task PostImportOrganization_WithExistingCollectionsAndWithoutImportCiphersPermissions_NotFoundException( + SutProvider sutProvider, + IFixture fixture, + User user) + { + // Arrange + var orgId = "AD89E6F8-4E84-4CFE-A978-256CC0DBF974"; + var orgIdGuid = Guid.Parse(orgId); + var existingCollections = fixture.CreateMany(2).ToArray(); + + sutProvider.GetDependency().SelfHosted = false; + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + var request = fixture.Build() + .With(x => x.Ciphers, fixture.Build() + .With(c => c.OrganizationId, Guid.NewGuid().ToString()) + .With(c => c.FolderId, Guid.NewGuid().ToString()) + .CreateMany(1).ToArray()) + .With(y => y.Collections, fixture.Build() + .With(c => c.Id, orgIdGuid) + .CreateMany(1).ToArray()) + .Create(); + + // AccessImportExport permission setup + sutProvider.GetDependency() + .AccessImportExport(Arg.Any()) + .Returns(false); + + // BulkCollectionOperations.ImportCiphers permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => + reqs.Contains(BulkCollectionOperations.ImportCiphers))) + .Returns(AuthorizationResult.Failed()); + + // BulkCollectionOperations.Create permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => + reqs.Contains(BulkCollectionOperations.Create))) + .Returns(AuthorizationResult.Success()); + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgIdGuid) + .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList()); + + // Act + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.PostImport(orgId, request)); + + // Assert + Assert.IsType(exception); + } + + [Theory, BitAutoData] + public async Task PostImportOrganization_WithoutCreatePermissions_NotFoundException( + SutProvider sutProvider, + IFixture fixture, + User user) + { + // Arrange + var orgId = "AD89E6F8-4E84-4CFE-A978-256CC0DBF974"; + var orgIdGuid = Guid.Parse(orgId); + var existingCollections = fixture.CreateMany(2).ToArray(); + + sutProvider.GetDependency().SelfHosted = false; + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + var request = fixture.Build() + .With(x => x.Ciphers, fixture.Build() + .With(c => c.OrganizationId, Guid.NewGuid().ToString()) + .With(c => c.FolderId, Guid.NewGuid().ToString()) + .CreateMany(1).ToArray()) + .With(y => y.Collections, fixture.Build() + .With(c => c.Id, orgIdGuid) + .CreateMany(1).ToArray()) + .Create(); + + // AccessImportExport permission setup + sutProvider.GetDependency() + .AccessImportExport(Arg.Any()) + .Returns(false); + + // BulkCollectionOperations.ImportCiphers permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => + reqs.Contains(BulkCollectionOperations.ImportCiphers))) + .Returns(AuthorizationResult.Success()); + + // BulkCollectionOperations.Create permission setup + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => + reqs.Contains(BulkCollectionOperations.Create))) + .Returns(AuthorizationResult.Failed()); + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgIdGuid) + .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList()); + + // Act + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.PostImport(orgId, request)); + + // Assert + Assert.IsType(exception); + } +} From 8354929ff16b51643f2bcfd9b9c2842c9aa3f286 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 27 Feb 2025 11:01:40 -0500 Subject: [PATCH 39/84] [PM-18608] Don't require new device verification on newly created accounts (#5440) * Limit new device verification to aged accounts * set user creation date context for test * formatting --- .../RequestValidators/DeviceValidator.cs | 7 +++++ .../IdentityServer/DeviceValidatorTests.cs | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 17d16f5949..3ddc28c0e1 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -120,6 +120,13 @@ public class DeviceValidator( return DeviceValidationResultType.Success; } + // User is newly registered, so don't require new device verification + var createdSpan = DateTime.UtcNow - user.CreationDate; + if (createdSpan < TimeSpan.FromHours(24)) + { + return DeviceValidationResultType.Success; + } + // CS exception flow // Check cache for user information var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, user.Id.ToString()); diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index fddcf2005d..b71dd6c230 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -447,6 +447,31 @@ public class DeviceValidatorTests Assert.NotNull(context.Device); } + [Theory, BitAutoData] + public async void HandleNewDeviceVerificationAsync_NewlyCreated_ReturnsSuccess( + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + ArrangeForHandleNewDeviceVerificationTest(context, request); + _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); + _globalSettings.EnableNewDeviceVerification = true; + _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); + context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromHours(23); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + await _userService.Received(0).SendOTPAsync(context.User); + await _deviceService.Received(1).SaveAsync(Arg.Any()); + + Assert.True(result); + Assert.False(context.CustomResponse.ContainsKey("ErrorModel")); + Assert.Equal(context.User.Id, context.Device.UserId); + Assert.NotNull(context.Device); + } + [Theory, BitAutoData] public async void HandleNewDeviceVerificationAsync_UserHasCacheValue_ReturnsSuccess( CustomValidatorRequestContext context, @@ -633,5 +658,9 @@ public class DeviceValidatorTests request.GrantType = "password"; context.TwoFactorRequired = false; context.SsoRequired = false; + if (context.User != null) + { + context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(365); + } } } From 326ecebba1756001fd7522f645f7795fea2c7c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 27 Feb 2025 17:43:07 +0100 Subject: [PATCH 40/84] Fix SDK bindings generation (#5450) --- src/Api/Utilities/ServiceCollectionExtensions.cs | 2 -- .../Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs | 4 ++-- .../Organizations/PreviewOrganizationInvoiceRequestModel.cs | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index be106786e8..feeac03e54 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -36,8 +36,6 @@ public static class ServiceCollectionExtensions } }); - config.CustomSchemaIds(type => type.FullName); - config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" }); config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs index 6dfb9894d5..8597cea09b 100644 --- a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs +++ b/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs @@ -5,13 +5,13 @@ namespace Bit.Core.Billing.Models.Api.Requests.Accounts; public class PreviewIndividualInvoiceRequestBody { [Required] - public PasswordManagerRequestModel PasswordManager { get; set; } + public IndividualPasswordManagerRequestModel PasswordManager { get; set; } [Required] public TaxInformationRequestModel TaxInformation { get; set; } } -public class PasswordManagerRequestModel +public class IndividualPasswordManagerRequestModel { [Range(0, int.MaxValue)] public int AdditionalStorage { get; set; } diff --git a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs index 18d9c352d7..466c32f42d 100644 --- a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs +++ b/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs @@ -8,7 +8,7 @@ public class PreviewOrganizationInvoiceRequestBody public Guid OrganizationId { get; set; } [Required] - public PasswordManagerRequestModel PasswordManager { get; set; } + public OrganizationPasswordManagerRequestModel PasswordManager { get; set; } public SecretsManagerRequestModel SecretsManager { get; set; } @@ -16,7 +16,7 @@ public class PreviewOrganizationInvoiceRequestBody public TaxInformationRequestModel TaxInformation { get; set; } } -public class PasswordManagerRequestModel +public class OrganizationPasswordManagerRequestModel { public PlanType Plan { get; set; } From 63f1c3cee3a4ee4232e90a82ad6460e0171e1d5c Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 27 Feb 2025 16:30:25 -0500 Subject: [PATCH 41/84] [PM-18086] Add CanRestore and CanDelete authorization methods. (#5407) --- .../Permissions/NormalCipherPermissions.cs | 38 +++++ .../NormalCipherPermissionTests.cs | 150 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/Core/Vault/Authorization/Permissions/NormalCipherPermissions.cs create mode 100644 test/Core.Test/Vault/Authorization/Permissions/NormalCipherPermissionTests.cs diff --git a/src/Core/Vault/Authorization/Permissions/NormalCipherPermissions.cs b/src/Core/Vault/Authorization/Permissions/NormalCipherPermissions.cs new file mode 100644 index 0000000000..fbd553d772 --- /dev/null +++ b/src/Core/Vault/Authorization/Permissions/NormalCipherPermissions.cs @@ -0,0 +1,38 @@ +#nullable enable +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Vault.Authorization.Permissions; + +public class NormalCipherPermissions +{ + public static bool CanDelete(User user, CipherDetails cipherDetails, OrganizationAbility? organizationAbility) + { + if (cipherDetails.OrganizationId == null && cipherDetails.UserId == null) + { + throw new Exception("Cipher needs to belong to a user or an organization."); + } + + if (user.Id == cipherDetails.UserId) + { + return true; + } + + if (organizationAbility?.Id != cipherDetails.OrganizationId) + { + throw new Exception("Cipher does not belong to the input organization."); + } + + if (organizationAbility is { LimitItemDeletion: true }) + { + return cipherDetails.Manage; + } + return cipherDetails.Manage || cipherDetails.Edit; + } + + public static bool CanRestore(User user, CipherDetails cipherDetails, OrganizationAbility? organizationAbility) + { + return CanDelete(user, cipherDetails, organizationAbility); + } +} diff --git a/test/Core.Test/Vault/Authorization/Permissions/NormalCipherPermissionTests.cs b/test/Core.Test/Vault/Authorization/Permissions/NormalCipherPermissionTests.cs new file mode 100644 index 0000000000..9d18adc3a6 --- /dev/null +++ b/test/Core.Test/Vault/Authorization/Permissions/NormalCipherPermissionTests.cs @@ -0,0 +1,150 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Vault.Authorization.Permissions; +using Bit.Core.Vault.Models.Data; +using Xunit; + +namespace Bit.Core.Test.Vault.Authorization.Permissions; + +public class NormalCipherPermissionTests +{ + [Theory] + [InlineData(true, true, true, true)] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, true)] + [InlineData(false, false, true, true)] + [InlineData(false, false, false, false)] + public void CanRestore_WhenCipherIsOwnedByOrganization( + bool limitItemDeletion, bool manage, bool edit, bool expectedResult) + { + // Arrange + var user = new User { Id = Guid.Empty }; + var organizationId = Guid.NewGuid(); + var cipherDetails = new CipherDetails { Manage = manage, Edit = edit, UserId = null, OrganizationId = organizationId }; + var organizationAbility = new OrganizationAbility { Id = organizationId, LimitItemDeletion = limitItemDeletion }; + + // Act + var result = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility); + + // Assert + Assert.Equal(result, expectedResult); + } + + [Fact] + public void CanRestore_WhenCipherIsOwnedByUser() + { + // Arrange + var userId = Guid.NewGuid(); + var user = new User { Id = userId }; + var cipherDetails = new CipherDetails { UserId = userId }; + var organizationAbility = new OrganizationAbility { }; + + // Act + var result = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanRestore_WhenCipherHasNoOwner_ShouldThrowException() + { + // Arrange + var user = new User { Id = Guid.NewGuid() }; + var cipherDetails = new CipherDetails { UserId = null }; + + + // Act + // Assert + Assert.Throws(() => NormalCipherPermissions.CanRestore(user, cipherDetails, null)); + } + + public static List TestCases => + [ + new object[] { new OrganizationAbility { Id = Guid.Empty } }, + new object[] { null }, + ]; + + [Theory] + [MemberData(nameof(TestCases))] + public void CanRestore_WhenCipherDoesNotBelongToInputOrganization_ShouldThrowException(OrganizationAbility? organizationAbility) + { + // Arrange + var user = new User { Id = Guid.NewGuid() }; + var cipherDetails = new CipherDetails { UserId = null, OrganizationId = Guid.NewGuid() }; + + // Act + var exception = Assert.Throws(() => NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility)); + + // Assert + Assert.Equal("Cipher does not belong to the input organization.", exception.Message); + } + + [Theory] + [InlineData(true, true, true, true)] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, true)] + [InlineData(false, false, true, true)] + [InlineData(false, false, false, false)] + public void CanDelete_WhenCipherIsOwnedByOrganization( + bool limitItemDeletion, bool manage, bool edit, bool expectedResult) + { + // Arrange + var user = new User { Id = Guid.Empty }; + var organizationId = Guid.NewGuid(); + var cipherDetails = new CipherDetails { Manage = manage, Edit = edit, UserId = null, OrganizationId = organizationId }; + var organizationAbility = new OrganizationAbility { Id = organizationId, LimitItemDeletion = limitItemDeletion }; + + // Act + var result = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility); + + // Assert + Assert.Equal(result, expectedResult); + } + + [Fact] + public void CanDelete_WhenCipherIsOwnedByUser() + { + // Arrange + var userId = Guid.NewGuid(); + var user = new User { Id = userId }; + var cipherDetails = new CipherDetails { UserId = userId }; + var organizationAbility = new OrganizationAbility { }; + + // Act + var result = NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanDelete_WhenCipherHasNoOwner_ShouldThrowException() + { + // Arrange + var user = new User { Id = Guid.NewGuid() }; + var cipherDetails = new CipherDetails { UserId = null }; + + + // Act + var exception = Assert.Throws(() => NormalCipherPermissions.CanDelete(user, cipherDetails, null)); + + // Assert + Assert.Equal("Cipher needs to belong to a user or an organization.", exception.Message); + } + + [Theory] + [MemberData(nameof(TestCases))] + public void CanDelete_WhenCipherDoesNotBelongToInputOrganization_ShouldThrowException(OrganizationAbility? organizationAbility) + { + // Arrange + var user = new User { Id = Guid.NewGuid() }; + var cipherDetails = new CipherDetails { UserId = null, OrganizationId = Guid.NewGuid() }; + + // Act + var exception = Assert.Throws(() => NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility)); + + // Assert + Assert.Equal("Cipher does not belong to the input organization.", exception.Message); + } +} From 0d89409abd7adde0d26158a44f2fd2394c43b4eb Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 28 Feb 2025 09:21:30 -0600 Subject: [PATCH 42/84] [PM-18076] - Fix compiler warnings (#5451) * fixed warnings in UpdateOrganizationUserCommand.cs * Removed null dereference and multiple enumeration warning. * Removed unused param. Imported type for xml docs * imported missing type. * Added nullable block around method. --- .../UpdateOrganizationUserCommand.cs | 17 +++++++++-------- .../CloudOrganizationSignUpCommand.cs | 6 ++---- .../Interfaces/IOrganizationDeleteCommand.cs | 1 + .../IOrganizationInitiateDeleteCommand.cs | 1 + .../TwoFactorAuthenticationPolicyValidator.cs | 11 +++++++++-- .../Repositories/OrganizationUserRepository.cs | 2 ++ 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index 657cc6c54d..4aaecef29f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -63,10 +63,10 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand List? collectionAccess, IEnumerable? groupAccess) { // Avoid multiple enumeration - collectionAccess = collectionAccess?.ToList(); + var collectionAccessList = collectionAccess?.ToList() ?? []; groupAccess = groupAccess?.ToList(); - if (organizationUser.Id.Equals(default(Guid))) + if (organizationUser.Id.Equals(Guid.Empty)) { throw new BadRequestException("Invite the user first."); } @@ -93,9 +93,9 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand } } - if (collectionAccess?.Any() == true) + if (collectionAccessList.Count != 0) { - await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccess.ToList()); + await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccessList); } if (groupAccess?.Any() == true) @@ -111,14 +111,15 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(organizationUser.OrganizationId, organizationUser.Type); if (organizationUser.Type != OrganizationUserType.Owner && - !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id })) + !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, + [organizationUser.Id])) { throw new BadRequestException("Organization must have at least one confirmed owner."); } - if (collectionAccess?.Count > 0) + if (collectionAccessList?.Count > 0) { - var invalidAssociations = collectionAccess.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); + var invalidAssociations = collectionAccessList.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); if (invalidAssociations.Any()) { throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true."); @@ -140,7 +141,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand } } - await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccess); + await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccessList); if (groupAccess != null) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 57cfd1e60f..60e090de2a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -24,8 +24,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; public record SignUpOrganizationResponse( Organization Organization, - OrganizationUser OrganizationUser, - Collection DefaultCollection); + OrganizationUser OrganizationUser); public interface ICloudOrganizationSignUpCommand { @@ -34,7 +33,6 @@ public interface ICloudOrganizationSignUpCommand public class CloudOrganizationSignUpCommand( IOrganizationUserRepository organizationUserRepository, - IFeatureService featureService, IOrganizationBillingService organizationBillingService, IPaymentService paymentService, IPolicyService policyService, @@ -144,7 +142,7 @@ public class CloudOrganizationSignUpCommand( // TODO: add reference events for SmSeats and Service Accounts - see AC-1481 }); - return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection); + return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser); } public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs index fc4de42bed..8153a10958 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs index a8d211f245..867d41e7db 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs index 04984b17be..c757a65913 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -73,6 +73,11 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator { var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization is null) + { + return; + } + var currentActiveRevocableOrganizationUsers = (await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId)) .Where(ou => ou.Status != OrganizationUserStatusType.Invited && @@ -90,9 +95,11 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator var revocableUsersWithTwoFactorStatus = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers); - var nonCompliantUsers = revocableUsersWithTwoFactorStatus.Where(x => !x.twoFactorIsEnabled); + var nonCompliantUsers = revocableUsersWithTwoFactorStatus + .Where(x => !x.twoFactorIsEnabled) + .ToArray(); - if (!nonCompliantUsers.Any()) + if (nonCompliantUsers.Length == 0) { return; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 0165360099..28e2f1a9e4 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -249,6 +249,7 @@ public class OrganizationUserRepository : Repository Collections)> GetDetailsByIdWithCollectionsAsync(Guid id) { var organizationUserUserDetails = await GetDetailsByIdAsync(id); @@ -269,6 +270,7 @@ public class OrganizationUserRepository : Repository GetDetailsByUserAsync(Guid userId, Guid organizationId, OrganizationUserStatusType? status = null) { From cb68ef711a3273680f2e21f58f1f586d1ed1e4b0 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 4 Mar 2025 08:21:02 -0600 Subject: [PATCH 43/84] Added optional param to exclude orgs from cipher list. (#5455) --- src/Admin/Controllers/UsersController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 38e863aae7..cebb7d4b1e 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -102,12 +102,13 @@ public class UsersController : Controller return RedirectToAction("Index"); } - var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, withOrganizations: false); var billingInfo = await _paymentService.GetBillingAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id); + return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); } From 356ae1063a1d49467f45afd8830bbd1d4c659afb Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 4 Mar 2025 13:52:07 -0600 Subject: [PATCH 44/84] Fixed last dereference. (#5457) --- .../OrganizationUsers/UpdateOrganizationUserCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index 4aaecef29f..bad7b14b87 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -117,7 +117,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand throw new BadRequestException("Organization must have at least one confirmed owner."); } - if (collectionAccessList?.Count > 0) + if (collectionAccessList.Count > 0) { var invalidAssociations = collectionAccessList.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); if (invalidAssociations.Any()) From a9739c2b9488db8aaa1c8f77b67fb40fdf2dedf1 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 5 Mar 2025 04:57:09 +0000 Subject: [PATCH 45/84] Bumped version to 2025.3.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4b56322adb..a994b2196e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.2.1 + 2025.3.0 Bit.$(MSBuildProjectName) enable From 1efc105028d45a31b2e342fa9c7815594411be94 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:31:43 -0500 Subject: [PATCH 46/84] fix(New Device Verification): [PM-18906] Removed flagging from BW Portal --- src/Admin/Views/Users/Edit.cshtml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 8169b72b3c..dd80889110 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -9,8 +9,7 @@ var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View); var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) && - GlobalSettings.EnableNewDeviceVerification && - FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.NewDeviceVerification); + GlobalSettings.EnableNewDeviceVerification; var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View); var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View); var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View); From 3c0f72340311d810f07dba8531106df9e4f0bcb8 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 5 Mar 2025 09:42:39 -0500 Subject: [PATCH 47/84] remove feature flag (#5462) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 82ceb817e3..d5e815b4e7 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -102,7 +102,6 @@ public static class AuthenticationSchemes public static class FeatureFlagKeys { /* Admin Console Team */ - public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; From 267f306c850ce3c993e396de6fd7b7a2d71c8743 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:55:12 -0500 Subject: [PATCH 48/84] Updated server version to 2025.2.2 (#5466) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index a994b2196e..131de6320a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.3.0 + 2025.2.2 Bit.$(MSBuildProjectName) enable From 10756ca35e9c5ae0da34374e91d24ce4986fb883 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 5 Mar 2025 16:22:16 +0100 Subject: [PATCH 49/84] [PM-5872] Credit load intermittently fails (#5424) --- src/Billing/Constants/BitPayInvoiceStatus.cs | 7 +++++++ src/Billing/Constants/BitPayNotificationCode.cs | 6 ++++++ src/Billing/Controllers/BitPayController.cs | 11 ++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/Billing/Constants/BitPayInvoiceStatus.cs create mode 100644 src/Billing/Constants/BitPayNotificationCode.cs diff --git a/src/Billing/Constants/BitPayInvoiceStatus.cs b/src/Billing/Constants/BitPayInvoiceStatus.cs new file mode 100644 index 0000000000..b9c1e5834d --- /dev/null +++ b/src/Billing/Constants/BitPayInvoiceStatus.cs @@ -0,0 +1,7 @@ +namespace Bit.Billing.Constants; + +public static class BitPayInvoiceStatus +{ + public const string Confirmed = "confirmed"; + public const string Complete = "complete"; +} diff --git a/src/Billing/Constants/BitPayNotificationCode.cs b/src/Billing/Constants/BitPayNotificationCode.cs new file mode 100644 index 0000000000..f1ace14b81 --- /dev/null +++ b/src/Billing/Constants/BitPayNotificationCode.cs @@ -0,0 +1,6 @@ +namespace Bit.Billing.Constants; + +public static class BitPayNotificationCode +{ + public const string InvoiceConfirmed = "invoice_confirmed"; +} diff --git a/src/Billing/Controllers/BitPayController.cs b/src/Billing/Controllers/BitPayController.cs index 3747631bd0..a8d1742fcb 100644 --- a/src/Billing/Controllers/BitPayController.cs +++ b/src/Billing/Controllers/BitPayController.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Bit.Billing.Constants; using Bit.Billing.Models; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Services; @@ -65,7 +66,7 @@ public class BitPayController : Controller return new BadRequestResult(); } - if (model.Event.Name != "invoice_confirmed") + if (model.Event.Name != BitPayNotificationCode.InvoiceConfirmed) { // Only processing confirmed invoice events for now. return new OkResult(); @@ -75,20 +76,20 @@ public class BitPayController : Controller if (invoice == null) { // Request forged...? - _logger.LogWarning("Invoice not found. #" + model.Data.Id); + _logger.LogWarning("Invoice not found. #{InvoiceId}", model.Data.Id); return new BadRequestResult(); } - if (invoice.Status != "confirmed" && invoice.Status != "completed") + if (invoice.Status != BitPayInvoiceStatus.Confirmed && invoice.Status != BitPayInvoiceStatus.Complete) { - _logger.LogWarning("Invoice status of '" + invoice.Status + "' is not acceptable. #" + invoice.Id); + _logger.LogWarning("Invoice status of '{InvoiceStatus}' is not acceptable. #{InvoiceId}", invoice.Status, invoice.Id); return new BadRequestResult(); } if (invoice.Currency != "USD") { // Only process USD payments - _logger.LogWarning("Non USD payment received. #" + invoice.Id); + _logger.LogWarning("Non USD payment received. #{InvoiceId}", invoice.Id); return new OkResult(); } From fa9099127007db4552a39a954afeec444d9a4835 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:59:15 -0500 Subject: [PATCH 50/84] [PM-12601] Add discount to MSP during creation in Admin Portal (#5391) * Add Provider DiscountId to database and Stripe customer * Fix tests * Add missing EF migrations * Run dotnet format --- .../Billing/ProviderBillingService.cs | 33 +- .../Billing/ProviderBillingServiceTests.cs | 12 - .../Models/CreateMspProviderModel.cs | 6 +- .../Views/Providers/CreateMsp.cshtml | 14 + .../Entities/Provider/Provider.cs | 1 + src/Core/Billing/Constants/StripeConstants.cs | 9 +- .../Implementations/ProviderMigrator.cs | 2 +- .../Billing/Models/Sales/OrganizationSale.cs | 3 +- .../dbo/Stored Procedures/Provider_Create.sql | 9 +- .../dbo/Stored Procedures/Provider_Update.sql | 6 +- src/Sql/dbo/Tables/Provider.sql | 1 + ...-02-11_00_AddColumn_ProviderDiscountId.sql | 171 + ...7_AddColumn_ProviderDiscountId.Designer.cs | 3013 ++++++++++++++++ ...0213140357_AddColumn_ProviderDiscountId.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...6_AddColumn_ProviderDiscountId.Designer.cs | 3019 +++++++++++++++++ ...0213140406_AddColumn_ProviderDiscountId.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...1_AddColumn_ProviderDiscountId.Designer.cs | 3002 ++++++++++++++++ ...0213140401_AddColumn_ProviderDiscountId.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + 21 files changed, 9356 insertions(+), 36 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-02-11_00_AddColumn_ProviderDiscountId.sql create mode 100644 util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.cs create mode 100644 util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.cs create mode 100644 util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.cs diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index b637cf37ef..294a926022 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -475,20 +475,17 @@ public class ProviderBillingService( Provider provider, TaxInfo taxInfo) { - ArgumentNullException.ThrowIfNull(provider); - ArgumentNullException.ThrowIfNull(taxInfo); - - if (string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || - string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode)) + if (taxInfo is not + { + BillingAddressCountry: not null and not "", + BillingAddressPostalCode: not null and not "" + }) { logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id); - throw new BillingException(); } - var providerDisplayName = provider.DisplayName(); - - var customerCreateOptions = new CustomerCreateOptions + var options = new CustomerCreateOptions { Address = new AddressOptions { @@ -508,9 +505,9 @@ public class ProviderBillingService( new CustomerInvoiceSettingsCustomFieldOptions { Name = provider.SubscriberType(), - Value = providerDisplayName?.Length <= 30 - ? providerDisplayName - : providerDisplayName?[..30] + Value = provider.DisplayName()?.Length <= 30 + ? provider.DisplayName() + : provider.DisplayName()?[..30] } ] }, @@ -522,7 +519,8 @@ public class ProviderBillingService( if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber)) { - var taxIdType = taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, + var taxIdType = taxService.GetStripeTaxCode( + taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); if (taxIdType == null) @@ -533,15 +531,20 @@ public class ProviderBillingService( throw new BadRequestException("billingTaxIdTypeInferenceError"); } - customerCreateOptions.TaxIdData = + options.TaxIdData = [ new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber } ]; } + if (!string.IsNullOrEmpty(provider.DiscountId)) + { + options.Coupon = provider.DiscountId; + } + try { - return await stripeAdapter.CustomerCreateAsync(customerCreateOptions); + return await stripeAdapter.CustomerCreateAsync(options); } catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.TaxIdInvalid) { diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 2fbd09a213..c1da732d60 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -731,18 +731,6 @@ public class ProviderBillingServiceTests #region SetupCustomer - [Theory, BitAutoData] - public async Task SetupCustomer_NullProvider_ThrowsArgumentNullException( - SutProvider sutProvider, - TaxInfo taxInfo) => - await Assert.ThrowsAsync(() => sutProvider.Sut.SetupCustomer(null, taxInfo)); - - [Theory, BitAutoData] - public async Task SetupCustomer_NullTaxInfo_ThrowsArgumentNullException( - SutProvider sutProvider, - Provider provider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.SetupCustomer(provider, null)); - [Theory, BitAutoData] public async Task SetupCustomer_MissingCountry_ContactSupport( SutProvider sutProvider, diff --git a/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs b/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs index f48cf21767..4ada2d4a5f 100644 --- a/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs +++ b/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs @@ -10,6 +10,9 @@ public class CreateMspProviderModel : IValidatableObject [Display(Name = "Owner Email")] public string OwnerEmail { get; set; } + [Display(Name = "Subscription Discount")] + public string DiscountId { get; set; } + [Display(Name = "Teams (Monthly) Seat Minimum")] public int TeamsMonthlySeatMinimum { get; set; } @@ -20,7 +23,8 @@ public class CreateMspProviderModel : IValidatableObject { return new Provider { - Type = ProviderType.Msp + Type = ProviderType.Msp, + DiscountId = DiscountId }; } diff --git a/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml index 38ae542355..fffe21d5fe 100644 --- a/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml @@ -1,3 +1,4 @@ +@using Bit.Core.Billing.Constants @model CreateMspProviderModel @{ @@ -12,6 +13,19 @@
+
+ @{ + var selectList = new List + { + new ("No discount", string.Empty, true), + new ("20% - Open", StripeConstants.CouponIDs.MSPDiscounts.Open), + new ("35% - Silver", StripeConstants.CouponIDs.MSPDiscounts.Silver), + new ("50% - Gold", StripeConstants.CouponIDs.MSPDiscounts.Gold) + }; + } + + +
diff --git a/src/Core/AdminConsole/Entities/Provider/Provider.cs b/src/Core/AdminConsole/Entities/Provider/Provider.cs index 266e3498ff..3872ed22e4 100644 --- a/src/Core/AdminConsole/Entities/Provider/Provider.cs +++ b/src/Core/AdminConsole/Entities/Provider/Provider.cs @@ -35,6 +35,7 @@ public class Provider : ITableObject, ISubscriber public GatewayType? Gateway { get; set; } public string? GatewayCustomerId { get; set; } public string? GatewaySubscriptionId { get; set; } + public string? DiscountId { get; set; } public string? BillingEmailAddress() => BillingEmail?.ToLowerInvariant().Trim(); diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 0a5faae947..080416e2bb 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -18,8 +18,15 @@ public static class StripeConstants public static class CouponIDs { - public const string MSPDiscount35 = "msp-discount-35"; + public const string LegacyMSPDiscount = "msp-discount-35"; public const string SecretsManagerStandalone = "sm-standalone"; + + public static class MSPDiscounts + { + public const string Open = "msp-open-discount"; + public const string Silver = "msp-silver-discount"; + public const string Gold = "msp-gold-discount"; + } } public static class ErrorCodes diff --git a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs index ea490d0d66..b5c4383556 100644 --- a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs @@ -254,7 +254,7 @@ public class ProviderMigrator( await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { - Coupon = StripeConstants.CouponIDs.MSPDiscount35 + Coupon = StripeConstants.CouponIDs.LegacyMSPDiscount }); provider.GatewayCustomerId = customer.Id; diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index f0ad7894e6..0602cf1dd9 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -46,7 +46,8 @@ public class OrganizationSale var customerSetup = new CustomerSetup { Coupon = signup.IsFromProvider - ? StripeConstants.CouponIDs.MSPDiscount35 + // TODO: Remove when last of the legacy providers has been migrated. + ? StripeConstants.CouponIDs.LegacyMSPDiscount : signup.IsFromSecretsManagerTrial ? StripeConstants.CouponIDs.SecretsManagerStandalone : null diff --git a/src/Sql/dbo/Stored Procedures/Provider_Create.sql b/src/Sql/dbo/Stored Procedures/Provider_Create.sql index 63baa1789c..da1f3ad9a7 100644 --- a/src/Sql/dbo/Stored Procedures/Provider_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Provider_Create.sql @@ -17,7 +17,8 @@ @RevisionDate DATETIME2(7), @Gateway TINYINT = 0, @GatewayCustomerId VARCHAR(50) = NULL, - @GatewaySubscriptionId VARCHAR(50) = NULL + @GatewaySubscriptionId VARCHAR(50) = NULL, + @DiscountId VARCHAR(50) = NULL AS BEGIN SET NOCOUNT ON @@ -42,7 +43,8 @@ BEGIN [RevisionDate], [Gateway], [GatewayCustomerId], - [GatewaySubscriptionId] + [GatewaySubscriptionId], + [DiscountId] ) VALUES ( @@ -64,6 +66,7 @@ BEGIN @RevisionDate, @Gateway, @GatewayCustomerId, - @GatewaySubscriptionId + @GatewaySubscriptionId, + @DiscountId ) END diff --git a/src/Sql/dbo/Stored Procedures/Provider_Update.sql b/src/Sql/dbo/Stored Procedures/Provider_Update.sql index 39bdd2d613..639f40a2ac 100644 --- a/src/Sql/dbo/Stored Procedures/Provider_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Provider_Update.sql @@ -17,7 +17,8 @@ @RevisionDate DATETIME2(7), @Gateway TINYINT = 0, @GatewayCustomerId VARCHAR(50) = NULL, - @GatewaySubscriptionId VARCHAR(50) = NULL + @GatewaySubscriptionId VARCHAR(50) = NULL, + @DiscountId VARCHAR(50) = NULL AS BEGIN SET NOCOUNT ON @@ -42,7 +43,8 @@ BEGIN [RevisionDate] = @RevisionDate, [Gateway] = @Gateway, [GatewayCustomerId] = @GatewayCustomerId, - [GatewaySubscriptionId] = @GatewaySubscriptionId + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [DiscountId] = @DiscountId WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Tables/Provider.sql b/src/Sql/dbo/Tables/Provider.sql index fa64c01ec0..4b14730eb4 100644 --- a/src/Sql/dbo/Tables/Provider.sql +++ b/src/Sql/dbo/Tables/Provider.sql @@ -18,5 +18,6 @@ [Gateway] TINYINT NULL, [GatewayCustomerId] VARCHAR (50) NULL, [GatewaySubscriptionId] VARCHAR (50) NULL, + [DiscountId] VARCHAR (50) NULL, CONSTRAINT [PK_Provider] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/util/Migrator/DbScripts/2025-02-11_00_AddColumn_ProviderDiscountId.sql b/util/Migrator/DbScripts/2025-02-11_00_AddColumn_ProviderDiscountId.sql new file mode 100644 index 0000000000..02add59069 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-11_00_AddColumn_ProviderDiscountId.sql @@ -0,0 +1,171 @@ +-- Add 'DiscountId' column to 'Provider' table. +IF COL_LENGTH('[dbo].[Provider]', 'DiscountId') IS NULL + BEGIN + ALTER TABLE + [dbo].[Provider] + ADD + [DiscountId] VARCHAR(50) NULL; + END +GO + +-- Recreate 'ProviderView' so that it includes the 'DiscountId' column. +CREATE OR ALTER VIEW [dbo].[ProviderView] +AS +SELECT + * +FROM + [dbo].[Provider] +GO + +-- Alter 'Provider_Create' SPROC to add 'DiscountId' column. +CREATE OR ALTER PROCEDURE [dbo].[Provider_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @BillingPhone NVARCHAR(50) = NULL, + @Status TINYINT, + @Type TINYINT = 0, + @UseEvents BIT, + @Enabled BIT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Gateway TINYINT = 0, + @GatewayCustomerId VARCHAR(50) = NULL, + @GatewaySubscriptionId VARCHAR(50) = NULL, + @DiscountId VARCHAR(50) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Provider] + ( + [Id], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [BillingPhone], + [Status], + [Type], + [UseEvents], + [Enabled], + [CreationDate], + [RevisionDate], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [DiscountId] + ) + VALUES + ( + @Id, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @BillingPhone, + @Status, + @Type, + @UseEvents, + @Enabled, + @CreationDate, + @RevisionDate, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @DiscountId + ) +END +GO + +-- Alter 'Provider_Update' SPROC to add 'DiscountId' column. +CREATE OR ALTER PROCEDURE [dbo].[Provider_Update] + @Id UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @BillingPhone NVARCHAR(50) = NULL, + @Status TINYINT, + @Type TINYINT = 0, + @UseEvents BIT, + @Enabled BIT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Gateway TINYINT = 0, + @GatewayCustomerId VARCHAR(50) = NULL, + @GatewaySubscriptionId VARCHAR(50) = NULL, + @DiscountId VARCHAR(50) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Provider] + SET + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [BillingPhone] = @BillingPhone, + [Status] = @Status, + [Type] = @Type, + [UseEvents] = @UseEvents, + [Enabled] = @Enabled, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [DiscountId] = @DiscountId + WHERE + [Id] = @Id +END +GO + +-- Refresh modules for SPROCs reliant on 'Provider' table/view. +IF OBJECT_ID('[dbo].[Provider_ReadAbilities]') IS NOT NULL + BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_ReadAbilities]'; + END +GO + +IF OBJECT_ID('[dbo].[Provider_ReadById]') IS NOT NULL + BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_ReadById]'; + END +GO + +IF OBJECT_ID('[dbo].[Provider_ReadByOrganizationId]') IS NOT NULL + BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_ReadByOrganizationId]'; + END +GO + +IF OBJECT_ID('[dbo].[Provider_Search]') IS NOT NULL + BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[Provider_Search]'; + END +GO diff --git a/util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.Designer.cs b/util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.Designer.cs new file mode 100644 index 0000000000..947483d796 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.Designer.cs @@ -0,0 +1,3013 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250213140357_AddColumn_ProviderDiscountId")] + partial class AddColumn_ProviderDiscountId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.cs b/util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.cs new file mode 100644 index 0000000000..53eb2350e8 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250213140357_AddColumn_ProviderDiscountId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddColumn_ProviderDiscountId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DiscountId", + table: "Provider", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DiscountId", + table: "Provider"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 73ed8e0c6b..bd04151035 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -284,6 +284,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("CreationDate") .HasColumnType("datetime(6)"); + b.Property("DiscountId") + .HasColumnType("longtext"); + b.Property("Enabled") .HasColumnType("tinyint(1)"); diff --git a/util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.Designer.cs b/util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.Designer.cs new file mode 100644 index 0000000000..79533f72ae --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.Designer.cs @@ -0,0 +1,3019 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250213140406_AddColumn_ProviderDiscountId")] + partial class AddColumn_ProviderDiscountId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.cs b/util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.cs new file mode 100644 index 0000000000..282f6f0fb8 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250213140406_AddColumn_ProviderDiscountId.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddColumn_ProviderDiscountId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DiscountId", + table: "Provider", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DiscountId", + table: "Provider"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index a6017652bf..b507984ad1 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -287,6 +287,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("CreationDate") .HasColumnType("timestamp with time zone"); + b.Property("DiscountId") + .HasColumnType("text"); + b.Property("Enabled") .HasColumnType("boolean"); diff --git a/util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.Designer.cs b/util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.Designer.cs new file mode 100644 index 0000000000..387e0a7f30 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.Designer.cs @@ -0,0 +1,3002 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250213140401_AddColumn_ProviderDiscountId")] + partial class AddColumn_ProviderDiscountId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.cs b/util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.cs new file mode 100644 index 0000000000..3081e35ac4 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250213140401_AddColumn_ProviderDiscountId.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddColumn_ProviderDiscountId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DiscountId", + table: "Provider", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DiscountId", + table: "Provider"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 185caf3074..158a02cd43 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -279,6 +279,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("CreationDate") .HasColumnType("TEXT"); + b.Property("DiscountId") + .HasColumnType("TEXT"); + b.Property("Enabled") .HasColumnType("INTEGER"); From 88ffde930fd8ff608030dcd2148885c5a35458a9 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:29:51 -0500 Subject: [PATCH 51/84] Check to see if cancellation comment is populated before disablement checks (#5468) --- .../Implementations/SubscriptionDeletedHandler.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index 8155928453..465da86c3f 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -41,10 +41,15 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler return; } - if (organizationId.HasValue && - subscription.CancellationDetails.Comment != providerMigrationCancellationComment && - !subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)) + if (organizationId.HasValue) { + if (!string.IsNullOrEmpty(subscription.CancellationDetails?.Comment) && + (subscription.CancellationDetails.Comment == providerMigrationCancellationComment || + subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment))) + { + return; + } + await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); } else if (userId.HasValue) From ac1d5b1a6959cc3c8476de1dd4110a58d6fa0bea Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 5 Mar 2025 23:05:04 +0000 Subject: [PATCH 52/84] Bumped version to 2025.2.3 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 131de6320a..403bf843d1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.2.2 + 2025.2.3 Bit.$(MSBuildProjectName) enable From 7281dd9b58a53c9f151a99bd9a035fcb105ceba3 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 6 Mar 2025 13:19:18 +0100 Subject: [PATCH 53/84] [PM-18163] Remove feature flag 'AC-1795_updated-subscription-status-section' (#5411) --- src/Core/Constants.cs | 1 - .../Implementations/StripePaymentService.cs | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d5e815b4e7..672188ce1f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -119,7 +119,6 @@ public static class FeatureFlagKeys public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; 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"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; public const string ExtensionRefresh = "extension-refresh"; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 1a8fe2085d..bfa94cf5ba 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1609,15 +1609,12 @@ public class StripePaymentService : IPaymentService { subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); - if (_featureService.IsEnabled(FeatureFlagKeys.AC1795_UpdatedSubscriptionStatusSection)) - { - var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub); + var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub); - if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue) - { - subscriptionInfo.Subscription.SuspensionDate = suspensionDate; - subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate; - } + if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue) + { + subscriptionInfo.Subscription.SuspensionDate = suspensionDate; + subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate; } } From c82908f40b7d67fbf9c736df0aa1578076128dbc Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 6 Mar 2025 11:16:58 -0500 Subject: [PATCH 54/84] [PM-15621] Add functionality to map command results to HTTP responses. (#5467) --- src/Api/Utilities/CommandResultExtensions.cs | 31 +++++ src/Core/Models/Commands/BadRequestFailure.cs | 23 ++++ src/Core/Models/Commands/CommandResult.cs | 40 ++++++- .../Models/Commands/NoRecordFoundFailure.cs | 24 ++++ .../Utilities/CommandResultExtensionTests.cs | 107 ++++++++++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/Api/Utilities/CommandResultExtensions.cs create mode 100644 src/Core/Models/Commands/BadRequestFailure.cs create mode 100644 src/Core/Models/Commands/NoRecordFoundFailure.cs create mode 100644 test/Api.Test/Utilities/CommandResultExtensionTests.cs diff --git a/src/Api/Utilities/CommandResultExtensions.cs b/src/Api/Utilities/CommandResultExtensions.cs new file mode 100644 index 0000000000..39104db7ff --- /dev/null +++ b/src/Api/Utilities/CommandResultExtensions.cs @@ -0,0 +1,31 @@ +using Bit.Core.Models.Commands; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Utilities; + +public static class CommandResultExtensions +{ + public static IActionResult MapToActionResult(this CommandResult commandResult) + { + return commandResult switch + { + NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, + BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Success success => new ObjectResult(success.Data) { StatusCode = StatusCodes.Status200OK }, + _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") + }; + } + + public static IActionResult MapToActionResult(this CommandResult commandResult) + { + return commandResult switch + { + NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, + BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK }, + _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") + }; + } +} diff --git a/src/Core/Models/Commands/BadRequestFailure.cs b/src/Core/Models/Commands/BadRequestFailure.cs new file mode 100644 index 0000000000..bd2753d4e4 --- /dev/null +++ b/src/Core/Models/Commands/BadRequestFailure.cs @@ -0,0 +1,23 @@ +namespace Bit.Core.Models.Commands; + +public class BadRequestFailure : Failure +{ + public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public BadRequestFailure(string errorMessage) : base(errorMessage) + { + } +} + +public class BadRequestFailure : Failure +{ + public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public BadRequestFailure(string errorMessage) : base(errorMessage) + { + } +} diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs index 9e5d91e09c..ae14b7d2f9 100644 --- a/src/Core/Models/Commands/CommandResult.cs +++ b/src/Core/Models/Commands/CommandResult.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Models.Commands; +#nullable enable + +namespace Bit.Core.Models.Commands; public class CommandResult(IEnumerable errors) { @@ -10,3 +12,39 @@ public class CommandResult(IEnumerable errors) public CommandResult() : this(Array.Empty()) { } } + +public class Failure : CommandResult +{ + protected Failure(IEnumerable errorMessages) : base(errorMessages) + { + + } + public Failure(string errorMessage) : base(errorMessage) + { + + } +} + +public class Success : CommandResult +{ +} + +public abstract class CommandResult +{ + +} + +public class Success(T data) : CommandResult +{ + public T? Data { get; init; } = data; +} + +public class Failure(IEnumerable errorMessage) : CommandResult +{ + public IEnumerable ErrorMessages { get; init; } = errorMessage; + + public Failure(string errorMessage) : this(new[] { errorMessage }) + { + } +} + diff --git a/src/Core/Models/Commands/NoRecordFoundFailure.cs b/src/Core/Models/Commands/NoRecordFoundFailure.cs new file mode 100644 index 0000000000..a8a322b928 --- /dev/null +++ b/src/Core/Models/Commands/NoRecordFoundFailure.cs @@ -0,0 +1,24 @@ +namespace Bit.Core.Models.Commands; + +public class NoRecordFoundFailure : Failure +{ + public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public NoRecordFoundFailure(string errorMessage) : base(errorMessage) + { + } +} + +public class NoRecordFoundFailure : Failure +{ + public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) + { + } + + public NoRecordFoundFailure(string errorMessage) : base(errorMessage) + { + } +} + diff --git a/test/Api.Test/Utilities/CommandResultExtensionTests.cs b/test/Api.Test/Utilities/CommandResultExtensionTests.cs new file mode 100644 index 0000000000..dafae10b5b --- /dev/null +++ b/test/Api.Test/Utilities/CommandResultExtensionTests.cs @@ -0,0 +1,107 @@ +using Bit.Api.Utilities; +using Bit.Core.Models.Commands; +using Bit.Core.Vault.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace Bit.Api.Test.Utilities; + +public class CommandResultExtensionTests +{ + public static IEnumerable WithGenericTypeTestCases() + { + yield return new object[] + { + new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), + new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } + }; + yield return new object[] + { + new BadRequestFailure("Error 3"), + new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + yield return new object[] + { + new Failure("Error 4"), + new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + var cipher = new Cipher() { Id = Guid.NewGuid() }; + + yield return new object[] + { + new Success(cipher), + new ObjectResult(cipher) { StatusCode = StatusCodes.Status200OK } + }; + } + + + [Theory] + [MemberData(nameof(WithGenericTypeTestCases))] + public void MapToActionResult_WithGenericType_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) + { + var result = input.MapToActionResult(); + + Assert.Equivalent(expected, result); + } + + + [Fact] + public void MapToActionResult_WithGenericType_ShouldThrowExceptionForUnhandledCommandResult() + { + var result = new NotImplementedCommandResult(); + + Assert.Throws(() => result.MapToActionResult()); + } + + public static IEnumerable TestCases() + { + yield return new object[] + { + new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), + new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } + }; + yield return new object[] + { + new BadRequestFailure("Error 3"), + new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + yield return new object[] + { + new Failure("Error 4"), + new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } + }; + yield return new object[] + { + new Success(), + new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK } + }; + } + + [Theory] + [MemberData(nameof(TestCases))] + public void MapToActionResult_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) + { + var result = input.MapToActionResult(); + + Assert.Equivalent(expected, result); + } + + [Fact] + public void MapToActionResult_ShouldThrowExceptionForUnhandledCommandResult() + { + var result = new NotImplementedCommandResult(); + + Assert.Throws(() => result.MapToActionResult()); + } +} + +public class NotImplementedCommandResult : CommandResult +{ + +} + +public class NotImplementedCommandResult : CommandResult +{ + +} From cb1c12794ff17220ee8b63081773a94f461977e8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:44:10 -0500 Subject: [PATCH 55/84] Derive item add on status from price metadata (#5389) --- src/Core/Models/Business/SubscriptionInfo.cs | 7 +++++-- src/Core/Utilities/StaticStore.cs | 17 ----------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index c9c3b31c7f..78a995fb94 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -73,8 +73,11 @@ public class SubscriptionInfo Name = item.Plan.Nickname; Amount = item.Plan.Amount.GetValueOrDefault() / 100M; Interval = item.Plan.Interval; - AddonSubscriptionItem = - Utilities.StaticStore.IsAddonSubscriptionItem(item.Plan.Id); + + if (item.Metadata != null) + { + AddonSubscriptionItem = item.Metadata.TryGetValue("isAddOn", out var value) && bool.Parse(value); + } } Quantity = (int)item.Quantity; diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 24e9ccd7bd..1cae361e29 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -158,21 +158,4 @@ public static class StaticStore public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType); - - /// - /// Determines if the stripe plan id is an addon item by checking if the provided stripe plan id - /// matches either the or - /// in any . - /// - /// - /// - /// True if the stripePlanId is a addon product, false otherwise - /// - public static bool IsAddonSubscriptionItem(string stripePlanId) - { - // TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-16844 - return Plans.Any(p => - p.PasswordManager.StripeStoragePlanId == stripePlanId || - (p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId)); - } } From 8628206fa90287e1c169c928e5ecad8b14b2210d Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 6 Mar 2025 22:13:02 +0100 Subject: [PATCH 56/84] ArgumentNullException: Value cannot be null in POST /push/register (#5472) --- .../Push/Controllers/PushController.cs | 5 +-- .../Push/Controllers/PushControllerTests.cs | 32 +++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index 28641a86cf..2a1f2b987d 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -43,8 +43,9 @@ public class PushController : Controller public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model) { CheckUsage(); - await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken), Prefix(model.DeviceId), - Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), model.InstallationId); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken), + Prefix(model.DeviceId), Prefix(model.UserId), Prefix(model.Identifier), model.Type, + model.OrganizationIds?.Select(Prefix) ?? [], model.InstallationId); } [HttpPost("delete")] diff --git a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs index 399913a0c4..6df09c17dc 100644 --- a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs +++ b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs @@ -243,20 +243,22 @@ public class PushControllerTests PushToken = "test-push-token", UserId = userId.ToString(), Type = DeviceType.Android, - Identifier = identifier.ToString() + Identifier = identifier.ToString(), })); Assert.Equal("Not correctly configured for push relays.", exception.Message); await sutProvider.GetDependency().Received(0) - .CreateOrUpdateRegistrationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any>(), Arg.Any()); + .CreateOrUpdateRegistrationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory] - [BitAutoData] - public async Task? RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(SutProvider sutProvider, - Guid installationId, Guid userId, Guid identifier, Guid deviceId, Guid organizationId) + [BitAutoData(false)] + [BitAutoData(true)] + public async Task RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(bool haveOrganizationId, + SutProvider sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId, + Guid organizationId) { sutProvider.GetDependency().SelfHosted = false; sutProvider.GetDependency().InstallationId.Returns(installationId); @@ -273,19 +275,29 @@ public class PushControllerTests UserId = userId.ToString(), Type = DeviceType.Android, Identifier = identifier.ToString(), - OrganizationIds = [organizationId.ToString()], + OrganizationIds = haveOrganizationId ? [organizationId.ToString()] : null, InstallationId = installationId }; await sutProvider.Sut.RegisterAsync(model); await sutProvider.GetDependency().Received(1) - .CreateOrUpdateRegistrationAsync(Arg.Is(data => data == new PushRegistrationData(model.PushToken)), expectedDeviceId, expectedUserId, + .CreateOrUpdateRegistrationAsync( + Arg.Is(data => data == new PushRegistrationData(model.PushToken)), + expectedDeviceId, expectedUserId, expectedIdentifier, DeviceType.Android, Arg.Do>(organizationIds => { + Assert.NotNull(organizationIds); var organizationIdsList = organizationIds.ToList(); - Assert.Contains(expectedOrganizationId, organizationIdsList); - Assert.Single(organizationIdsList); + if (haveOrganizationId) + { + Assert.Contains(expectedOrganizationId, organizationIdsList); + Assert.Single(organizationIdsList); + } + else + { + Assert.Empty(organizationIdsList); + } }), installationId); } } From bea0d0d76f0057796f279e34516953b168a668b2 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 6 Mar 2025 21:51:25 +0000 Subject: [PATCH 57/84] Bumped version to 2025.2.4 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 403bf843d1..03594371e9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.2.3 + 2025.2.4 Bit.$(MSBuildProjectName) enable From 6cb97d9bf9c02d4036a549bd140030f9206f8b5a Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 6 Mar 2025 20:32:21 -0600 Subject: [PATCH 58/84] [PM-18972] - Fix query for Org By User Domain (#5474) * Changed query to avoid table scan. Added index to speed up query as well. --- .../Repositories/OrganizationRepository.cs | 34 +++++++++++++------ ...anization_ReadByClaimedUserEmailDomain.sql | 21 ++++++++---- src/Sql/dbo/Tables/OrganizationDomain.sql | 5 +++ ..._ReadByClaimedUserEmailDomain_AndIndex.sql | 31 +++++++++++++++++ 4 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-03-06_00_ReadByClaimedUserEmailDomain_AndIndex.sql diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index ea4e1334c6..6fc42b699d 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -290,21 +290,33 @@ public class OrganizationRepository : Repository> GetByVerifiedUserEmailDomainAsync(Guid userId) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); + using var scope = ServiceScopeFactory.CreateScope(); - var query = from u in dbContext.Users - join ou in dbContext.OrganizationUsers on u.Id equals ou.UserId - join o in dbContext.Organizations on ou.OrganizationId equals o.Id - join od in dbContext.OrganizationDomains on ou.OrganizationId equals od.OrganizationId + var dbContext = GetDatabaseContext(scope); + + var userQuery = from u in dbContext.Users where u.Id == userId - && od.VerifiedDate != null - && u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower()) - select o; + select u; - return await query.ToArrayAsync(); + var user = await userQuery.FirstOrDefaultAsync(); + + if (user is null) + { + return new List(); } + + var userWithDomain = new { UserId = user.Id, EmailDomain = user.Email.Split('@').Last() }; + + var query = from o in dbContext.Organizations + join ou in dbContext.OrganizationUsers on o.Id equals ou.OrganizationId + join od in dbContext.OrganizationDomains on ou.OrganizationId equals od.OrganizationId + where ou.UserId == userWithDomain.UserId && + od.DomainName == userWithDomain.EmailDomain && + od.VerifiedDate != null && + o.Enabled == true + select o; + + return await query.ToArrayAsync(); } public async Task> GetAddableToProviderByUserIdAsync( diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql index 39cf5d384c..583f548c8b 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql @@ -4,12 +4,19 @@ AS BEGIN SET NOCOUNT ON; + WITH CTE_User AS ( + SELECT + U.*, + SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain + FROM dbo.[UserView] U + WHERE U.[Id] = @UserId + ) SELECT O.* - FROM [dbo].[UserView] U - INNER JOIN [dbo].[OrganizationUserView] OU ON U.[Id] = OU.[UserId] - INNER JOIN [dbo].[OrganizationView] O ON OU.[OrganizationId] = O.[Id] - INNER JOIN [dbo].[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId] - WHERE U.[Id] = @UserId - AND OD.[VerifiedDate] IS NOT NULL - AND U.[Email] LIKE '%@' + OD.[DomainName]; + FROM CTE_User CU + INNER JOIN dbo.[OrganizationUserView] OU ON CU.[Id] = OU.[UserId] + INNER JOIN dbo.[OrganizationView] O ON OU.[OrganizationId] = O.[Id] + INNER JOIN dbo.[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId] + WHERE OD.[VerifiedDate] IS NOT NULL + AND CU.EmailDomain = OD.[DomainName] + AND O.[Enabled] = 1 END diff --git a/src/Sql/dbo/Tables/OrganizationDomain.sql b/src/Sql/dbo/Tables/OrganizationDomain.sql index 09e4997d74..615dcc1557 100644 --- a/src/Sql/dbo/Tables/OrganizationDomain.sql +++ b/src/Sql/dbo/Tables/OrganizationDomain.sql @@ -22,3 +22,8 @@ CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_VerifiedDate] ON [dbo].[OrganizationDomain] ([VerifiedDate]) INCLUDE ([OrganizationId],[DomainName]); GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId] + ON [dbo].[OrganizationDomain] ([DomainName],[VerifiedDate]) + INCLUDE ([OrganizationId]) +GO diff --git a/util/Migrator/DbScripts/2025-03-06_00_ReadByClaimedUserEmailDomain_AndIndex.sql b/util/Migrator/DbScripts/2025-03-06_00_ReadByClaimedUserEmailDomain_AndIndex.sql new file mode 100644 index 0000000000..a28b869c4e --- /dev/null +++ b/util/Migrator/DbScripts/2025-03-06_00_ReadByClaimedUserEmailDomain_AndIndex.sql @@ -0,0 +1,31 @@ +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadByClaimedUserEmailDomain] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + WITH CTE_User AS ( + SELECT + U.*, + SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain + FROM dbo.[UserView] U + WHERE U.[Id] = @UserId + ) + SELECT O.* + FROM CTE_User CU + INNER JOIN dbo.[OrganizationUserView] OU ON CU.[Id] = OU.[UserId] + INNER JOIN dbo.[OrganizationView] O ON OU.[OrganizationId] = O.[Id] + INNER JOIN dbo.[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId] + WHERE OD.[VerifiedDate] IS NOT NULL + AND CU.EmailDomain = OD.[DomainName] + AND O.[Enabled] = 1 +END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId') + BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId] + ON [dbo].[OrganizationDomain] ([DomainName],[VerifiedDate]) + INCLUDE ([OrganizationId]) + END +GO From c589f9a330575831b075f93c30223cdb799e28d9 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Fri, 7 Mar 2025 09:52:04 +0100 Subject: [PATCH 59/84] [BEEEP] [PM-18518] Cleanup StripePaymentService (#5435) --- .../Billing/Extensions/CustomerExtensions.cs | 5 + .../Extensions/SubscriberExtensions.cs | 26 + src/Core/Services/IPaymentService.cs | 12 - .../Implementations/StripePaymentService.cs | 737 +--------------- .../Extensions/SubscriberExtensionsTests.cs | 23 + .../Services/StripePaymentServiceTests.cs | 828 ------------------ 6 files changed, 58 insertions(+), 1573 deletions(-) create mode 100644 src/Core/Billing/Extensions/SubscriberExtensions.cs create mode 100644 test/Core.Test/Extensions/SubscriberExtensionsTests.cs delete mode 100644 test/Core.Test/Services/StripePaymentServiceTests.cs diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs index 1847abb0ad..1ab595342e 100644 --- a/src/Core/Billing/Extensions/CustomerExtensions.cs +++ b/src/Core/Billing/Extensions/CustomerExtensions.cs @@ -22,4 +22,9 @@ public static class CustomerExtensions /// public static bool HasTaxLocationVerified(this Customer customer) => customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; + + public static decimal GetBillingBalance(this Customer customer) + { + return customer != null ? customer.Balance / 100M : default; + } } diff --git a/src/Core/Billing/Extensions/SubscriberExtensions.cs b/src/Core/Billing/Extensions/SubscriberExtensions.cs new file mode 100644 index 0000000000..e322ed7317 --- /dev/null +++ b/src/Core/Billing/Extensions/SubscriberExtensions.cs @@ -0,0 +1,26 @@ +using Bit.Core.Entities; + +namespace Bit.Core.Billing.Extensions; + +public static class SubscriberExtensions +{ + /// + /// We are taking only first 30 characters of the SubscriberName because stripe provide for 30 characters for + /// custom_fields,see the link: https://stripe.com/docs/api/invoices/create + /// + /// + /// + public static string GetFormattedInvoiceName(this ISubscriber subscriber) + { + var subscriberName = subscriber.SubscriberName(); + + if (string.IsNullOrWhiteSpace(subscriberName)) + { + return string.Empty; + } + + return subscriberName.Length <= 30 + ? subscriberName + : subscriberName[..30]; + } +} diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 5bd2bede33..e3495c0e65 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -14,18 +14,8 @@ namespace Bit.Core.Services; public interface IPaymentService { Task CancelAndRecoverChargesAsync(ISubscriber subscriber); - Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, - string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, - bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0, - int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false); - Task PurchaseOrganizationNoPaymentMethod(Organization org, Plan plan, int additionalSeats, - bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0, - bool signupIsFromSecretsManagerTrial = false); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); - Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade); - Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, - short additionalStorageGb, TaxInfo taxInfo); Task AdjustSubscription( Organization organization, Plan updatedPlan, @@ -56,9 +46,7 @@ public interface IPaymentService Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo); Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, int additionalServiceAccount); - Task RisksSubscriptionFailure(Organization organization); Task HasSecretsManagerStandalone(Organization organization); - Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription); Task PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); Task PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index bfa94cf5ba..ca377407f4 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -25,9 +25,6 @@ namespace Bit.Core.Services; public class StripePaymentService : IPaymentService { - private const string PremiumPlanId = "premium-annually"; - private const string StoragePlanId = "storage-gb-annually"; - private const string ProviderDiscountId = "msp-discount-35"; private const string SecretsManagerStandaloneDiscountId = "sm-standalone"; private readonly ITransactionRepository _transactionRepository; @@ -62,240 +59,6 @@ public class StripePaymentService : IPaymentService _pricingClient = pricingClient; } - public async Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, - string paymentToken, StaticStore.Plan plan, short additionalStorageGb, - int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, - int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false) - { - Braintree.Customer braintreeCustomer = null; - string stipeCustomerSourceToken = null; - string stipeCustomerPaymentMethodId = null; - var stripeCustomerMetadata = new Dictionary - { - { "region", _globalSettings.BaseServiceUri.CloudRegion } - }; - var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || - paymentMethodType == PaymentMethodType.BankAccount; - - if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken)) - { - if (paymentToken.StartsWith("pm_")) - { - stipeCustomerPaymentMethodId = paymentToken; - } - else - { - stipeCustomerSourceToken = paymentToken; - } - } - else if (paymentMethodType == PaymentMethodType.PayPal) - { - var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false); - var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest - { - PaymentMethodNonce = paymentToken, - Email = org.BillingEmail, - Id = org.BraintreeCustomerIdPrefix() + org.Id.ToString("N").ToLower() + randomSuffix, - CustomFields = new Dictionary - { - [org.BraintreeIdField()] = org.Id.ToString(), - [org.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion - } - }); - - if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) - { - throw new GatewayException("Failed to create PayPal customer record."); - } - - braintreeCustomer = customerResult.Target; - stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); - } - else - { - throw new GatewayException("Payment method is not supported at this time."); - } - - var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon - , additionalSmSeats, additionalServiceAccount); - - Customer customer = null; - Subscription subscription; - try - { - if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) - { - taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - - if (taxInfo.TaxIdType == null) - { - _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - } - - var customerCreateOptions = new CustomerCreateOptions - { - Description = org.DisplayBusinessName(), - Email = org.BillingEmail, - Source = stipeCustomerSourceToken, - PaymentMethod = stipeCustomerPaymentMethodId, - Metadata = stripeCustomerMetadata, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = stipeCustomerPaymentMethodId, - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions - { - Name = org.SubscriberType(), - Value = GetFirstThirtyCharacters(org.SubscriberName()), - } - ], - }, - Coupon = signupIsFromSecretsManagerTrial - ? SecretsManagerStandaloneDiscountId - : provider - ? ProviderDiscountId - : null, - Address = new AddressOptions - { - Country = taxInfo?.BillingAddressCountry, - PostalCode = taxInfo?.BillingAddressPostalCode, - // Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead. - Line1 = taxInfo?.BillingAddressLine1 ?? string.Empty, - Line2 = taxInfo?.BillingAddressLine2, - City = taxInfo?.BillingAddressCity, - State = taxInfo?.BillingAddressState, - }, - TaxIdData = !string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) - ? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }] - : null - }; - - customerCreateOptions.AddExpand("tax"); - - customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); - subCreateOptions.AddExpand("latest_invoice.payment_intent"); - subCreateOptions.Customer = customer.Id; - subCreateOptions.EnableAutomaticTax(customer); - - subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); - if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) - { - if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method") - { - await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions()); - throw new GatewayException("Payment method was declined."); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating customer, walking back operation."); - if (customer != null) - { - await _stripeAdapter.CustomerDeleteAsync(customer.Id); - } - if (braintreeCustomer != null) - { - await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); - } - throw; - } - - org.Gateway = GatewayType.Stripe; - org.GatewayCustomerId = customer.Id; - org.GatewaySubscriptionId = subscription.Id; - - if (subscription.Status == "incomplete" && - subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") - { - org.Enabled = false; - return subscription.LatestInvoice.PaymentIntent.ClientSecret; - } - else - { - org.Enabled = true; - org.ExpirationDate = subscription.CurrentPeriodEnd; - return null; - } - } - - public async Task PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon, - int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false) - { - - var stripeCustomerMetadata = new Dictionary - { - { "region", _globalSettings.BaseServiceUri.CloudRegion } - }; - var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, new TaxInfo(), additionalSeats, 0, premiumAccessAddon - , additionalSmSeats, additionalServiceAccount); - - Customer customer = null; - Subscription subscription; - try - { - var customerCreateOptions = new CustomerCreateOptions - { - Description = org.DisplayBusinessName(), - Email = org.BillingEmail, - Metadata = stripeCustomerMetadata, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions - { - Name = org.SubscriberType(), - Value = GetFirstThirtyCharacters(org.SubscriberName()), - } - ], - }, - Coupon = signupIsFromSecretsManagerTrial - ? SecretsManagerStandaloneDiscountId - : null, - TaxIdData = null, - }; - - customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); - subCreateOptions.AddExpand("latest_invoice.payment_intent"); - subCreateOptions.Customer = customer.Id; - - subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating customer, walking back operation."); - if (customer != null) - { - await _stripeAdapter.CustomerDeleteAsync(customer.Id); - } - - throw; - } - - org.Gateway = GatewayType.Stripe; - org.GatewayCustomerId = customer.Id; - org.GatewaySubscriptionId = subscription.Id; - - if (subscription.Status == "incomplete" && - subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") - { - org.Enabled = false; - return subscription.LatestInvoice.PaymentIntent.ClientSecret; - } - - org.Enabled = true; - org.ExpirationDate = subscription.CurrentPeriodEnd; - return null; - - } - private async Task ChangeOrganizationSponsorship( Organization org, OrganizationSponsorship sponsorship, @@ -324,458 +87,6 @@ public class StripePaymentService : IPaymentService public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) => ChangeOrganizationSponsorship(org, sponsorship, false); - public async Task UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan, - OrganizationUpgrade upgrade) - { - if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)) - { - throw new BadRequestException("Organization already has a subscription."); - } - - var customerOptions = new CustomerGetOptions(); - customerOptions.AddExpand("default_source"); - customerOptions.AddExpand("invoice_settings.default_payment_method"); - customerOptions.AddExpand("tax"); - var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions); - if (customer == null) - { - throw new GatewayException("Could not find customer payment profile."); - } - - if (!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressCountry) && - !string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressPostalCode)) - { - var addressOptions = new AddressOptions - { - Country = upgrade.TaxInfo.BillingAddressCountry, - PostalCode = upgrade.TaxInfo.BillingAddressPostalCode, - // Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead. - Line1 = upgrade.TaxInfo.BillingAddressLine1 ?? string.Empty, - Line2 = upgrade.TaxInfo.BillingAddressLine2, - City = upgrade.TaxInfo.BillingAddressCity, - State = upgrade.TaxInfo.BillingAddressState, - }; - var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions }; - customerUpdateOptions.AddExpand("default_source"); - customerUpdateOptions.AddExpand("invoice_settings.default_payment_method"); - customerUpdateOptions.AddExpand("tax"); - customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions); - } - - var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); - - subCreateOptions.EnableAutomaticTax(customer); - - var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); - - var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, - stripePaymentMethod, paymentMethodType, subCreateOptions, null); - org.GatewaySubscriptionId = subscription.Id; - - if (subscription.Status == "incomplete" && - subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") - { - org.Enabled = false; - return subscription.LatestInvoice.PaymentIntent.ClientSecret; - } - else - { - org.Enabled = true; - org.ExpirationDate = subscription.CurrentPeriodEnd; - return null; - } - } - - private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod( - Customer customer, SubscriptionCreateOptions subCreateOptions) - { - var stripePaymentMethod = false; - var paymentMethodType = PaymentMethodType.Credit; - var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId"); - if (hasBtCustomerId) - { - paymentMethodType = PaymentMethodType.PayPal; - } - else - { - if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card") - { - paymentMethodType = PaymentMethodType.Card; - stripePaymentMethod = true; - } - else if (customer.DefaultSource != null) - { - if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard) - { - paymentMethodType = PaymentMethodType.Card; - stripePaymentMethod = true; - } - else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit) - { - paymentMethodType = PaymentMethodType.BankAccount; - stripePaymentMethod = true; - } - } - else - { - var paymentMethod = GetLatestCardPaymentMethod(customer.Id); - if (paymentMethod != null) - { - paymentMethodType = PaymentMethodType.Card; - stripePaymentMethod = true; - subCreateOptions.DefaultPaymentMethod = paymentMethod.Id; - } - } - } - return (stripePaymentMethod, paymentMethodType); - } - - public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, - string paymentToken, short additionalStorageGb, TaxInfo taxInfo) - { - if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken)) - { - throw new BadRequestException("Payment token is required."); - } - if (paymentMethodType == PaymentMethodType.Credit && - (user.Gateway != GatewayType.Stripe || string.IsNullOrWhiteSpace(user.GatewayCustomerId))) - { - throw new BadRequestException("Your account does not have any credit available."); - } - if (paymentMethodType is PaymentMethodType.BankAccount) - { - throw new GatewayException("Payment method is not supported at this time."); - } - - var createdStripeCustomer = false; - Customer customer = null; - Braintree.Customer braintreeCustomer = null; - var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount - or PaymentMethodType.Credit; - - string stipeCustomerPaymentMethodId = null; - string stipeCustomerSourceToken = null; - if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken)) - { - if (paymentToken.StartsWith("pm_")) - { - stipeCustomerPaymentMethodId = paymentToken; - } - else - { - stipeCustomerSourceToken = paymentToken; - } - } - - if (user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId)) - { - if (!string.IsNullOrWhiteSpace(paymentToken)) - { - await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo); - } - - try - { - var customerGetOptions = new CustomerGetOptions(); - customerGetOptions.AddExpand("tax"); - customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId, customerGetOptions); - } - catch - { - _logger.LogWarning( - "Attempted to get existing customer from Stripe, but customer ID was not found. Attempting to recreate customer..."); - } - } - - if (customer == null && !string.IsNullOrWhiteSpace(paymentToken)) - { - var stripeCustomerMetadata = new Dictionary - { - { "region", _globalSettings.BaseServiceUri.CloudRegion } - }; - if (paymentMethodType == PaymentMethodType.PayPal) - { - var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false); - var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest - { - PaymentMethodNonce = paymentToken, - Email = user.Email, - Id = user.BraintreeCustomerIdPrefix() + user.Id.ToString("N").ToLower() + randomSuffix, - CustomFields = new Dictionary - { - [user.BraintreeIdField()] = user.Id.ToString(), - [user.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion - } - }); - - if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) - { - throw new GatewayException("Failed to create PayPal customer record."); - } - - braintreeCustomer = customerResult.Target; - stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); - } - else if (!stripePaymentMethod) - { - throw new GatewayException("Payment method is not supported at this time."); - } - - var customerCreateOptions = new CustomerCreateOptions - { - Description = user.Name, - Email = user.Email, - Metadata = stripeCustomerMetadata, - PaymentMethod = stipeCustomerPaymentMethodId, - Source = stipeCustomerSourceToken, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = stipeCustomerPaymentMethodId, - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions() - { - Name = user.SubscriberType(), - Value = GetFirstThirtyCharacters(user.SubscriberName()), - } - - ] - }, - Address = new AddressOptions - { - Line1 = string.Empty, - Country = taxInfo.BillingAddressCountry, - PostalCode = taxInfo.BillingAddressPostalCode, - }, - }; - customerCreateOptions.AddExpand("tax"); - customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions); - createdStripeCustomer = true; - } - - if (customer == null) - { - throw new GatewayException("Could not set up customer payment profile."); - } - - var subCreateOptions = new SubscriptionCreateOptions - { - Customer = customer.Id, - Items = [], - Metadata = new Dictionary - { - [user.GatewayIdField()] = user.Id.ToString() - } - }; - - subCreateOptions.Items.Add(new SubscriptionItemOptions - { - Plan = PremiumPlanId, - Quantity = 1 - }); - - if (additionalStorageGb > 0) - { - subCreateOptions.Items.Add(new SubscriptionItemOptions - { - Plan = StoragePlanId, - Quantity = additionalStorageGb - }); - } - - subCreateOptions.EnableAutomaticTax(customer); - - var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer, - stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer); - - user.Gateway = GatewayType.Stripe; - user.GatewayCustomerId = customer.Id; - user.GatewaySubscriptionId = subscription.Id; - - if (subscription.Status == "incomplete" && - subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") - { - return subscription.LatestInvoice.PaymentIntent.ClientSecret; - } - - user.Premium = true; - user.PremiumExpirationDate = subscription.CurrentPeriodEnd; - return null; - } - - private async Task ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer, - bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType, - SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer) - { - var addedCreditToStripeCustomer = false; - Braintree.Transaction braintreeTransaction = null; - - var subInvoiceMetadata = new Dictionary(); - Subscription subscription = null; - try - { - if (!stripePaymentMethod) - { - var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions - { - Customer = customer.Id, - SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) - }); - - if (customer.HasTaxLocationVerified()) - { - previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; - } - - if (previewInvoice.AmountDue > 0) - { - var braintreeCustomerId = customer.Metadata != null && - customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null; - if (!string.IsNullOrWhiteSpace(braintreeCustomerId)) - { - var btInvoiceAmount = (previewInvoice.AmountDue / 100M); - var transactionResult = await _btGateway.Transaction.SaleAsync( - new Braintree.TransactionRequest - { - Amount = btInvoiceAmount, - CustomerId = braintreeCustomerId, - Options = new Braintree.TransactionOptionsRequest - { - SubmitForSettlement = true, - PayPal = new Braintree.TransactionOptionsPayPalRequest - { - CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}" - } - }, - CustomFields = new Dictionary - { - [subscriber.BraintreeIdField()] = subscriber.Id.ToString(), - [subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion - } - }); - - if (!transactionResult.IsSuccess()) - { - throw new GatewayException("Failed to charge PayPal customer."); - } - - braintreeTransaction = transactionResult.Target; - subInvoiceMetadata.Add("btTransactionId", braintreeTransaction.Id); - subInvoiceMetadata.Add("btPayPalTransactionId", - braintreeTransaction.PayPalDetails.AuthorizationId); - } - else - { - throw new GatewayException("No payment was able to be collected."); - } - - await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Balance = customer.Balance - previewInvoice.AmountDue - }); - addedCreditToStripeCustomer = true; - } - } - else if (paymentMethodType == PaymentMethodType.Credit) - { - var upcomingInvoiceOptions = new UpcomingInvoiceOptions - { - Customer = customer.Id, - SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items), - SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates, - }; - - upcomingInvoiceOptions.EnableAutomaticTax(customer, null); - - var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); - - if (previewInvoice.AmountDue > 0) - { - throw new GatewayException("Your account does not have enough credit available."); - } - } - - subCreateOptions.OffSession = true; - subCreateOptions.AddExpand("latest_invoice.payment_intent"); - - subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); - if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) - { - if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method") - { - await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions()); - throw new GatewayException("Payment method was declined."); - } - } - - if (!stripePaymentMethod && subInvoiceMetadata.Any()) - { - var invoices = await _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions - { - Subscription = subscription.Id - }); - - var invoice = invoices?.FirstOrDefault(); - if (invoice == null) - { - throw new GatewayException("Invoice not found."); - } - - await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions - { - Metadata = subInvoiceMetadata - }); - } - - return subscription; - } - catch (Exception e) - { - if (customer != null) - { - if (createdStripeCustomer) - { - await _stripeAdapter.CustomerDeleteAsync(customer.Id); - } - else if (addedCreditToStripeCustomer || customer.Balance < 0) - { - await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Balance = customer.Balance - }); - } - } - if (braintreeTransaction != null) - { - await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id); - } - if (braintreeCustomer != null) - { - await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); - } - - if (e is StripeException strEx && - (strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false)) - { - throw new GatewayException("Bank account is not yet verified."); - } - - throw; - } - } - - private List ToInvoiceSubscriptionItemOptions( - List subItemOptions) - { - return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions - { - Plan = si.Plan, - Price = si.Price, - Quantity = si.Quantity, - Id = si.Id - }).ToList(); - } - private async Task FinalizeSubscriptionChangeAsync(ISubscriber subscriber, SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false) { @@ -1400,7 +711,7 @@ public class StripePaymentService : IPaymentService new CustomerInvoiceSettingsCustomFieldOptions() { Name = subscriber.SubscriberType(), - Value = GetFirstThirtyCharacters(subscriber.SubscriberName()), + Value = subscriber.GetFormattedInvoiceName() } ] @@ -1492,7 +803,7 @@ public class StripePaymentService : IPaymentService new CustomerInvoiceSettingsCustomFieldOptions() { Name = subscriber.SubscriberType(), - Value = GetFirstThirtyCharacters(subscriber.SubscriberName()) + Value = subscriber.GetFormattedInvoiceName() } ] }, @@ -1560,7 +871,7 @@ public class StripePaymentService : IPaymentService var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions()); var billingInfo = new BillingInfo { - Balance = GetBillingBalance(customer), + Balance = customer.GetBillingBalance(), PaymentSource = await GetBillingPaymentSourceAsync(customer) }; @@ -1768,27 +1079,6 @@ public class StripePaymentService : IPaymentService new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), true); - public async Task RisksSubscriptionFailure(Organization organization) - { - var subscriptionInfo = await GetSubscriptionAsync(organization); - - if (subscriptionInfo.Subscription is not - { - Status: "active" or "trialing" or "past_due", - CollectionMethod: "charge_automatically" - } - || subscriptionInfo.UpcomingInvoice == null) - { - return false; - } - - var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions()); - - var paymentSource = await GetBillingPaymentSourceAsync(customer); - - return paymentSource == null; - } - public async Task HasSecretsManagerStandalone(Organization organization) { if (string.IsNullOrEmpty(organization.GatewayCustomerId)) @@ -1801,7 +1091,7 @@ public class StripePaymentService : IPaymentService return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId; } - public async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription) + private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription) { if (subscription.Status is not "past_due" && subscription.Status is not "unpaid") { @@ -2117,11 +1407,6 @@ public class StripePaymentService : IPaymentService return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault(); } - private decimal GetBillingBalance(Customer customer) - { - return customer != null ? customer.Balance / 100M : default; - } - private async Task GetBillingPaymentSourceAsync(Customer customer) { if (customer == null) @@ -2252,18 +1537,4 @@ public class StripePaymentService : IPaymentService throw new GatewayException("Failed to retrieve current invoices", exception); } } - - // We are taking only first 30 characters of the SubscriberName because stripe provide - // for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create - private static string GetFirstThirtyCharacters(string subscriberName) - { - if (string.IsNullOrWhiteSpace(subscriberName)) - { - return string.Empty; - } - - return subscriberName.Length <= 30 - ? subscriberName - : subscriberName[..30]; - } } diff --git a/test/Core.Test/Extensions/SubscriberExtensionsTests.cs b/test/Core.Test/Extensions/SubscriberExtensionsTests.cs new file mode 100644 index 0000000000..e0b4cfd9f2 --- /dev/null +++ b/test/Core.Test/Extensions/SubscriberExtensionsTests.cs @@ -0,0 +1,23 @@ +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Extensions; +using Xunit; + +namespace Bit.Core.Test.Extensions; + +public class SubscriberExtensionsTests +{ + [Theory] + [InlineData("Alexandria Villanueva Gonzalez Pablo", "Alexandria Villanueva Gonzalez")] + [InlineData("John Snow", "John Snow")] + public void GetFormattedInvoiceName_Returns_FirstThirtyCaractersOfName(string name, string expected) + { + // arrange + var provider = new Provider { Name = name }; + + // act + var actual = provider.GetFormattedInvoiceName(); + + // assert + Assert.Equal(expected, actual); + } +} diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs deleted file mode 100644 index 11a19656e1..0000000000 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ /dev/null @@ -1,828 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Services; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Models.Business; -using Bit.Core.Services; -using Bit.Core.Settings; -using Bit.Core.Utilities; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Braintree; -using NSubstitute; -using Xunit; -using Customer = Braintree.Customer; -using PaymentMethod = Braintree.PaymentMethod; -using PaymentMethodType = Bit.Core.Enums.PaymentMethodType; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class StripePaymentServiceTests -{ - [Theory] - [BitAutoData(PaymentMethodType.BitPay)] - [BitAutoData(PaymentMethodType.BitPay)] - [BitAutoData(PaymentMethodType.Credit)] - [BitAutoData(PaymentMethodType.WireTransfer)] - [BitAutoData(PaymentMethodType.Check)] - public async Task PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.PurchaseOrganizationAsync(null, paymentMethodType, null, null, 0, 0, false, null, false, -1, -1)); - - Assert.Equal("Payment method is not supported at this time.", exception.Message); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - }); - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, provider); - - Assert.Null(result); - Assert.Equal(GatewayType.Stripe, organization.Gateway); - Assert.Equal("C-1", organization.GatewayCustomerId); - Assert.Equal("S-1", organization.GatewaySubscriptionId); - Assert.True(organization.Enabled); - Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); - - await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => - c.Description == organization.BusinessName && - c.Email == organization.BillingEmail && - c.Source == paymentToken && - c.PaymentMethod == null && - c.Coupon == "msp-discount-35" && - c.Metadata.Count == 1 && - c.Metadata["region"] == "US" && - c.InvoiceSettings.DefaultPaymentMethod == null && - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType - )); - - await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => - s.Customer == "C-1" && - s.Expand[0] == "latest_invoice.payment_intent" && - s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && - s.Items.Count == 0 - )); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_SM_Stripe_ProviderOrg_Coupon_Add(SutProvider sutProvider, Organization organization, - string paymentToken, TaxInfo taxInfo, bool provider = true) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - organization.UseSecretsManager = true; - - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - - }); - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 1, 1, - false, taxInfo, provider, 1, 1); - - Assert.Null(result); - Assert.Equal(GatewayType.Stripe, organization.Gateway); - Assert.Equal("C-1", organization.GatewayCustomerId); - Assert.Equal("S-1", organization.GatewaySubscriptionId); - Assert.True(organization.Enabled); - Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); - - await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => - c.Description == organization.BusinessName && - c.Email == organization.BillingEmail && - c.Source == paymentToken && - c.PaymentMethod == null && - c.Coupon == "msp-discount-35" && - c.Metadata.Count == 1 && - c.Metadata["region"] == "US" && - c.InvoiceSettings.DefaultPaymentMethod == null && - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType - )); - - await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => - s.Customer == "C-1" && - s.Expand[0] == "latest_invoice.payment_intent" && - s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && - s.Items.Count == 4 - )); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Stripe(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - organization.UseSecretsManager = true; - - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - }); - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0 - , false, taxInfo, false, 8, 10); - - Assert.Null(result); - Assert.Equal(GatewayType.Stripe, organization.Gateway); - Assert.Equal("C-1", organization.GatewayCustomerId); - Assert.Equal("S-1", organization.GatewaySubscriptionId); - Assert.True(organization.Enabled); - Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); - await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => - c.Description == organization.BusinessName && - c.Email == organization.BillingEmail && - c.Source == paymentToken && - c.PaymentMethod == null && - c.Metadata.Count == 1 && - c.Metadata["region"] == "US" && - c.InvoiceSettings.DefaultPaymentMethod == null && - c.InvoiceSettings.CustomFields != null && - c.InvoiceSettings.CustomFields[0].Name == "Organization" && - c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) && - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType - )); - - await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => - s.Customer == "C-1" && - s.Expand[0] == "latest_invoice.payment_intent" && - s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && - s.Items.Count == 2 - )); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Stripe_PM(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - paymentToken = "pm_" + paymentToken; - - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - }); - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo); - - Assert.Null(result); - Assert.Equal(GatewayType.Stripe, organization.Gateway); - Assert.Equal("C-1", organization.GatewayCustomerId); - Assert.Equal("S-1", organization.GatewaySubscriptionId); - Assert.True(organization.Enabled); - Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); - - await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => - c.Description == organization.BusinessName && - c.Email == organization.BillingEmail && - c.Source == null && - c.PaymentMethod == paymentToken && - c.Metadata.Count == 1 && - c.Metadata["region"] == "US" && - c.InvoiceSettings.DefaultPaymentMethod == paymentToken && - c.InvoiceSettings.CustomFields != null && - c.InvoiceSettings.CustomFields[0].Name == "Organization" && - c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) && - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType - )); - - await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => - s.Customer == "C-1" && - s.Expand[0] == "latest_invoice.payment_intent" && - s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && - s.Items.Count == 0 - )); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - paymentToken = "pm_" + paymentToken; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - Status = "incomplete", - LatestInvoice = new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent - { - Status = "requires_payment_method", - }, - }, - }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo)); - - Assert.Equal("Payment method was declined.", exception.Message); - - await stripeAdapter.Received(1).CustomerDeleteAsync("C-1"); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_SM_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - paymentToken = "pm_" + paymentToken; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - Status = "incomplete", - LatestInvoice = new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent - { - Status = "requires_payment_method", - }, - }, - }); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, - 1, 12, false, taxInfo, false, 10, 10)); - - Assert.Equal("Payment method was declined.", exception.Message); - - await stripeAdapter.Received(1).CustomerDeleteAsync("C-1"); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - Status = "incomplete", - LatestInvoice = new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent - { - Status = "requires_action", - ClientSecret = "clientSecret", - }, - }, - }); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo); - - Assert.Equal("clientSecret", result); - Assert.False(organization.Enabled); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_SM_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - Status = "incomplete", - LatestInvoice = new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent - { - Status = "requires_action", - ClientSecret = "clientSecret", - }, - }, - }); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, - 10, 10, false, taxInfo, false, 10, 10); - - Assert.Equal("clientSecret", result); - Assert.False(organization.Enabled); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Paypal(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - }); - - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - - var customer = Substitute.For(); - customer.Id.ReturnsForAnyArgs("Braintree-Id"); - customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() }); - var customerResult = Substitute.For>(); - customerResult.IsSuccess().Returns(true); - customerResult.Target.ReturnsForAnyArgs(customer); - - var braintreeGateway = sutProvider.GetDependency(); - braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); - - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo); - - Assert.Null(result); - Assert.Equal(GatewayType.Stripe, organization.Gateway); - Assert.Equal("C-1", organization.GatewayCustomerId); - Assert.Equal("S-1", organization.GatewaySubscriptionId); - Assert.True(organization.Enabled); - Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); - - await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => - c.Description == organization.BusinessName && - c.Email == organization.BillingEmail && - c.PaymentMethod == null && - c.Metadata.Count == 2 && - c.Metadata["btCustomerId"] == "Braintree-Id" && - c.Metadata["region"] == "US" && - c.InvoiceSettings.DefaultPaymentMethod == null && - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType - )); - - await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => - s.Customer == "C-1" && - s.Expand[0] == "latest_invoice.payment_intent" && - s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && - s.Items.Count == 0 - )); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_SM_Paypal(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - organization.UseSecretsManager = true; - - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - }); - - var customer = Substitute.For(); - customer.Id.ReturnsForAnyArgs("Braintree-Id"); - customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() }); - var customerResult = Substitute.For>(); - customerResult.IsSuccess().Returns(true); - customerResult.Target.ReturnsForAnyArgs(customer); - - var braintreeGateway = sutProvider.GetDependency(); - braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); - - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - - var additionalStorage = (short)2; - var additionalSeats = 10; - var additionalSmSeats = 5; - var additionalServiceAccounts = 20; - var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, - additionalStorage, additionalSeats, false, taxInfo, false, additionalSmSeats, additionalServiceAccounts); - - Assert.Null(result); - Assert.Equal(GatewayType.Stripe, organization.Gateway); - Assert.Equal("C-1", organization.GatewayCustomerId); - Assert.Equal("S-1", organization.GatewaySubscriptionId); - Assert.True(organization.Enabled); - Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); - - await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => - c.Description == organization.BusinessName && - c.Email == organization.BillingEmail && - c.PaymentMethod == null && - c.Metadata.Count == 2 && - c.Metadata["region"] == "US" && - c.Metadata["btCustomerId"] == "Braintree-Id" && - c.InvoiceSettings.DefaultPaymentMethod == null && - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState && - c.TaxIdData.First().Value == taxInfo.TaxIdNumber && - c.TaxIdData.First().Type == taxInfo.TaxIdType - )); - - await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => - s.Customer == "C-1" && - s.Expand[0] == "latest_invoice.payment_intent" && - s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && - s.Items.Count == 4 && - s.Items.Count(i => i.Plan == plan.PasswordManager.StripeSeatPlanId && i.Quantity == additionalSeats) == 1 && - s.Items.Count(i => i.Plan == plan.PasswordManager.StripeStoragePlanId && i.Quantity == additionalStorage) == 1 && - s.Items.Count(i => i.Plan == plan.SecretsManager.StripeSeatPlanId && i.Quantity == additionalSmSeats) == 1 && - s.Items.Count(i => i.Plan == plan.SecretsManager.StripeServiceAccountPlanId && i.Quantity == additionalServiceAccounts) == 1 - )); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - var customerResult = Substitute.For>(); - customerResult.IsSuccess().Returns(false); - - var braintreeGateway = sutProvider.GetDependency(); - braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo)); - - Assert.Equal("Failed to create PayPal customer record.", exception.Message); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_SM_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - var customerResult = Substitute.For>(); - customerResult.IsSuccess().Returns(false); - - var braintreeGateway = sutProvider.GetDependency(); - braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, - 1, 1, false, taxInfo, false, 8, 8)); - - Assert.Equal("Failed to create PayPal customer record.", exception.Message); - } - - [Theory, BitAutoData] - public async Task PurchaseOrganizationAsync_PayPal_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - var plans = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - paymentToken = "pm_" + paymentToken; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - Status = "incomplete", - LatestInvoice = new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent - { - Status = "requires_payment_method", - }, - }, - }); - - var customer = Substitute.For(); - customer.Id.ReturnsForAnyArgs("Braintree-Id"); - customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() }); - var customerResult = Substitute.For>(); - customerResult.IsSuccess().Returns(true); - customerResult.Target.ReturnsForAnyArgs(customer); - - var braintreeGateway = sutProvider.GetDependency(); - braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo)); - - Assert.Equal("Payment method was declined.", exception.Message); - - await stripeAdapter.Received(1).CustomerDeleteAsync("C-1"); - await braintreeGateway.Customer.Received(1).DeleteAsync("Braintree-Id"); - } - - [Theory] - [BitAutoData("ES", "A5372895732985327895237")] - public async Task PurchaseOrganizationAsync_ThrowsBadRequestException_WhenTaxIdInvalid(string country, string taxId, SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) - { - taxInfo.BillingAddressCountry = country; - taxInfo.TaxIdNumber = taxId; - taxInfo.TaxIdType = null; - - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - organization.UseSecretsManager = true; - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription - { - Id = "S-1", - CurrentPeriodEnd = DateTime.Today.AddDays(10), - }); - sutProvider.GetDependency() - .BaseServiceUri.CloudRegion - .Returns("US"); - sutProvider - .GetDependency() - .GetStripeTaxCode(Arg.Is(p => p == country), Arg.Is(p => p == taxId)) - .Returns((string)null); - - var actual = await Assert.ThrowsAsync(async () => await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, false, 8, 10)); - - Assert.Equal("billingTaxIdTypeInferenceError", actual.Message); - - await stripeAdapter.Received(0).CustomerCreateAsync(Arg.Any()); - await stripeAdapter.Received(0).SubscriptionCreateAsync(Arg.Any()); - } - - - [Theory, BitAutoData] - public async Task UpgradeFreeOrganizationAsync_Success(SutProvider sutProvider, - Organization organization, TaxInfo taxInfo) - { - organization.GatewaySubscriptionId = null; - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - Metadata = new Dictionary - { - { "btCustomerId", "B-123" }, - } - }); - stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - Metadata = new Dictionary - { - { "btCustomerId", "B-123" }, - } - }); - stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", }, - AmountDue = 0 - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { }); - - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - - var upgrade = new OrganizationUpgrade() - { - AdditionalStorageGb = 0, - AdditionalSeats = 0, - PremiumAccessAddon = false, - TaxInfo = taxInfo, - AdditionalSmSeats = 0, - AdditionalServiceAccounts = 0 - }; - var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade); - - Assert.Null(result); - } - - [Theory, BitAutoData] - public async Task UpgradeFreeOrganizationAsync_SM_Success(SutProvider sutProvider, - Organization organization, TaxInfo taxInfo) - { - organization.GatewaySubscriptionId = null; - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - Metadata = new Dictionary - { - { "btCustomerId", "B-123" }, - } - }); - stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - Metadata = new Dictionary - { - { "btCustomerId", "B-123" }, - } - }); - stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", }, - AmountDue = 0 - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { }); - - var upgrade = new OrganizationUpgrade() - { - AdditionalStorageGb = 1, - AdditionalSeats = 10, - PremiumAccessAddon = false, - TaxInfo = taxInfo, - AdditionalSmSeats = 5, - AdditionalServiceAccounts = 50 - }; - - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade); - - Assert.Null(result); - } - - [Theory, BitAutoData] - public async Task UpgradeFreeOrganizationAsync_WhenCustomerHasNoAddress_UpdatesCustomerAddressWithTaxInfo( - SutProvider sutProvider, - Organization organization, - TaxInfo taxInfo) - { - organization.GatewaySubscriptionId = null; - var stripeAdapter = sutProvider.GetDependency(); - var featureService = sutProvider.GetDependency(); - stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - Metadata = new Dictionary - { - { "btCustomerId", "B-123" }, - } - }); - stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer - { - Id = "C-1", - Metadata = new Dictionary - { - { "btCustomerId", "B-123" }, - } - }); - stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice - { - PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", }, - AmountDue = 0 - }); - stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { }); - - var upgrade = new OrganizationUpgrade() - { - AdditionalStorageGb = 1, - AdditionalSeats = 10, - PremiumAccessAddon = false, - TaxInfo = taxInfo, - AdditionalSmSeats = 5, - AdditionalServiceAccounts = 50 - }; - - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); - _ = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade); - - await stripeAdapter.Received() - .CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(c => - c.Address.Country == taxInfo.BillingAddressCountry && - c.Address.PostalCode == taxInfo.BillingAddressPostalCode && - c.Address.Line1 == taxInfo.BillingAddressLine1 && - c.Address.Line2 == taxInfo.BillingAddressLine2 && - c.Address.City == taxInfo.BillingAddressCity && - c.Address.State == taxInfo.BillingAddressState)); - } -} From 34358acf61035d250ebcdde4fc8cf9324f902659 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:09:54 +0100 Subject: [PATCH 60/84] Fix user context on importing into individual vaults (#5465) Pass in the current userId instead of trying to infer it from the folders or ciphers passed into the ImportCiphersCommand Kudos go to @MJebran who pointed this out on https://github.com/bitwarden/server/pull/4896 Co-authored-by: Daniel James Smith --- .../Tools/Controllers/ImportCiphersController.cs | 2 +- .../Tools/ImportFeatures/ImportCiphersCommand.cs | 14 +++++--------- .../Interfaces/IImportCiphersCommand.cs | 2 +- .../Controllers/ImportCiphersControllerTests.cs | 3 ++- .../ImportCiphersAsyncCommandTests.cs | 4 ++-- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index d6104de354..62c55aceb8 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -56,7 +56,7 @@ public class ImportCiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList(); var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList(); - await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships); + await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships, userId); } [HttpPost("import-organization")] diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 646121db52..59d3e5be34 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -54,12 +54,11 @@ public class ImportCiphersCommand : IImportCiphersCommand public async Task ImportIntoIndividualVaultAsync( List folders, List ciphers, - IEnumerable> folderRelationships) + IEnumerable> folderRelationships, + Guid importingUserId) { - var userId = folders.FirstOrDefault()?.UserId ?? ciphers.FirstOrDefault()?.UserId; - // Make sure the user can save new ciphers to their personal vault - var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, PolicyType.PersonalOwnership); + var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership); if (anyPersonalOwnershipPolicies) { throw new BadRequestException("You cannot import items into your personal vault because you are " + @@ -76,7 +75,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } } - var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(userId ?? Guid.Empty)).Select(f => f.Id).ToList(); + var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList(); //Assign id to the ones that don't exist in DB //Need to keep the list order to create the relationships @@ -109,10 +108,7 @@ public class ImportCiphersCommand : IImportCiphersCommand await _cipherRepository.CreateAsync(ciphers, newFolders); // push - if (userId.HasValue) - { - await _pushService.PushSyncVaultAsync(userId.Value); - } + await _pushService.PushSyncVaultAsync(importingUserId); } public async Task ImportIntoOrganizationalVaultAsync( diff --git a/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs index 378024d3a0..732b2f43a8 100644 --- a/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs @@ -7,7 +7,7 @@ namespace Bit.Core.Tools.ImportFeatures.Interfaces; public interface IImportCiphersCommand { Task ImportIntoIndividualVaultAsync(List folders, List ciphers, - IEnumerable> folderRelationships); + IEnumerable> folderRelationships, Guid importingUserId); Task ImportIntoOrganizationalVaultAsync(List collections, List ciphers, IEnumerable> collectionRelationships, Guid importingUserId); diff --git a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs index c07f9791a3..76055a6b64 100644 --- a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs @@ -79,7 +79,8 @@ public class ImportCiphersControllerTests .ImportIntoIndividualVaultAsync( Arg.Any>(), Arg.Any>(), - Arg.Any>>() + Arg.Any>>(), + user.Id ); } diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 1e97856281..5e7a30d814 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -44,7 +44,7 @@ public class ImportCiphersAsyncCommandTests var folderRelationships = new List>(); // Act - await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships); + await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); // Assert await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>()); @@ -68,7 +68,7 @@ public class ImportCiphersAsyncCommandTests var folderRelationships = new List>(); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships)); + sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId)); Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message); } From bd7a0a8ed854afbc366de5ed328bec1137759bbb Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:56:04 -0400 Subject: [PATCH 61/84] Codespaces improvements (#4969) * Skip one_time_setup in GH Codespaces * Make .env File Optional * Wrap Path in Single Quotes * Comment out .env File * Add Modify Database Task * Work on modify_database.ps1 * Add space * Remove compose version * Do changes in community as well * Do required: false * Reverse check * Remove printenv * Skip DB changes * Remove docker outside of docker feature * Remove newlines --- .devcontainer/bitwarden_common/docker-compose.yml | 5 ++--- .devcontainer/community_dev/postCreateCommand.sh | 8 +++++++- .devcontainer/internal_dev/docker-compose.override.yml | 2 -- .devcontainer/internal_dev/postCreateCommand.sh | 8 +++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.devcontainer/bitwarden_common/docker-compose.yml b/.devcontainer/bitwarden_common/docker-compose.yml index 52f0901c70..2f3a62877e 100644 --- a/.devcontainer/bitwarden_common/docker-compose.yml +++ b/.devcontainer/bitwarden_common/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: bitwarden_server: image: mcr.microsoft.com/devcontainers/dotnet:8.0 @@ -13,7 +11,8 @@ services: platform: linux/amd64 restart: unless-stopped env_file: - ../../dev/.env + - path: ../../dev/.env + required: false environment: ACCEPT_EULA: "Y" MSSQL_PID: Developer diff --git a/.devcontainer/community_dev/postCreateCommand.sh b/.devcontainer/community_dev/postCreateCommand.sh index 832f510f3f..8f1813ed78 100755 --- a/.devcontainer/community_dev/postCreateCommand.sh +++ b/.devcontainer/community_dev/postCreateCommand.sh @@ -51,4 +51,10 @@ Proceed? [y/N] " response } # main -one_time_setup +if [[ -z "${CODESPACES}" ]]; then + one_time_setup +else + # Ignore interactive elements when running in codespaces since they are not supported there + # TODO Write codespaces specific instructions and link here + echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup" +fi diff --git a/.devcontainer/internal_dev/docker-compose.override.yml b/.devcontainer/internal_dev/docker-compose.override.yml index 9aaee9ee62..acf7b0b66e 100644 --- a/.devcontainer/internal_dev/docker-compose.override.yml +++ b/.devcontainer/internal_dev/docker-compose.override.yml @@ -1,5 +1,3 @@ -version: '3' - services: bitwarden_storage: image: mcr.microsoft.com/azure-storage/azurite:latest diff --git a/.devcontainer/internal_dev/postCreateCommand.sh b/.devcontainer/internal_dev/postCreateCommand.sh index 668b776447..071ffc0b29 100755 --- a/.devcontainer/internal_dev/postCreateCommand.sh +++ b/.devcontainer/internal_dev/postCreateCommand.sh @@ -89,4 +89,10 @@ install_stripe_cli() { } # main -one_time_setup +if [[ -z "${CODESPACES}" ]]; then + one_time_setup +else + # Ignore interactive elements when running in codespaces since they are not supported there + # TODO Write codespaces specific instructions and link here + echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup" +fi \ No newline at end of file From f26f14165ca9067c67c0f66a5acbbfbe596f1cad Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 10 Mar 2025 10:28:50 +0000 Subject: [PATCH 62/84] Bumped version to 2025.3.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 03594371e9..a994b2196e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.2.4 + 2025.3.0 Bit.$(MSBuildProjectName) enable From 88e91734f10989d25c959e2ee812399d42fb6b0c Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:46:44 +0100 Subject: [PATCH 63/84] [PM-17594]Remove feature flag self-host license refactor (#5372) * Remove the feature flag Signed-off-by: Cy Okeke * Resolve the failing test Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- src/Core/Constants.cs | 1 - .../Cloud/CloudGetOrganizationLicenseQuery.cs | 5 +---- src/Core/Services/Implementations/UserService.cs | 5 +---- .../CloudGetOrganizationLicenseQueryTests.cs | 4 +--- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 672188ce1f..b1cbc8d519 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -155,7 +155,6 @@ public static class FeatureFlagKeys public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string InlineMenuTotp = "inline-menu-totp"; - public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor"; public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string AppReviewPrompt = "app-review-prompt"; public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs index 0c3bfe16cf..44edde1495 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -42,10 +42,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer var subscriptionInfo = await GetSubscriptionAsync(organization); var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version); - if (_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor)) - { - license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo); - } + license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo); return license; } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index e419b832a7..5076c8282e 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1218,10 +1218,7 @@ public class UserService : UserManager, IUserService, IDisposable ? new UserLicense(user, _licenseService) : new UserLicense(user, subscriptionInfo, _licenseService); - if (_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor)) - { - userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo); - } + userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo); return userLicense; } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs index 650d33f64c..cc8ab956ca 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -56,7 +56,6 @@ public class CloudGetOrganizationLicenseQueryTests sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); sutProvider.GetDependency().GetSubscriptionAsync(organization).Returns(subInfo); sutProvider.GetDependency().SignLicense(Arg.Any()).Returns(licenseSignature); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor).Returns(false); var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId); @@ -64,7 +63,7 @@ public class CloudGetOrganizationLicenseQueryTests Assert.Equal(organization.Id, result.Id); Assert.Equal(installationId, result.InstallationId); Assert.Equal(licenseSignature, result.SignatureBytes); - Assert.Null(result.Token); + Assert.Equal(string.Empty, result.Token); } [Theory] @@ -77,7 +76,6 @@ public class CloudGetOrganizationLicenseQueryTests sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); sutProvider.GetDependency().GetSubscriptionAsync(organization).Returns(subInfo); sutProvider.GetDependency().SignLicense(Arg.Any()).Returns(licenseSignature); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor).Returns(true); sutProvider.GetDependency() .CreateOrganizationTokenAsync(organization, installationId, subInfo) .Returns(token); From 6e7c5b172ceb5ddad51126ab4ce5b1cd1c7e7da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 10 Mar 2025 15:27:30 +0000 Subject: [PATCH 64/84] [PM-18087] Add cipher permissions to response models (#5418) * Add Manage permission to UserCipherDetails and CipherDetails_ReadByIdUserId * Add Manage property to CipherDetails and UserCipherDetailsQuery * Add integration test for CipherRepository Manage permission rules * Update CipherDetails_ReadWithoutOrganizationsByUserId to include Manage permission * Refactor UserCipherDetailsQuery to include detailed permission and organization properties * Refactor CipherRepositoryTests to improve test organization and readability - Split large test method into smaller, focused methods - Added helper methods for creating test data and performing assertions - Improved test coverage for cipher permissions in different scenarios - Maintained existing test logic while enhancing code structure * Refactor CipherRepositoryTests to consolidate cipher permission tests - Removed redundant helper methods for permission assertions - Simplified test methods for GetCipherPermissionsForOrganizationAsync, GetManyByUserIdAsync, and GetByIdAsync - Maintained existing test coverage for cipher manage permissions - Improved code readability and reduced code duplication * Add integration test for CipherRepository group collection manage permissions - Added new test method GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules - Implemented helper method CreateCipherInOrganizationCollectionWithGroup to support group-based collection permission testing - Verified manage permissions are correctly applied based on group collection access settings * Add @Manage parameter to Cipher stored procedures - Updated CipherDetails_Create, CipherDetails_CreateWithCollections, and CipherDetails_Update stored procedures - Added @Manage parameter with comment "-- not used" - Included new stored procedure implementations in migration script - Consistent with previous work on adding Manage property to cipher details * Update UserCipherDetails functions to reorder Manage and ViewPassword columns * [PM-18086] Add CanRestore and CanDelete authorization methods. * [PM-18086] Address code review feedback. * [PM-18086] Add missing part. * [PM-18087] Add CipherPermissionsResponseModel for cipher permissions * Add GetManyOrganizationAbilityAsync method to application cache service * Add organization ability context to cipher response models This change introduces organization ability context to various cipher response models across multiple controllers. The modifications include: - Updating CipherResponseModel to include permissions based on user and organization ability - Modifying CiphersController methods to fetch and pass organization abilities - Updating SyncController to include organization abilities in sync response - Adding organization ability context to EmergencyAccessController response generation * Remove organization ability context from EmergencyAccessController This change simplifies the EmergencyAccessController by removing unnecessary organization ability fetching and passing. Since emergency access only retrieves personal ciphers, the organization ability context is no longer needed in the response generation. * Remove unused IApplicationCacheService from EmergencyAccessController * Refactor EmergencyAccessViewResponseModel constructor Remove unnecessary JsonConstructor attribute and simplify constructor initialization for EmergencyAccessViewResponseModel * Refactor organization ability retrieval in CiphersController Extract methods to simplify organization ability fetching for ciphers, reducing code duplication and improving readability. Added two private helper methods: - GetOrganizationAbilityAsync: Retrieves organization ability for a single cipher - GetManyOrganizationAbilitiesAsync: Retrieves organization abilities for multiple ciphers * Update CiphersControllerTests to use GetUserByPrincipalAsync Modify test methods to: - Replace GetProperUserId with GetUserByPrincipalAsync - Use User object instead of separate userId - Update mocking to return User object - Ensure user ID is correctly set in test scenarios * Refactor CipherPermissionsResponseModel to use constructor-based initialization * Refactor CipherPermissionsResponseModel to use record type and init-only properties * [PM-18086] Undo files * [PM-18086] Undo files * Refactor organization abilities retrieval in cipher-related controllers and models - Update CiphersController to use GetOrganizationAbilitiesAsync instead of individual methods - Modify CipherResponseModel and CipherDetailsResponseModel to accept organization abilities dictionary - Update CipherPermissionsResponseModel to handle organization abilities lookup - Remove deprecated organization ability retrieval methods - Simplify sync and emergency access response model handling of organization abilities * Remove GetManyOrganizationAbilityAsync method - Delete unused method from IApplicationCacheService interface - Remove corresponding implementation in InMemoryApplicationCacheService - Continues cleanup of organization ability retrieval methods * Update CiphersControllerTests to include organization abilities retrieval - Add organization abilities retrieval in test setup for PutCollections_vNext method - Ensure consistent mocking of IApplicationCacheService in test scenarios * Update error message for missing organization ability --------- Co-authored-by: Jimmy Vo --- .../Controllers/EmergencyAccessController.cs | 2 +- .../Response/EmergencyAccessResponseModel.cs | 10 +- .../Vault/Controllers/CiphersController.cs | 174 ++++++++++++------ src/Api/Vault/Controllers/SyncController.cs | 9 +- .../CipherPermissionsResponseModel.cs | 27 +++ .../Models/Response/CipherResponseModel.cs | 35 +++- .../Models/Response/SyncResponseModel.cs | 10 +- .../Controllers/CiphersControllerTests.cs | 18 +- 8 files changed, 206 insertions(+), 79 deletions(-) create mode 100644 src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 9f8ea3df01..5d1f47de73 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -167,7 +167,7 @@ public class EmergencyAccessController : Controller { var user = await _userService.GetUserByPrincipalAsync(User); var viewResult = await _emergencyAccessService.ViewAsync(id, user); - return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers); + return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user); } [HttpGet("{id}/{cipherId}/attachment/{attachmentId}")] diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index a72f3cf03f..2fb9a67199 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -116,11 +116,17 @@ public class EmergencyAccessViewResponseModel : ResponseModel public EmergencyAccessViewResponseModel( IGlobalSettings globalSettings, EmergencyAccess emergencyAccess, - IEnumerable ciphers) + IEnumerable ciphers, + User user) : base("emergencyAccessView") { KeyEncrypted = emergencyAccess.KeyEncrypted; - Ciphers = ciphers.Select(c => new CipherResponseModel(c, globalSettings)); + Ciphers = ciphers.Select(cipher => + new CipherResponseModel( + cipher, + user, + organizationAbilities: null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed + globalSettings)); } public string KeyEncrypted { get; set; } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 5a7d427963..62f07005ee 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -79,14 +79,16 @@ public class CiphersController : Controller [HttpGet("{id}")] public async Task Get(Guid id) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null) { throw new NotFoundException(); } - return new CipherResponseModel(cipher, _globalSettings); + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + + return new CipherResponseModel(cipher, user, organizationAbilities, _globalSettings); } [HttpGet("{id}/admin")] @@ -109,32 +111,37 @@ public class CiphersController : Controller [HttpGet("{id}/details")] public async Task GetDetails(Guid id) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null) { throw new NotFoundException(); } - var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); - return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers); + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); + return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers); } [HttpGet("")] public async Task> Get() { - var userId = _userService.GetProperUserId(User).Value; + var user = await _userService.GetUserByPrincipalAsync(User); var hasOrgs = _currentContext.Organizations?.Any() ?? false; // TODO: Use hasOrgs proper for cipher listing here? - var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: true || hasOrgs); + var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true || hasOrgs); Dictionary> collectionCiphersGroupDict = null; if (hasOrgs) { - var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId); + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id); collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); } - - var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings, + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var responses = ciphers.Select(cipher => new CipherDetailsResponseModel( + cipher, + user, + organizationAbilities, + _globalSettings, collectionCiphersGroupDict)).ToList(); return new ListResponseModel(responses); } @@ -142,30 +149,38 @@ public class CiphersController : Controller [HttpPost("")] public async Task Post([FromBody] CipherRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = model.ToCipherDetails(userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = model.ToCipherDetails(user.Id); if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { throw new NotFoundException(); } - await _cipherService.SaveDetailsAsync(cipher, userId, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue); - var response = new CipherResponseModel(cipher, _globalSettings); + await _cipherService.SaveDetailsAsync(cipher, user.Id, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue); + var response = new CipherResponseModel( + cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); return response; } [HttpPost("create")] public async Task PostCreate([FromBody] CipherCreateRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = model.Cipher.ToCipherDetails(userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = model.Cipher.ToCipherDetails(user.Id); if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { throw new NotFoundException(); } - await _cipherService.SaveDetailsAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue); - var response = new CipherResponseModel(cipher, _globalSettings); + await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue); + var response = new CipherResponseModel( + cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); return response; } @@ -191,8 +206,8 @@ public class CiphersController : Controller [HttpPost("{id}")] public async Task Put(Guid id, [FromBody] CipherRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null) { throw new NotFoundException(); @@ -200,7 +215,7 @@ public class CiphersController : Controller ValidateClientVersionForFido2CredentialSupport(cipher); - var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList(); + var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList(); var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ? (Guid?)null : new Guid(model.OrganizationId); if (cipher.OrganizationId != modelOrgId) @@ -209,9 +224,13 @@ public class CiphersController : Controller "then try again."); } - await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), userId, model.LastKnownRevisionDate, collectionIds); + await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds); - var response = new CipherResponseModel(cipher, _globalSettings); + var response = new CipherResponseModel( + cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); return response; } @@ -278,7 +297,14 @@ public class CiphersController : Controller })); } - var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings)); + var user = await _userService.GetUserByPrincipalAsync(User); + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var responses = ciphers.Select(cipher => + new CipherDetailsResponseModel( + cipher, + user, + organizationAbilities, + _globalSettings)); return new ListResponseModel(responses); } @@ -572,12 +598,16 @@ public class CiphersController : Controller [HttpPost("{id}/partial")] public async Task PutPartial(Guid id, [FromBody] CipherPartialRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; + var user = await _userService.GetUserByPrincipalAsync(User); var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId); - await _cipherRepository.UpdatePartialAsync(id, userId, folderId, model.Favorite); + await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite); - var cipher = await GetByIdAsync(id, userId); - var response = new CipherResponseModel(cipher, _globalSettings); + var cipher = await GetByIdAsync(id, user.Id); + var response = new CipherResponseModel( + cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); return response; } @@ -585,9 +615,9 @@ public class CiphersController : Controller [HttpPost("{id}/share")] public async Task PutShare(Guid id, [FromBody] CipherShareRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; + var user = await _userService.GetUserByPrincipalAsync(User); var cipher = await _cipherRepository.GetByIdAsync(id); - if (cipher == null || cipher.UserId != userId || + if (cipher == null || cipher.UserId != user.Id || !await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId))) { throw new NotFoundException(); @@ -597,10 +627,14 @@ public class CiphersController : Controller var original = cipher.Clone(); await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId), - model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate); + model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate); - var sharedCipher = await GetByIdAsync(id, userId); - var response = new CipherResponseModel(sharedCipher, _globalSettings); + var sharedCipher = await GetByIdAsync(id, user.Id); + var response = new CipherResponseModel( + sharedCipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); return response; } @@ -608,8 +642,8 @@ public class CiphersController : Controller [HttpPost("{id}/collections")] public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null || !cipher.OrganizationId.HasValue || !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { @@ -617,20 +651,25 @@ public class CiphersController : Controller } await _cipherService.SaveCollectionsAsync(cipher, - model.CollectionIds.Select(c => new Guid(c)), userId, false); + model.CollectionIds.Select(c => new Guid(c)), user.Id, false); - var updatedCipher = await GetByIdAsync(id, userId); - var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); + var updatedCipher = await GetByIdAsync(id, user.Id); + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); - return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers); + return new CipherDetailsResponseModel( + updatedCipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings, + collectionCiphers); } [HttpPut("{id}/collections_v2")] [HttpPost("{id}/collections_v2")] public async Task PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null || !cipher.OrganizationId.HasValue || !await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword) { @@ -638,10 +677,10 @@ public class CiphersController : Controller } await _cipherService.SaveCollectionsAsync(cipher, - model.CollectionIds.Select(c => new Guid(c)), userId, false); + model.CollectionIds.Select(c => new Guid(c)), user.Id, false); - var updatedCipher = await GetByIdAsync(id, userId); - var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); + var updatedCipher = await GetByIdAsync(id, user.Id); + var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); // If a user removes the last Can Manage access of a cipher, the "updatedCipher" will return null // We will be returning an "Unavailable" property so the client knows the user can no longer access this var response = new OptionalCipherDetailsResponseModel() @@ -649,7 +688,12 @@ public class CiphersController : Controller Unavailable = updatedCipher is null, Cipher = updatedCipher is null ? null - : new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers) + : new CipherDetailsResponseModel( + updatedCipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings, + collectionCiphers) }; return response; } @@ -839,15 +883,19 @@ public class CiphersController : Controller [HttpPut("{id}/restore")] public async Task PutRestore(Guid id) { - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null) { throw new NotFoundException(); } - await _cipherService.RestoreAsync(cipher, userId); - return new CipherResponseModel(cipher, _globalSettings); + await _cipherService.RestoreAsync(cipher, user.Id); + return new CipherResponseModel( + cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); } [HttpPut("{id}/restore-admin")] @@ -996,10 +1044,10 @@ public class CiphersController : Controller [HttpPost("{id}/attachment/v2")] public async Task PostAttachment(Guid id, [FromBody] AttachmentRequestModel request) { - var userId = _userService.GetProperUserId(User).Value; + var user = await _userService.GetUserByPrincipalAsync(User); var cipher = request.AdminRequest ? await _cipherRepository.GetOrganizationDetailsByIdAsync(id) : - await GetByIdAsync(id, userId); + await GetByIdAsync(id, user.Id); if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue || !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })))) @@ -1013,13 +1061,17 @@ public class CiphersController : Controller } var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher, - request.Key, request.FileName, request.FileSize, request.AdminRequest, userId); + request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id); return new AttachmentUploadDataResponseModel { AttachmentId = attachmentId, Url = uploadUrl, FileUploadType = _attachmentStorageService.FileUploadType, - CipherResponse = request.AdminRequest ? null : new CipherResponseModel((CipherDetails)cipher, _globalSettings), + CipherResponse = request.AdminRequest ? null : new CipherResponseModel( + (CipherDetails)cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings), CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null, }; } @@ -1077,8 +1129,8 @@ public class CiphersController : Controller { ValidateAttachment(); - var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var user = await _userService.GetUserByPrincipalAsync(User); + var cipher = await GetByIdAsync(id, user.Id); if (cipher == null) { throw new NotFoundException(); @@ -1087,10 +1139,14 @@ public class CiphersController : Controller await Request.GetFileAsync(async (stream, fileName, key) => { await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, - Request.ContentLength.GetValueOrDefault(0), userId); + Request.ContentLength.GetValueOrDefault(0), user.Id); }); - return new CipherResponseModel(cipher, _globalSettings); + return new CipherResponseModel( + cipher, + user, + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings); } [HttpPost("{id}/attachment-admin")] diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index c08a5f86e0..1b8978fc65 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -36,6 +36,7 @@ public class SyncController : Controller private readonly ICurrentContext _currentContext; private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); private readonly IFeatureService _featureService; + private readonly IApplicationCacheService _applicationCacheService; public SyncController( IUserService userService, @@ -49,7 +50,8 @@ public class SyncController : Controller ISendRepository sendRepository, GlobalSettings globalSettings, ICurrentContext currentContext, - IFeatureService featureService) + IFeatureService featureService, + IApplicationCacheService applicationCacheService) { _userService = userService; _folderRepository = folderRepository; @@ -63,6 +65,7 @@ public class SyncController : Controller _globalSettings = globalSettings; _currentContext = currentContext; _featureService = featureService; + _applicationCacheService = applicationCacheService; } [HttpGet("")] @@ -104,7 +107,9 @@ public class SyncController : Controller var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id); var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); - var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + + var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities, organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; diff --git a/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs b/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs new file mode 100644 index 0000000000..4f2f7e86b2 --- /dev/null +++ b/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs @@ -0,0 +1,27 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Vault.Authorization.Permissions; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Api.Vault.Models.Response; + +public record CipherPermissionsResponseModel +{ + public bool Delete { get; init; } + public bool Restore { get; init; } + + public CipherPermissionsResponseModel( + User user, + CipherDetails cipherDetails, + IDictionary organizationAbilities) + { + OrganizationAbility organizationAbility = null; + if (cipherDetails.OrganizationId.HasValue && !organizationAbilities.TryGetValue(cipherDetails.OrganizationId.Value, out organizationAbility)) + { + throw new Exception("OrganizationAbility not found for organization cipher."); + } + + Delete = NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility); + Restore = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility); + } +} diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 207017227a..358da3e62a 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Models.Api; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; @@ -96,26 +97,37 @@ public class CipherMiniResponseModel : ResponseModel public class CipherResponseModel : CipherMiniResponseModel { - public CipherResponseModel(CipherDetails cipher, IGlobalSettings globalSettings, string obj = "cipher") + public CipherResponseModel( + CipherDetails cipher, + User user, + IDictionary organizationAbilities, + IGlobalSettings globalSettings, + string obj = "cipher") : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj) { FolderId = cipher.FolderId; Favorite = cipher.Favorite; Edit = cipher.Edit; ViewPassword = cipher.ViewPassword; + Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbilities); } public Guid? FolderId { get; set; } public bool Favorite { get; set; } public bool Edit { get; set; } public bool ViewPassword { get; set; } + public CipherPermissionsResponseModel Permissions { get; set; } } public class CipherDetailsResponseModel : CipherResponseModel { - public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings, + public CipherDetailsResponseModel( + CipherDetails cipher, + User user, + IDictionary organizationAbilities, + GlobalSettings globalSettings, IDictionary> collectionCiphers, string obj = "cipherDetails") - : base(cipher, globalSettings, obj) + : base(cipher, user, organizationAbilities, globalSettings, obj) { if (collectionCiphers?.ContainsKey(cipher.Id) ?? false) { @@ -127,15 +139,24 @@ public class CipherDetailsResponseModel : CipherResponseModel } } - public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings, + public CipherDetailsResponseModel( + CipherDetails cipher, + User user, + IDictionary organizationAbilities, + GlobalSettings globalSettings, IEnumerable collectionCiphers, string obj = "cipherDetails") - : base(cipher, globalSettings, obj) + : base(cipher, user, organizationAbilities, globalSettings, obj) { CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List(); } - public CipherDetailsResponseModel(CipherDetailsWithCollections cipher, GlobalSettings globalSettings, string obj = "cipherDetails") - : base(cipher, globalSettings, obj) + public CipherDetailsResponseModel( + CipherDetailsWithCollections cipher, + User user, + IDictionary organizationAbilities, + GlobalSettings globalSettings, + string obj = "cipherDetails") + : base(cipher, user, organizationAbilities, globalSettings, obj) { CollectionIds = cipher.CollectionIds ?? new List(); } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index a9b87ac31e..f1465264f2 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; using Bit.Core.Tools.Entities; @@ -21,6 +22,7 @@ public class SyncResponseModel : ResponseModel User user, bool userTwoFactorEnabled, bool userHasPremiumFromOrganization, + IDictionary organizationAbilities, IEnumerable organizationIdsManagingUser, IEnumerable organizationUserDetails, IEnumerable providerUserDetails, @@ -37,7 +39,13 @@ public class SyncResponseModel : ResponseModel Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser); Folders = folders.Select(f => new FolderResponseModel(f)); - Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); + Ciphers = ciphers.Select(cipher => + new CipherDetailsResponseModel( + cipher, + user, + organizationAbilities, + globalSettings, + collectionCiphersDict)); Collections = collections?.Select( c => new CollectionDetailsResponseModel(c)) ?? new List(); Domains = excludeDomains ? null : new DomainsResponseModel(user, false); diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 2afce14ac5..5c8de51062 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -27,17 +27,18 @@ namespace Bit.Api.Test.Controllers; public class CiphersControllerTests { [Theory, BitAutoData] - public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(Guid userId, Guid folderId, SutProvider sutProvider) + public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(User user, Guid folderId, SutProvider sutProvider) { var isFavorite = true; var cipherId = Guid.NewGuid(); sutProvider.GetDependency() - .GetProperUserId(Arg.Any()) - .Returns(userId); + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); var cipherDetails = new CipherDetails { + UserId = user.Id, Favorite = isFavorite, FolderId = folderId, Type = Core.Vault.Enums.CipherType.SecureNote, @@ -45,7 +46,7 @@ public class CiphersControllerTests }; sutProvider.GetDependency() - .GetByIdAsync(cipherId, userId) + .GetByIdAsync(cipherId, user.Id) .Returns(Task.FromResult(cipherDetails)); var result = await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() }); @@ -55,12 +56,12 @@ public class CiphersControllerTests } [Theory, BitAutoData] - public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, Guid userId, + public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, User user, SutProvider sutProvider) { - sutProvider.GetDependency().GetProperUserId(default).Returns(userId); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); sutProvider.GetDependency().OrganizationUser(Guid.NewGuid()).Returns(false); - sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsNull(); + sutProvider.GetDependency().GetByIdAsync(id, user.Id).ReturnsNull(); var requestAction = async () => await sutProvider.Sut.PutCollections_vNext(id, model); @@ -75,6 +76,7 @@ public class CiphersControllerTests sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilitiesAsync().Returns(new Dictionary { { cipherDetails.OrganizationId.Value, new OrganizationAbility() } }); var cipherService = sutProvider.GetDependency(); await sutProvider.Sut.PutCollections_vNext(id, model); @@ -90,6 +92,7 @@ public class CiphersControllerTests sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilitiesAsync().Returns(new Dictionary { { cipherDetails.OrganizationId.Value, new OrganizationAbility() } }); var result = await sutProvider.Sut.PutCollections_vNext(id, model); @@ -115,6 +118,7 @@ public class CiphersControllerTests private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns(new List()); } From 031e188e82f6f2e4278979479eb094b27b4fb431 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:53:07 +0100 Subject: [PATCH 65/84] Remove extension-refresh feature flag (#5410) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b1cbc8d519..6baa9227e1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -121,7 +121,6 @@ public static class FeatureFlagKeys public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; - public const string ExtensionRefresh = "extension-refresh"; public const string RestrictProviderAccess = "restrict-provider-access"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string VaultBulkManagementAction = "vault-bulk-management-action"; From 913da4a629c156ef2634a99dd3324bed060abc46 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:16:43 -0400 Subject: [PATCH 66/84] [PM-15015] Add Country Name to auth request from request headers (#5471) * feat(pm-15015) : * Add `CountryName` column to AuthRequest Table in Database, and refreshing AuthRequestView * Modify database stored procedures and Entity Framework migrations for AuthRequest Repositories * Add property to `ICurrentContext` and response models. --- ...ingOrganizationAuthRequestResponseModel.cs | 2 + .../Response/AuthRequestResponseModel.cs | 2 + src/Core/Auth/Entities/AuthRequest.cs | 6 + .../Implementations/AuthRequestService.cs | 8 +- src/Core/Context/CurrentContext.cs | 7 + src/Core/Context/ICurrentContext.cs | 1 + .../Stored Procedures/AuthRequest_Create.sql | 43 +- .../Stored Procedures/AuthRequest_Update.sql | 44 +- .../AuthRequest_UpdateMany.sql | 4 +- src/Sql/Auth/dbo/Tables/AuthRequest.sql | 2 +- .../2025-02-27_00_AlterAuthRequest.sql | 168 + ...0250304221039_AlterAuthRequest.Designer.cs | 3014 ++++++++++++++++ .../20250304221039_AlterAuthRequest.cs | 29 + .../DatabaseContextModelSnapshot.cs | 4 + ...04204625_AlterAuthRequestTable.Designer.cs | 3020 +++++++++++++++++ .../20250304204625_AlterAuthRequestTable.cs | 28 + .../DatabaseContextModelSnapshot.cs | 4 + ...04204635_AlterAuthRequestTable.Designer.cs | 3003 ++++++++++++++++ .../20250304204635_AlterAuthRequestTable.cs | 28 + .../DatabaseContextModelSnapshot.cs | 4 + 20 files changed, 9372 insertions(+), 49 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-02-27_00_AlterAuthRequest.sql create mode 100644 util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.cs create mode 100644 util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.cs create mode 100644 util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.cs diff --git a/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs b/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs index 1805bdb07e..3e242cba7b 100644 --- a/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs @@ -23,6 +23,7 @@ public class PendingOrganizationAuthRequestResponseModel : ResponseModel RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString()) .FirstOrDefault()?.GetCustomAttribute()?.GetName(); RequestIpAddress = authRequest.RequestIpAddress; + RequestCountryName = authRequest.RequestCountryName; CreationDate = authRequest.CreationDate; } @@ -34,5 +35,6 @@ public class PendingOrganizationAuthRequestResponseModel : ResponseModel public string RequestDeviceIdentifier { get; set; } public string RequestDeviceType { get; set; } public string RequestIpAddress { get; set; } + public string RequestCountryName { get; set; } public DateTime CreationDate { get; set; } } diff --git a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs index 50f7f5a3e7..7a9734d844 100644 --- a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs +++ b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs @@ -23,6 +23,7 @@ public class AuthRequestResponseModel : ResponseModel RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString()) .FirstOrDefault()?.GetCustomAttribute()?.GetName(); RequestIpAddress = authRequest.RequestIpAddress; + RequestCountryName = authRequest.RequestCountryName; Key = authRequest.Key; MasterPasswordHash = authRequest.MasterPasswordHash; CreationDate = authRequest.CreationDate; @@ -37,6 +38,7 @@ public class AuthRequestResponseModel : ResponseModel public DeviceType RequestDeviceTypeValue { get; set; } public string RequestDeviceType { get; set; } public string RequestIpAddress { get; set; } + public string RequestCountryName { get; set; } public string Key { get; set; } public string MasterPasswordHash { get; set; } public DateTime CreationDate { get; set; } diff --git a/src/Core/Auth/Entities/AuthRequest.cs b/src/Core/Auth/Entities/AuthRequest.cs index d1d337b8a1..088c24b88a 100644 --- a/src/Core/Auth/Entities/AuthRequest.cs +++ b/src/Core/Auth/Entities/AuthRequest.cs @@ -16,6 +16,12 @@ public class AuthRequest : ITableObject public DeviceType RequestDeviceType { get; set; } [MaxLength(50)] public string RequestIpAddress { get; set; } + /// + /// This country name is populated through a header value fetched from the ISO-3166 country code. + /// It will always be the English short form of the country name. The length should never be over 200 characters. + /// + [MaxLength(200)] + public string RequestCountryName { get; set; } public Guid? ResponseDeviceId { get; set; } [MaxLength(25)] public string AccessCode { get; set; } diff --git a/src/Core/Auth/Services/Implementations/AuthRequestService.cs b/src/Core/Auth/Services/Implementations/AuthRequestService.cs index b70a690338..c10fa6ce92 100644 --- a/src/Core/Auth/Services/Implementations/AuthRequestService.cs +++ b/src/Core/Auth/Services/Implementations/AuthRequestService.cs @@ -164,6 +164,7 @@ public class AuthRequestService : IAuthRequestService RequestDeviceIdentifier = model.DeviceIdentifier, RequestDeviceType = _currentContext.DeviceType.Value, RequestIpAddress = _currentContext.IpAddress, + RequestCountryName = _currentContext.CountryName, AccessCode = model.AccessCode, PublicKey = model.PublicKey, UserId = user.Id, @@ -176,12 +177,7 @@ public class AuthRequestService : IAuthRequestService public async Task UpdateAuthRequestAsync(Guid authRequestId, Guid currentUserId, AuthRequestUpdateRequestModel model) { - var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId); - - if (authRequest == null) - { - throw new NotFoundException(); - } + var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId) ?? throw new NotFoundException(); // Once Approval/Disapproval has been set, this AuthRequest should not be updated again. if (authRequest.Approved is not null) diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index b4a250fe2b..cbd90055b0 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -30,6 +30,7 @@ public class CurrentContext : ICurrentContext public virtual string DeviceIdentifier { get; set; } public virtual DeviceType? DeviceType { get; set; } public virtual string IpAddress { get; set; } + public virtual string CountryName { get; set; } public virtual List Organizations { get; set; } public virtual List Providers { get; set; } public virtual Guid? InstallationId { get; set; } @@ -104,6 +105,12 @@ public class CurrentContext : ICurrentContext { ClientVersionIsPrerelease = clientVersionIsPrerelease == "1"; } + + if (httpContext.Request.Headers.TryGetValue("country-name", out var countryName)) + { + CountryName = countryName; + } + } public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings) diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 9361480229..42843ce6d7 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -20,6 +20,7 @@ public interface ICurrentContext string DeviceIdentifier { get; set; } DeviceType? DeviceType { get; set; } string IpAddress { get; set; } + string CountryName { get; set; } List Organizations { get; set; } Guid? InstallationId { get; set; } Guid? OrganizationId { get; set; } diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Create.sql b/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Create.sql index 81490182f3..41d1698220 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Create.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Create.sql @@ -6,6 +6,7 @@ @RequestDeviceIdentifier NVARCHAR(50), @RequestDeviceType TINYINT, @RequestIpAddress VARCHAR(50), + @RequestCountryName NVARCHAR(200), @ResponseDeviceId UNIQUEIDENTIFIER, @AccessCode VARCHAR(25), @PublicKey VARCHAR(MAX), @@ -20,7 +21,7 @@ BEGIN SET NOCOUNT ON INSERT INTO [dbo].[AuthRequest] - ( + ( [Id], [UserId], [OrganizationId], @@ -28,6 +29,7 @@ BEGIN [RequestDeviceIdentifier], [RequestDeviceType], [RequestIpAddress], + [RequestCountryName], [ResponseDeviceId], [AccessCode], [PublicKey], @@ -37,24 +39,25 @@ BEGIN [CreationDate], [ResponseDate], [AuthenticationDate] - ) + ) VALUES - ( - @Id, - @UserId, - @OrganizationId, - @Type, - @RequestDeviceIdentifier, - @RequestDeviceType, - @RequestIpAddress, - @ResponseDeviceId, - @AccessCode, - @PublicKey, - @Key, - @MasterPasswordHash, - @Approved, - @CreationDate, - @ResponseDate, - @AuthenticationDate + ( + @Id, + @UserId, + @OrganizationId, + @Type, + @RequestDeviceIdentifier, + @RequestDeviceType, + @RequestIpAddress, + @RequestCountryName, + @ResponseDeviceId, + @AccessCode, + @PublicKey, + @Key, + @MasterPasswordHash, + @Approved, + @CreationDate, + @ResponseDate, + @AuthenticationDate ) -END \ No newline at end of file +END diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Update.sql b/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Update.sql index 0af4109da4..dde8de1b44 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Update.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Update.sql @@ -2,10 +2,11 @@ @Id UNIQUEIDENTIFIER OUTPUT, @UserId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER = NULL, - @Type SMALLINT, + @Type SMALLINT, @RequestDeviceIdentifier NVARCHAR(50), @RequestDeviceType SMALLINT, @RequestIpAddress VARCHAR(50), + @RequestCountryName NVARCHAR(200), @ResponseDeviceId UNIQUEIDENTIFIER, @AccessCode VARCHAR(25), @PublicKey VARCHAR(MAX), @@ -14,29 +15,30 @@ @Approved BIT, @CreationDate DATETIME2 (7), @ResponseDate DATETIME2 (7), - @AuthenticationDate DATETIME2 (7) + @AuthenticationDate DATETIME2 (7) AS BEGIN SET NOCOUNT ON UPDATE - [dbo].[AuthRequest] - SET - [UserId] = @UserId, - [Type] = @Type, - [OrganizationId] = @OrganizationId, - [RequestDeviceIdentifier] = @RequestDeviceIdentifier, - [RequestDeviceType] = @RequestDeviceType, - [RequestIpAddress] = @RequestIpAddress, - [ResponseDeviceId] = @ResponseDeviceId, - [AccessCode] = @AccessCode, - [PublicKey] = @PublicKey, - [Key] = @Key, - [MasterPasswordHash] = @MasterPasswordHash, - [Approved] = @Approved, - [CreationDate] = @CreationDate, - [ResponseDate] = @ResponseDate, - [AuthenticationDate] = @AuthenticationDate - WHERE - [Id] = @Id + [dbo].[AuthRequest] +SET + [UserId] = @UserId, + [Type] = @Type, + [OrganizationId] = @OrganizationId, + [RequestDeviceIdentifier] = @RequestDeviceIdentifier, + [RequestDeviceType] = @RequestDeviceType, + [RequestIpAddress] = @RequestIpAddress, + [RequestCountryName] = @RequestCountryName, + [ResponseDeviceId] = @ResponseDeviceId, + [AccessCode] = @AccessCode, + [PublicKey] = @PublicKey, + [Key] = @Key, + [MasterPasswordHash] = @MasterPasswordHash, + [Approved] = @Approved, + [CreationDate] = @CreationDate, + [ResponseDate] = @ResponseDate, + [AuthenticationDate] = @AuthenticationDate +WHERE + [Id] = @Id END diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_UpdateMany.sql b/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_UpdateMany.sql index 227abbb3e1..c42ceba9f6 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_UpdateMany.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_UpdateMany.sql @@ -10,6 +10,7 @@ BEGIN [RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier], [RequestDeviceType] = ARI.[RequestDeviceType], [RequestIpAddress] = ARI.[RequestIpAddress], + [RequestCountryName] = ARI.[RequestCountryName], [ResponseDeviceId] = ARI.[ResponseDeviceId], [AccessCode] = ARI.[AccessCode], [PublicKey] = ARI.[PublicKey], @@ -22,7 +23,7 @@ BEGIN [OrganizationId] = ARI.[OrganizationId] FROM [dbo].[AuthRequest] AR - INNER JOIN + INNER JOIN OPENJSON(@jsonData) WITH ( Id UNIQUEIDENTIFIER '$.Id', @@ -31,6 +32,7 @@ BEGIN RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier', RequestDeviceType SMALLINT '$.RequestDeviceType', RequestIpAddress VARCHAR(50) '$.RequestIpAddress', + RequestCountryName NVARCHAR(200) '$.RequestCountryName', ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId', AccessCode VARCHAR(25) '$.AccessCode', PublicKey VARCHAR(MAX) '$.PublicKey', diff --git a/src/Sql/Auth/dbo/Tables/AuthRequest.sql b/src/Sql/Auth/dbo/Tables/AuthRequest.sql index 4f2b3193fb..234f89c5ec 100644 --- a/src/Sql/Auth/dbo/Tables/AuthRequest.sql +++ b/src/Sql/Auth/dbo/Tables/AuthRequest.sql @@ -15,11 +15,11 @@ [ResponseDate] DATETIME2 (7) NULL, [AuthenticationDate] DATETIME2 (7) NULL, [OrganizationId] UNIQUEIDENTIFIER NULL, + [RequestCountryName] NVARCHAR(200) NULL, CONSTRAINT [PK_AuthRequest] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_AuthRequest_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]), CONSTRAINT [FK_AuthRequest_ResponseDevice] FOREIGN KEY ([ResponseDeviceId]) REFERENCES [dbo].[Device] ([Id]), CONSTRAINT [FK_AuthRequest_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ); - GO diff --git a/util/Migrator/DbScripts/2025-02-27_00_AlterAuthRequest.sql b/util/Migrator/DbScripts/2025-02-27_00_AlterAuthRequest.sql new file mode 100644 index 0000000000..3d0732ed88 --- /dev/null +++ b/util/Migrator/DbScripts/2025-02-27_00_AlterAuthRequest.sql @@ -0,0 +1,168 @@ +ALTER TABLE + [dbo].[AuthRequest] +ADD + [RequestCountryName] NVARCHAR(200) NULL; +GO + +EXECUTE sp_refreshview 'dbo.AuthRequestView' +GO + +CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER = NULL, + @Type TINYINT, + @RequestDeviceIdentifier NVARCHAR(50), + @RequestDeviceType TINYINT, + @RequestIpAddress VARCHAR(50), + @RequestCountryName NVARCHAR(200), + @ResponseDeviceId UNIQUEIDENTIFIER, + @AccessCode VARCHAR(25), + @PublicKey VARCHAR(MAX), + @Key VARCHAR(MAX), + @MasterPasswordHash VARCHAR(MAX), + @Approved BIT, + @CreationDate DATETIME2(7), + @ResponseDate DATETIME2(7), + @AuthenticationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[AuthRequest] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [RequestDeviceIdentifier], + [RequestDeviceType], + [RequestIpAddress], + [RequestCountryName], + [ResponseDeviceId], + [AccessCode], + [PublicKey], + [Key], + [MasterPasswordHash], + [Approved], + [CreationDate], + [ResponseDate], + [AuthenticationDate] + ) + VALUES + ( + @Id, + @UserId, + @OrganizationId, + @Type, + @RequestDeviceIdentifier, + @RequestDeviceType, + @RequestIpAddress, + @RequestCountryName, + @ResponseDeviceId, + @AccessCode, + @PublicKey, + @Key, + @MasterPasswordHash, + @Approved, + @CreationDate, + @ResponseDate, + @AuthenticationDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER = NULL, + @Type SMALLINT, + @RequestDeviceIdentifier NVARCHAR(50), + @RequestDeviceType SMALLINT, + @RequestIpAddress VARCHAR(50), + @RequestCountryName NVARCHAR(200), + @ResponseDeviceId UNIQUEIDENTIFIER, + @AccessCode VARCHAR(25), + @PublicKey VARCHAR(MAX), + @Key VARCHAR(MAX), + @MasterPasswordHash VARCHAR(MAX), + @Approved BIT, + @CreationDate DATETIME2 (7), + @ResponseDate DATETIME2 (7), + @AuthenticationDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[AuthRequest] +SET + [UserId] = @UserId, + [Type] = @Type, + [OrganizationId] = @OrganizationId, + [RequestDeviceIdentifier] = @RequestDeviceIdentifier, + [RequestDeviceType] = @RequestDeviceType, + [RequestIpAddress] = @RequestIpAddress, + [RequestCountryName] = @RequestCountryName, + [ResponseDeviceId] = @ResponseDeviceId, + [AccessCode] = @AccessCode, + [PublicKey] = @PublicKey, + [Key] = @Key, + [MasterPasswordHash] = @MasterPasswordHash, + [Approved] = @Approved, + [CreationDate] = @CreationDate, + [ResponseDate] = @ResponseDate, + [AuthenticationDate] = @AuthenticationDate +WHERE + [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE AuthRequest_UpdateMany + @jsonData NVARCHAR(MAX) +AS +BEGIN + UPDATE AR + SET + [Id] = ARI.[Id], + [UserId] = ARI.[UserId], + [Type] = ARI.[Type], + [RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier], + [RequestDeviceType] = ARI.[RequestDeviceType], + [RequestIpAddress] = ARI.[RequestIpAddress], + [RequestCountryName] = ARI.[RequestCountryName], + [ResponseDeviceId] = ARI.[ResponseDeviceId], + [AccessCode] = ARI.[AccessCode], + [PublicKey] = ARI.[PublicKey], + [Key] = ARI.[Key], + [MasterPasswordHash] = ARI.[MasterPasswordHash], + [Approved] = ARI.[Approved], + [CreationDate] = ARI.[CreationDate], + [ResponseDate] = ARI.[ResponseDate], + [AuthenticationDate] = ARI.[AuthenticationDate], + [OrganizationId] = ARI.[OrganizationId] + FROM + [dbo].[AuthRequest] AR + INNER JOIN + OPENJSON(@jsonData) + WITH ( + Id UNIQUEIDENTIFIER '$.Id', + UserId UNIQUEIDENTIFIER '$.UserId', + Type SMALLINT '$.Type', + RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier', + RequestDeviceType SMALLINT '$.RequestDeviceType', + RequestIpAddress VARCHAR(50) '$.RequestIpAddress', + RequestCountryName NVARCHAR(200) '$.RequestCountryName', + ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId', + AccessCode VARCHAR(25) '$.AccessCode', + PublicKey VARCHAR(MAX) '$.PublicKey', + [Key] VARCHAR(MAX) '$.Key', + MasterPasswordHash VARCHAR(MAX) '$.MasterPasswordHash', + Approved BIT '$.Approved', + CreationDate DATETIME2 '$.CreationDate', + ResponseDate DATETIME2 '$.ResponseDate', + AuthenticationDate DATETIME2 '$.AuthenticationDate', + OrganizationId UNIQUEIDENTIFIER '$.OrganizationId' + ) ARI ON AR.Id = ARI.Id; +END +GO \ No newline at end of file diff --git a/util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.Designer.cs b/util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.Designer.cs new file mode 100644 index 0000000000..771b7a372f --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.Designer.cs @@ -0,0 +1,3014 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250304221039_AlterAuthRequest")] + partial class AlterAuthRequest + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.cs b/util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.cs new file mode 100644 index 0000000000..ec6e89baad --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250304221039_AlterAuthRequest.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AlterAuthRequest : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RequestCountryName", + table: "AuthRequest", + type: "varchar(200)", + maxLength: 200, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RequestCountryName", + table: "AuthRequest"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index bd04151035..dfd5d4a983 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -407,6 +407,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("AuthenticationDate") .HasColumnType("datetime(6)"); + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + b.Property("CreationDate") .HasColumnType("datetime(6)"); diff --git a/util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.Designer.cs b/util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.Designer.cs new file mode 100644 index 0000000000..a761482cfd --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.Designer.cs @@ -0,0 +1,3020 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250304204625_AlterAuthRequestTable")] + partial class AlterAuthRequestTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.cs b/util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.cs new file mode 100644 index 0000000000..be5cfae89b --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250304204625_AlterAuthRequestTable.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AlterAuthRequestTable : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RequestCountryName", + table: "AuthRequest", + type: "character varying(200)", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RequestCountryName", + table: "AuthRequest"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index b507984ad1..a54bc6bddf 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -410,6 +410,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("AuthenticationDate") .HasColumnType("timestamp with time zone"); + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); diff --git a/util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.Designer.cs b/util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.Designer.cs new file mode 100644 index 0000000000..5708973630 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.Designer.cs @@ -0,0 +1,3003 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250304204635_AlterAuthRequestTable")] + partial class AlterAuthRequestTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.cs b/util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.cs new file mode 100644 index 0000000000..3f851d0176 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250304204635_AlterAuthRequestTable.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AlterAuthRequestTable : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RequestCountryName", + table: "AuthRequest", + type: "TEXT", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RequestCountryName", + table: "AuthRequest"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 158a02cd43..824f2ffec5 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -402,6 +402,10 @@ namespace Bit.SqliteMigrations.Migrations b.Property("AuthenticationDate") .HasColumnType("TEXT"); + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + b.Property("CreationDate") .HasColumnType("TEXT"); From ac25ec45194dd62bab6956e005647c8fb3492562 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Mon, 10 Mar 2025 18:14:22 +0100 Subject: [PATCH 67/84] [PM-19002] Extract billing code from AccountsController (#5477) --- .../Auth/Controllers/AccountsController.cs | 231 ----------------- .../Billing/Controllers/AccountsController.cs | 237 ++++++++++++++++++ .../Controllers/AccountsControllerTests.cs | 19 -- 3 files changed, 237 insertions(+), 250 deletions(-) create mode 100644 src/Api/Billing/Controllers/AccountsController.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 176bc183b6..6c19049c49 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -4,11 +4,9 @@ using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.KeyManagement.Validators; -using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Tools.Models.Request; -using Bit.Api.Utilities; using Bit.Api.Vault.Models.Request; using Bit.Core; using Bit.Core.AdminConsole.Enums.Provider; @@ -19,23 +17,15 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; -using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Api.Response; -using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Authorization; @@ -47,20 +37,15 @@ namespace Bit.Api.Auth.Controllers; [Authorize("Application")] public class AccountsController : Controller { - private readonly GlobalSettings _globalSettings; private readonly IOrganizationService _organizationService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IProviderUserRepository _providerUserRepository; - private readonly IPaymentService _paymentService; private readonly IUserService _userService; private readonly IPolicyService _policyService; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly IFeatureService _featureService; - private readonly ISubscriberService _subscriberService; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly IRotationValidator, IEnumerable> _cipherValidator; private readonly IRotationValidator, IEnumerable> _folderValidator; @@ -75,20 +60,15 @@ public class AccountsController : Controller public AccountsController( - GlobalSettings globalSettings, IOrganizationService organizationService, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, - IPaymentService paymentService, IUserService userService, IPolicyService policyService, ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, IRotateUserKeyCommand rotateUserKeyCommand, IFeatureService featureService, - ISubscriberService subscriberService, - IReferenceEventService referenceEventService, - ICurrentContext currentContext, IRotationValidator, IEnumerable> cipherValidator, IRotationValidator, IEnumerable> folderValidator, IRotationValidator, IReadOnlyList> sendValidator, @@ -99,20 +79,15 @@ public class AccountsController : Controller IRotationValidator, IEnumerable> webAuthnKeyValidator ) { - _globalSettings = globalSettings; _organizationService = organizationService; _organizationUserRepository = organizationUserRepository; _providerUserRepository = providerUserRepository; - _paymentService = paymentService; _userService = userService; _policyService = policyService; _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _rotateUserKeyCommand = rotateUserKeyCommand; _featureService = featureService; - _subscriberService = subscriberService; - _referenceEventService = referenceEventService; - _currentContext = currentContext; _cipherValidator = cipherValidator; _folderValidator = folderValidator; _sendValidator = sendValidator; @@ -638,212 +613,6 @@ public class AccountsController : Controller throw new BadRequestException(ModelState); } - [HttpPost("premium")] - public async Task PostPremium(PremiumRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var valid = model.Validate(_globalSettings); - UserLicense license = null; - if (valid && _globalSettings.SelfHosted) - { - license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); - } - - if (!valid && !_globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country)) - { - throw new BadRequestException("Country is required."); - } - - if (!valid || (_globalSettings.SelfHosted && license == null)) - { - throw new BadRequestException("Invalid license."); - } - - var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken, - model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, - new TaxInfo - { - BillingAddressCountry = model.Country, - BillingAddressPostalCode = model.PostalCode - }); - - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); - var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); - - var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser); - return new PaymentResponseModel - { - UserProfile = profile, - PaymentIntentClientSecret = result.Item2, - Success = result.Item1 - }; - } - - [HttpGet("subscription")] - public async Task GetSubscription() - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - if (!_globalSettings.SelfHosted && user.Gateway != null) - { - var subscriptionInfo = await _paymentService.GetSubscriptionAsync(user); - var license = await _userService.GenerateLicenseAsync(user, subscriptionInfo); - return new SubscriptionResponseModel(user, subscriptionInfo, license); - } - else if (!_globalSettings.SelfHosted) - { - var license = await _userService.GenerateLicenseAsync(user); - return new SubscriptionResponseModel(user, license); - } - else - { - return new SubscriptionResponseModel(user); - } - } - - [HttpPost("payment")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostPayment([FromBody] PaymentRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - await _userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType.Value, - new TaxInfo - { - BillingAddressLine1 = model.Line1, - BillingAddressLine2 = model.Line2, - BillingAddressCity = model.City, - BillingAddressState = model.State, - BillingAddressCountry = model.Country, - BillingAddressPostalCode = model.PostalCode, - TaxIdNumber = model.TaxId - }); - } - - [HttpPost("storage")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostStorage([FromBody] StorageRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var result = await _userService.AdjustStorageAsync(user, model.StorageGbAdjustment.Value); - return new PaymentResponseModel - { - Success = true, - PaymentIntentClientSecret = result - }; - } - - [HttpPost("license")] - [SelfHosted(SelfHostedOnly = true)] - public async Task PostLicense(LicenseRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); - if (license == null) - { - throw new BadRequestException("Invalid license"); - } - - await _userService.UpdateLicenseAsync(user, license); - } - - [HttpPost("cancel")] - public async Task PostCancel([FromBody] SubscriptionCancellationRequestModel request) - { - var user = await _userService.GetUserByPrincipalAsync(User); - - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - await _subscriberService.CancelSubscription(user, - new OffboardingSurveyResponse - { - UserId = user.Id, - Reason = request.Reason, - Feedback = request.Feedback - }, - user.IsExpired()); - - await _referenceEventService.RaiseEventAsync(new ReferenceEvent( - ReferenceEventType.CancelSubscription, - user, - _currentContext) - { - EndOfPeriod = user.IsExpired() - }); - } - - [HttpPost("reinstate-premium")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostReinstate() - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - await _userService.ReinstatePremiumAsync(user); - } - - [HttpGet("tax")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task GetTaxInfo() - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var taxInfo = await _paymentService.GetTaxInfoAsync(user); - return new TaxInfoResponseModel(taxInfo); - } - - [HttpPut("tax")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PutTaxInfo([FromBody] TaxInfoUpdateRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var taxInfo = new TaxInfo - { - BillingAddressPostalCode = model.PostalCode, - BillingAddressCountry = model.Country, - }; - await _paymentService.SaveTaxInfoAsync(user, taxInfo); - } - [HttpDelete("sso/{organizationId}")] public async Task DeleteSsoUser(string organizationId) { diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs new file mode 100644 index 0000000000..9c5811b195 --- /dev/null +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -0,0 +1,237 @@ +#nullable enable +using Bit.Api.Models.Request; +using Bit.Api.Models.Request.Accounts; +using Bit.Api.Models.Response; +using Bit.Api.Utilities; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Billing.Controllers; + +[Route("accounts")] +[Authorize("Application")] +public class AccountsController( + IUserService userService) : Controller +{ + [HttpPost("premium")] + public async Task PostPremiumAsync( + PremiumRequestModel model, + [FromServices] GlobalSettings globalSettings) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var valid = model.Validate(globalSettings); + UserLicense? license = null; + if (valid && globalSettings.SelfHosted) + { + license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); + } + + if (!valid && !globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country)) + { + throw new BadRequestException("Country is required."); + } + + if (!valid || (globalSettings.SelfHosted && license == null)) + { + throw new BadRequestException("Invalid license."); + } + + var result = await userService.SignUpPremiumAsync(user, model.PaymentToken, + model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, + new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); + + var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user); + var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); + var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id); + + var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, + userHasPremiumFromOrganization, organizationIdsManagingActiveUser); + return new PaymentResponseModel + { + UserProfile = profile, + PaymentIntentClientSecret = result.Item2, + Success = result.Item1 + }; + } + + [HttpGet("subscription")] + public async Task GetSubscriptionAsync( + [FromServices] GlobalSettings globalSettings, + [FromServices] IPaymentService paymentService) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + if (!globalSettings.SelfHosted && user.Gateway != null) + { + var subscriptionInfo = await paymentService.GetSubscriptionAsync(user); + var license = await userService.GenerateLicenseAsync(user, subscriptionInfo); + return new SubscriptionResponseModel(user, subscriptionInfo, license); + } + else if (!globalSettings.SelfHosted) + { + var license = await userService.GenerateLicenseAsync(user); + return new SubscriptionResponseModel(user, license); + } + else + { + return new SubscriptionResponseModel(user); + } + } + + [HttpPost("payment")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task PostPaymentAsync([FromBody] PaymentRequestModel model) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + await userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType!.Value, + new TaxInfo + { + BillingAddressLine1 = model.Line1, + BillingAddressLine2 = model.Line2, + BillingAddressCity = model.City, + BillingAddressState = model.State, + BillingAddressCountry = model.Country, + BillingAddressPostalCode = model.PostalCode, + TaxIdNumber = model.TaxId + }); + } + + [HttpPost("storage")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task PostStorageAsync([FromBody] StorageRequestModel model) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var result = await userService.AdjustStorageAsync(user, model.StorageGbAdjustment!.Value); + return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result }; + } + + + + [HttpPost("license")] + [SelfHosted(SelfHostedOnly = true)] + public async Task PostLicenseAsync(LicenseRequestModel model) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); + if (license == null) + { + throw new BadRequestException("Invalid license"); + } + + await userService.UpdateLicenseAsync(user, license); + } + + [HttpPost("cancel")] + public async Task PostCancelAsync( + [FromBody] SubscriptionCancellationRequestModel request, + [FromServices] ICurrentContext currentContext, + [FromServices] IReferenceEventService referenceEventService, + [FromServices] ISubscriberService subscriberService) + { + var user = await userService.GetUserByPrincipalAsync(User); + + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + await subscriberService.CancelSubscription(user, + new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback }, + user.IsExpired()); + + await referenceEventService.RaiseEventAsync(new ReferenceEvent( + ReferenceEventType.CancelSubscription, + user, + currentContext) + { EndOfPeriod = user.IsExpired() }); + } + + [HttpPost("reinstate-premium")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task PostReinstateAsync() + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + await userService.ReinstatePremiumAsync(user); + } + + [HttpGet("tax")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task GetTaxInfoAsync( + [FromServices] IPaymentService paymentService) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var taxInfo = await paymentService.GetTaxInfoAsync(user); + return new TaxInfoResponseModel(taxInfo); + } + + [HttpPut("tax")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task PutTaxInfoAsync( + [FromBody] TaxInfoUpdateRequestModel model, + [FromServices] IPaymentService paymentService) + { + var user = await userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var taxInfo = new TaxInfo + { + BillingAddressPostalCode = model.PostalCode, + BillingAddressCountry = model.Country, + }; + await paymentService.SaveTaxInfoAsync(user, taxInfo); + } + + private async Task> GetOrganizationIdsManagingUserAsync(Guid userId) + { + var organizationManagingUser = await userService.GetOrganizationsManagingUserAsync(userId); + return organizationManagingUser.Select(o => o.Id); + } +} diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 6a9862b3d6..15c7573aca 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -15,16 +15,12 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; -using Bit.Core.Billing.Services; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Services; using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; @@ -37,10 +33,8 @@ public class AccountsControllerTests : IDisposable { private readonly AccountsController _sut; - private readonly GlobalSettings _globalSettings; private readonly IOrganizationService _organizationService; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IPaymentService _paymentService; private readonly IUserService _userService; private readonly IProviderUserRepository _providerUserRepository; private readonly IPolicyService _policyService; @@ -48,9 +42,6 @@ public class AccountsControllerTests : IDisposable private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; - private readonly ISubscriberService _subscriberService; - private readonly IReferenceEventService _referenceEventService; - private readonly ICurrentContext _currentContext; private readonly IRotationValidator, IEnumerable> _cipherValidator; private readonly IRotationValidator, IEnumerable> _folderValidator; @@ -70,16 +61,11 @@ public class AccountsControllerTests : IDisposable _organizationService = Substitute.For(); _organizationUserRepository = Substitute.For(); _providerUserRepository = Substitute.For(); - _paymentService = Substitute.For(); - _globalSettings = new GlobalSettings(); _policyService = Substitute.For(); _setInitialMasterPasswordCommand = Substitute.For(); _rotateUserKeyCommand = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); - _subscriberService = Substitute.For(); - _referenceEventService = Substitute.For(); - _currentContext = Substitute.For(); _cipherValidator = Substitute.For, IEnumerable>>(); _folderValidator = @@ -93,20 +79,15 @@ public class AccountsControllerTests : IDisposable IReadOnlyList>>(); _sut = new AccountsController( - _globalSettings, _organizationService, _organizationUserRepository, _providerUserRepository, - _paymentService, _userService, _policyService, _setInitialMasterPasswordCommand, _tdeOffboardingPasswordCommand, _rotateUserKeyCommand, _featureService, - _subscriberService, - _referenceEventService, - _currentContext, _cipherValidator, _folderValidator, _sendValidator, From 8287d0a968946f3418184db9f300dbe8153e7d89 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Mon, 10 Mar 2025 15:57:56 -0400 Subject: [PATCH 68/84] Replace secret checking logic with branch detection logic (#5454) --- .github/workflows/test-database.yml | 20 +------------------- .github/workflows/test.yml | 20 +------------------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 81119414ff..0e10d15ad1 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -32,28 +32,10 @@ on: - "src/**/Entities/**/*.cs" # Database entity definitions jobs: - check-test-secrets: - name: Check for test secrets - runs-on: ubuntu-22.04 - outputs: - available: ${{ steps.check-test-secrets.outputs.available }} - permissions: - contents: read - - steps: - - name: Check - id: check-test-secrets - run: | - if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then - echo "available=true" >> $GITHUB_OUTPUT; - else - echo "available=false" >> $GITHUB_OUTPUT; - fi test: name: Run tests runs-on: ubuntu-22.04 - needs: check-test-secrets steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -167,7 +149,7 @@ jobs: - name: Report test results uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 - if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && && !cancelled() }} with: name: Test Results path: "**/*-test-results.trx" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 817547fc65..8d115b39cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,29 +13,11 @@ env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" jobs: - check-test-secrets: - name: Check for test secrets - runs-on: ubuntu-22.04 - outputs: - available: ${{ steps.check-test-secrets.outputs.available }} - permissions: - contents: read - - steps: - - name: Check - id: check-test-secrets - run: | - if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then - echo "available=true" >> $GITHUB_OUTPUT; - else - echo "available=false" >> $GITHUB_OUTPUT; - fi testing: name: Run tests if: ${{ startsWith(github.head_ref, 'version_bump_') == false }} runs-on: ubuntu-22.04 - needs: check-test-secrets permissions: checks: write contents: read @@ -69,7 +51,7 @@ jobs: - name: Report test results uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 - if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results path: "**/*-test-results.trx" From 29dc69a77b7b1a781dc93cbbcd4bf5475184ddf1 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Mon, 10 Mar 2025 16:13:35 -0400 Subject: [PATCH 69/84] Remove extra && (#5484) --- .github/workflows/test-database.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 0e10d15ad1..a9c4737acc 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -149,7 +149,7 @@ jobs: - name: Report test results uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 - if: ${{ github.event.pull_request.head.repo.full_name == github.repository && && !cancelled() }} + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results path: "**/*-test-results.trx" From 224ef1272e92add54e04d31687e57221c93d7473 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 11 Mar 2025 10:46:09 +1000 Subject: [PATCH 70/84] [PM-18876] Refine PolicyRequirements API (#5445) * make the PolicyRequirements API more granular, e.g. replace factory methods with a factory interface * update Send to use the new API --- .../Implementations/PolicyRequirementQuery.cs | 18 ++- .../BasePolicyRequirementFactory.cs | 44 ++++++ .../DisableSendPolicyRequirement.cs | 27 ++++ .../PolicyRequirements/IPolicyRequirement.cs | 23 +-- .../IPolicyRequirementFactory.cs | 39 +++++ .../PolicyRequirementHelpers.cs | 36 +---- .../SendOptionsPolicyRequirement.cs | 34 +++++ .../SendPolicyRequirement.cs | 54 ------- .../PolicyServiceCollectionExtensions.cs | 29 +--- .../Services/Implementations/SendService.cs | 8 +- .../Policies/PolicyRequirementFixtures.cs | 23 +++ .../Policies/PolicyRequirementQueryTests.cs | 80 ++++++---- .../BasePolicyRequirementFactoryTests.cs | 90 ++++++++++++ ...isableSendPolicyRequirementFactoryTests.cs | 32 ++++ ...endOptionsPolicyRequirementFactoryTests.cs | 49 +++++++ .../SendPolicyRequirementTests.cs | 138 ------------------ .../Tools/Services/SendServiceTests.cs | 18 ++- 17 files changed, 429 insertions(+), 313 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs delete mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index 585d2348ef..de4796d4b5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -8,21 +8,25 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; public class PolicyRequirementQuery( IPolicyRepository policyRepository, - IEnumerable> factories) + IEnumerable> factories) : IPolicyRequirementQuery { public async Task GetAsync(Guid userId) where T : IPolicyRequirement { - var factory = factories.OfType>().SingleOrDefault(); + var factory = factories.OfType>().SingleOrDefault(); if (factory is null) { - throw new NotImplementedException("No Policy Requirement found for " + typeof(T)); + throw new NotImplementedException("No Requirement Factory found for " + typeof(T)); } - return factory(await GetPolicyDetails(userId)); + var policyDetails = await GetPolicyDetails(userId); + var filteredPolicies = policyDetails + .Where(p => p.PolicyType == factory.PolicyType) + .Where(factory.Enforce); + var requirement = factory.Create(filteredPolicies); + return requirement; } - private Task> GetPolicyDetails(Guid userId) => - policyRepository.GetPolicyDetailsByUserId(userId); + private Task> GetPolicyDetails(Guid userId) + => policyRepository.GetPolicyDetailsByUserId(userId); } - diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs new file mode 100644 index 0000000000..cebbe91904 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs @@ -0,0 +1,44 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// A simple base implementation of which will be suitable for most policies. +/// It provides sensible defaults to help teams to implement their own Policy Requirements. +/// +/// +public abstract class BasePolicyRequirementFactory : IPolicyRequirementFactory where T : IPolicyRequirement +{ + /// + /// User roles that are exempt from policy enforcement. + /// Owners and Admins are exempt by default but this may be overridden. + /// + protected virtual IEnumerable ExemptRoles { get; } = + [OrganizationUserType.Owner, OrganizationUserType.Admin]; + + /// + /// User statuses that are exempt from policy enforcement. + /// Invited and Revoked users are exempt by default, which is appropriate in the majority of cases. + /// + protected virtual IEnumerable ExemptStatuses { get; } = + [OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked]; + + /// + /// Whether a Provider User for the organization is exempt from policy enforcement. + /// Provider Users are exempt by default, which is appropriate in the majority of cases. + /// + protected virtual bool ExemptProviders { get; } = true; + + /// + public abstract PolicyType PolicyType { get; } + + public bool Enforce(PolicyDetails policyDetails) + => !policyDetails.HasRole(ExemptRoles) && + !policyDetails.HasStatus(ExemptStatuses) && + (!policyDetails.IsProvider || !ExemptProviders); + + /// + public abstract T Create(IEnumerable policyDetails); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs new file mode 100644 index 0000000000..1cb7f4f619 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs @@ -0,0 +1,27 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Disable Send policy. +/// +public class DisableSendPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends. + /// They may still delete existing Sends. + /// + public bool DisableSend { get; init; } +} + +public class DisableSendPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.DisableSend; + + public override DisableSendPolicyRequirement Create(IEnumerable policyDetails) + { + var result = new DisableSendPolicyRequirement { DisableSend = policyDetails.Any() }; + return result; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs index 3f331b1130..dcb82b1ac0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs @@ -1,24 +1,11 @@ -#nullable enable - -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; /// -/// Represents the business requirements of how one or more enterprise policies will be enforced against a user. -/// The implementation of this interface will depend on how the policies are enforced in the relevant domain. +/// An object that represents how a will be enforced against a user. +/// This acts as a bridge between the entity saved to the database and the domain that the policy +/// affects. You may represent the impact of the policy in any way that makes sense for the domain. /// public interface IPolicyRequirement; - -/// -/// A factory function that takes a sequence of and transforms them into a single -/// for consumption by the relevant domain. This will receive *all* policy types -/// that may be enforced against a user; when implementing this delegate, you must filter out irrelevant policy types -/// as well as policies that should not be enforced against a user (e.g. due to the user's role or status). -/// -/// -/// See for extension methods to handle common requirements when implementing -/// this delegate. -/// -public delegate T RequirementFactory(IEnumerable policyDetails) - where T : IPolicyRequirement; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs new file mode 100644 index 0000000000..e0b51a46a2 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs @@ -0,0 +1,39 @@ +#nullable enable + +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// An interface that defines how to create a single from a sequence of +/// . +/// +/// The that the factory produces. +/// +/// See for a simple base implementation suitable for most policies. +/// +public interface IPolicyRequirementFactory where T : IPolicyRequirement +{ + /// + /// The that the requirement relates to. + /// + PolicyType PolicyType { get; } + + /// + /// A predicate that determines whether a policy should be enforced against the user. + /// + /// Use this to exempt users based on their role, status or other attributes. + /// Policy details for the defined PolicyType. + /// True if the policy should be enforced against the user, false otherwise. + bool Enforce(PolicyDetails policyDetails); + + /// + /// A reducer method that creates a single from a set of PolicyDetails. + /// + /// + /// PolicyDetails for the specified PolicyType, after they have been filtered by the Enforce predicate. That is, + /// this is the final interface to be called. + /// + T Create(IEnumerable policyDetails); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs index fc4cd91a3d..3497c18031 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -7,35 +6,16 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements public static class PolicyRequirementHelpers { /// - /// Filters the PolicyDetails by PolicyType. This is generally required to only get the PolicyDetails that your - /// IPolicyRequirement relates to. + /// Returns true if the is for one of the specified roles, false otherwise. /// - public static IEnumerable GetPolicyType( - this IEnumerable policyDetails, - PolicyType type) - => policyDetails.Where(x => x.PolicyType == type); - - /// - /// Filters the PolicyDetails to remove the specified user roles. This can be used to exempt - /// owners and admins from policy enforcement. - /// - public static IEnumerable ExemptRoles( - this IEnumerable policyDetails, + public static bool HasRole( + this PolicyDetails policyDetails, IEnumerable roles) - => policyDetails.Where(x => !roles.Contains(x.OrganizationUserType)); + => roles.Contains(policyDetails.OrganizationUserType); /// - /// Filters the PolicyDetails to remove organization users who are also provider users for the organization. - /// This can be used to exempt provider users from policy enforcement. + /// Returns true if the relates to one of the specified statuses, false otherwise. /// - public static IEnumerable ExemptProviders(this IEnumerable policyDetails) - => policyDetails.Where(x => !x.IsProvider); - - /// - /// Filters the PolicyDetails to remove the specified organization user statuses. For example, this can be used - /// to exempt users in the invited and revoked statuses from policy enforcement. - /// - public static IEnumerable ExemptStatus( - this IEnumerable policyDetails, IEnumerable status) - => policyDetails.Where(x => !status.Contains(x.OrganizationUserStatus)); + public static bool HasStatus(this PolicyDetails policyDetails, IEnumerable status) + => status.Contains(policyDetails.OrganizationUserStatus); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs new file mode 100644 index 0000000000..9ba11c11df --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs @@ -0,0 +1,34 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Send Options policy. +/// +public class SendOptionsPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send. + /// + public bool DisableHideEmail { get; init; } +} + +public class SendOptionsPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.SendOptions; + + public override SendOptionsPolicyRequirement Create(IEnumerable policyDetails) + { + var result = policyDetails + .Select(p => p.GetDataModel()) + .Aggregate( + new SendOptionsPolicyRequirement(), + (result, data) => new SendOptionsPolicyRequirement + { + DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail + }); + + return result; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs deleted file mode 100644 index c54cc98373..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirement.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.Enums; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -/// -/// Policy requirements for the Disable Send and Send Options policies. -/// -public class SendPolicyRequirement : IPolicyRequirement -{ - /// - /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends. - /// They may still delete existing Sends. - /// - public bool DisableSend { get; init; } - /// - /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send. - /// - public bool DisableHideEmail { get; init; } - - /// - /// Create a new SendPolicyRequirement. - /// - /// All PolicyDetails relating to the user. - /// - /// This is a for the SendPolicyRequirement. - /// - public static SendPolicyRequirement Create(IEnumerable policyDetails) - { - var filteredPolicies = policyDetails - .ExemptRoles([OrganizationUserType.Owner, OrganizationUserType.Admin]) - .ExemptStatus([OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked]) - .ExemptProviders() - .ToList(); - - var result = filteredPolicies - .GetPolicyType(PolicyType.SendOptions) - .Select(p => p.GetDataModel()) - .Aggregate( - new SendPolicyRequirement - { - // Set Disable Send requirement in the initial seed - DisableSend = filteredPolicies.GetPolicyType(PolicyType.DisableSend).Any() - }, - (result, data) => new SendPolicyRequirement - { - DisableSend = result.DisableSend, - DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail - }); - - return result; - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 7bc8a7b5a3..6c698f9ffc 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -31,32 +31,7 @@ public static class PolicyServiceCollectionExtensions private static void AddPolicyRequirements(this IServiceCollection services) { - // Register policy requirement factories here - services.AddPolicyRequirement(SendPolicyRequirement.Create); + services.AddScoped, DisableSendPolicyRequirementFactory>(); + services.AddScoped, SendOptionsPolicyRequirementFactory>(); } - - /// - /// Used to register simple policy requirements where its factory method implements CreateRequirement. - /// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has - /// the correct type to be injected and then identified by at runtime. - /// - /// The specific PolicyRequirement being registered. - private static void AddPolicyRequirement(this IServiceCollection serviceCollection, RequirementFactory factory) - where T : class, IPolicyRequirement - => serviceCollection.AddPolicyRequirement(_ => factory); - - /// - /// Used to register policy requirements where you need to access additional dependencies (usually to return a - /// curried factory method). - /// This MUST be used rather than calling AddScoped directly, because it will ensure the factory method has - /// the correct type to be injected and then identified by at runtime. - /// - /// - /// A callback that takes IServiceProvider and returns a RequirementFactory for - /// your policy requirement. - /// - private static void AddPolicyRequirement(this IServiceCollection serviceCollection, - Func> factory) - where T : class, IPolicyRequirement - => serviceCollection.AddScoped>(factory); } diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs index bddaa93bfc..e09787d7eb 100644 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ b/src/Core/Tools/Services/Implementations/SendService.cs @@ -326,14 +326,14 @@ public class SendService : ISendService return; } - var sendPolicyRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - - if (sendPolicyRequirement.DisableSend) + var disableSendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (disableSendRequirement.DisableSend) { throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); } - if (sendPolicyRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) + var sendOptionsRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) { throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs new file mode 100644 index 0000000000..4838d1e3c4 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs @@ -0,0 +1,23 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +/// +/// Intentionally simplified PolicyRequirement that just holds the input PolicyDetails for us to assert against. +/// +public class TestPolicyRequirement : IPolicyRequirement +{ + public IEnumerable Policies { get; init; } = []; +} + +public class TestPolicyRequirementFactory(Func enforce) : IPolicyRequirementFactory +{ + public PolicyType PolicyType => PolicyType.SingleOrg; + + public bool Enforce(PolicyDetails policyDetails) => enforce(policyDetails); + + public TestPolicyRequirement Create(IEnumerable policyDetails) + => new() { Policies = policyDetails }; +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs index 4c98353774..56b6740678 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -11,50 +11,72 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; [SutProviderCustomize] public class PolicyRequirementQueryTests { - /// - /// Tests that the query correctly registers, retrieves and instantiates arbitrary IPolicyRequirements - /// according to their provided CreateRequirement delegate. - /// [Theory, BitAutoData] - public async Task GetAsync_Works(Guid userId, Guid organizationId) + public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId) { + var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; + var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso }; var policyRepository = Substitute.For(); - var factories = new List> - { - // In prod this cast is handled when the CreateRequirement delegate is registered in DI - (RequirementFactory)TestPolicyRequirement.Create - }; + policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]); - var sut = new PolicyRequirementQuery(policyRepository, factories); - policyRepository.GetPolicyDetailsByUserId(userId).Returns([ - new PolicyDetails - { - OrganizationId = organizationId - } - ]); + var factory = new TestPolicyRequirementFactory(_ => true); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); var requirement = await sut.GetAsync(userId); - Assert.Equal(organizationId, requirement.OrganizationId); + + Assert.Contains(thisPolicy, requirement.Policies); + Assert.DoesNotContain(otherPolicy, requirement.Policies); } [Theory, BitAutoData] - public async Task GetAsync_ThrowsIfNoRequirementRegistered(Guid userId) + public async Task GetAsync_CallsEnforceCallback(Guid userId) + { + // Arrange policies + var policyRepository = Substitute.For(); + var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; + var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; + policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]); + + // Arrange a substitute Enforce function so that we can inspect the received calls + var callback = Substitute.For>(); + callback(Arg.Any()).Returns(x => x.Arg() == thisPolicy); + + // Arrange the sut + var factory = new TestPolicyRequirementFactory(callback); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + // Act + var requirement = await sut.GetAsync(userId); + + // Assert + Assert.Contains(thisPolicy, requirement.Policies); + Assert.DoesNotContain(otherPolicy, requirement.Policies); + callback.Received()(Arg.Is(thisPolicy)); + callback.Received()(Arg.Is(otherPolicy)); + } + + [Theory, BitAutoData] + public async Task GetAsync_ThrowsIfNoFactoryRegistered(Guid userId) { var policyRepository = Substitute.For(); var sut = new PolicyRequirementQuery(policyRepository, []); var exception = await Assert.ThrowsAsync(() => sut.GetAsync(userId)); - Assert.Contains("No Policy Requirement found", exception.Message); + Assert.Contains("No Requirement Factory found", exception.Message); } - /// - /// Intentionally simplified PolicyRequirement that just holds the Policy.OrganizationId for us to assert against. - /// - private class TestPolicyRequirement : IPolicyRequirement + [Theory, BitAutoData] + public async Task GetAsync_HandlesNoPolicies(Guid userId) { - public Guid OrganizationId { get; init; } - public static TestPolicyRequirement Create(IEnumerable policyDetails) - => new() { OrganizationId = policyDetails.Single().OrganizationId }; + var policyRepository = Substitute.For(); + policyRepository.GetPolicyDetailsByUserId(userId).Returns([]); + + var factory = new TestPolicyRequirementFactory(x => x.IsProvider); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + var requirement = await sut.GetAsync(userId); + + Assert.Empty(requirement.Policies); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..e81459808d --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs @@ -0,0 +1,90 @@ +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Enums; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +public class BasePolicyRequirementFactoryTests +{ + [Theory, AutoData] + public void ExemptRoles_DoesNotEnforceAgainstThoseRoles( + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner)] PolicyDetails ownerPolicy, + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Admin)] PolicyDetails adminPolicy, + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Custom)] PolicyDetails customPolicy, + [PolicyDetails(PolicyType.SingleOrg)] PolicyDetails userPolicy) + { + var sut = new TestPolicyRequirementFactory( + // These exempt roles are intentionally unusual to make sure we're properly testing the sut + [OrganizationUserType.User, OrganizationUserType.Custom], + [], + false); + + Assert.True(sut.Enforce(ownerPolicy)); + Assert.True(sut.Enforce(adminPolicy)); + Assert.False(sut.Enforce(customPolicy)); + Assert.False(sut.Enforce(userPolicy)); + } + + [Theory, AutoData] + public void ExemptStatuses_DoesNotEnforceAgainstThoseStatuses( + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails invitedPolicy, + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails acceptedPolicy, + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails confirmedPolicy, + [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Revoked)] PolicyDetails revokedPolicy) + { + var sut = new TestPolicyRequirementFactory( + [], + // These exempt statuses are intentionally unusual to make sure we're properly testing the sut + [OrganizationUserStatusType.Confirmed, OrganizationUserStatusType.Accepted], + false); + + Assert.True(sut.Enforce(invitedPolicy)); + Assert.True(sut.Enforce(revokedPolicy)); + Assert.False(sut.Enforce(confirmedPolicy)); + Assert.False(sut.Enforce(acceptedPolicy)); + } + + [Theory, AutoData] + public void ExemptProviders_DoesNotEnforceAgainstProviders( + [PolicyDetails(PolicyType.SingleOrg, isProvider: true)] PolicyDetails policy) + { + var sut = new TestPolicyRequirementFactory( + [], + [], + true); + + Assert.False(sut.Enforce(policy)); + } + + [Theory, AutoData] + public void NoExemptions_EnforcesAgainstAdminsAndProviders( + [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner, isProvider: true)] PolicyDetails policy) + { + var sut = new TestPolicyRequirementFactory( + [], + [], + false); + + Assert.True(sut.Enforce(policy)); + } + + private class TestPolicyRequirementFactory( + IEnumerable exemptRoles, + IEnumerable exemptStatuses, + bool exemptProviders + ) : BasePolicyRequirementFactory + { + public override PolicyType PolicyType => PolicyType.SingleOrg; + protected override IEnumerable ExemptRoles => exemptRoles; + protected override IEnumerable ExemptStatuses => exemptStatuses; + + protected override bool ExemptProviders => exemptProviders; + + public override TestPolicyRequirement Create(IEnumerable policyDetails) + => new() { Policies = policyDetails }; + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..2304c0e9ae --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs @@ -0,0 +1,32 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class DisableSendPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisableSend_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableSend); + } + + [Theory, BitAutoData] + public void DisableSend_IsTrue_IfAnyDisableSendPolicies( + [PolicyDetails(PolicyType.DisableSend)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableSend); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..af66d858ef --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs @@ -0,0 +1,49 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class SendOptionsPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void DisableHideEmail_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsFalse_IfNotConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.False(actual.DisableHideEmail); + } + + [Theory, BitAutoData] + public void DisableHideEmail_IsTrue_IfAnyConfigured( + [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies, + SutProvider sutProvider + ) + { + policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); + policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false }); + + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.DisableHideEmail); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs deleted file mode 100644 index 4d7bf5db4e..0000000000 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendPolicyRequirementTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using AutoFixture.Xunit2; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.Enums; -using Bit.Core.Test.AdminConsole.AutoFixture; -using Xunit; - -namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; - -public class SendPolicyRequirementTests -{ - [Theory, AutoData] - public void DisableSend_IsFalse_IfNoDisableSendPolicies( - [PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1, - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails otherPolicy2) - { - EnableDisableHideEmail(otherPolicy2); - - var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]); - - Assert.False(actual.DisableSend); - } - - [Theory] - [InlineAutoData(OrganizationUserType.Owner, false)] - [InlineAutoData(OrganizationUserType.Admin, false)] - [InlineAutoData(OrganizationUserType.User, true)] - [InlineAutoData(OrganizationUserType.Custom, true)] - public void DisableSend_TestRoles( - OrganizationUserType userType, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails) - { - policyDetails.OrganizationUserType = userType; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableSend); - } - - [Theory, AutoData] - public void DisableSend_Not_EnforcedAgainstProviders( - [PolicyDetails(PolicyType.DisableSend, isProvider: true)] PolicyDetails policyDetails) - { - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.False(actual.DisableSend); - } - - [Theory] - [InlineAutoData(OrganizationUserStatusType.Confirmed, true)] - [InlineAutoData(OrganizationUserStatusType.Accepted, true)] - [InlineAutoData(OrganizationUserStatusType.Invited, false)] - [InlineAutoData(OrganizationUserStatusType.Revoked, false)] - public void DisableSend_TestStatuses( - OrganizationUserStatusType userStatus, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.DisableSend)] PolicyDetails policyDetails) - { - policyDetails.OrganizationUserStatus = userStatus; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableSend); - } - - [Theory, AutoData] - public void DisableHideEmail_IsFalse_IfNoSendOptionsPolicies( - [PolicyDetails(PolicyType.RequireSso)] PolicyDetails otherPolicy1, - [PolicyDetails(PolicyType.DisableSend)] PolicyDetails otherPolicy2) - { - var actual = SendPolicyRequirement.Create([otherPolicy1, otherPolicy2]); - - Assert.False(actual.DisableHideEmail); - } - - [Theory] - [InlineAutoData(OrganizationUserType.Owner, false)] - [InlineAutoData(OrganizationUserType.Admin, false)] - [InlineAutoData(OrganizationUserType.User, true)] - [InlineAutoData(OrganizationUserType.Custom, true)] - public void DisableHideEmail_TestRoles( - OrganizationUserType userType, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) - { - EnableDisableHideEmail(policyDetails); - policyDetails.OrganizationUserType = userType; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableHideEmail); - } - - [Theory, AutoData] - public void DisableHideEmail_Not_EnforcedAgainstProviders( - [PolicyDetails(PolicyType.SendOptions, isProvider: true)] PolicyDetails policyDetails) - { - EnableDisableHideEmail(policyDetails); - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.False(actual.DisableHideEmail); - } - - [Theory] - [InlineAutoData(OrganizationUserStatusType.Confirmed, true)] - [InlineAutoData(OrganizationUserStatusType.Accepted, true)] - [InlineAutoData(OrganizationUserStatusType.Invited, false)] - [InlineAutoData(OrganizationUserStatusType.Revoked, false)] - public void DisableHideEmail_TestStatuses( - OrganizationUserStatusType userStatus, - bool shouldBeEnforced, - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) - { - EnableDisableHideEmail(policyDetails); - policyDetails.OrganizationUserStatus = userStatus; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.Equal(shouldBeEnforced, actual.DisableHideEmail); - } - - [Theory, AutoData] - public void DisableHideEmail_HandlesNullData( - [PolicyDetails(PolicyType.SendOptions)] PolicyDetails policyDetails) - { - policyDetails.PolicyData = null; - - var actual = SendPolicyRequirement.Create([policyDetails]); - - Assert.False(actual.DisableHideEmail); - } - - private static void EnableDisableHideEmail(PolicyDetails policyDetails) - => policyDetails.SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true }); -} diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs index ae65ee1388..86d476340d 100644 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ b/test/Core.Test/Tools/Services/SendServiceTests.cs @@ -123,10 +123,12 @@ public class SendServiceTests // Disable Send policy check - vNext private void SaveSendAsync_Setup_vNext(SutProvider sutProvider, Send send, - SendPolicyRequirement sendPolicyRequirement) + DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement) { - sutProvider.GetDependency().GetAsync(send.UserId!.Value) - .Returns(sendPolicyRequirement); + sutProvider.GetDependency().GetAsync(send.UserId!.Value) + .Returns(disableSendPolicyRequirement); + sutProvider.GetDependency().GetAsync(send.UserId!.Value) + .Returns(sendOptionsPolicyRequirement); sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); // Should not be called in these tests @@ -141,7 +143,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableSend = true }); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement { DisableSend = true }, new SendOptionsPolicyRequirement()); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.", @@ -155,7 +157,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); await sutProvider.Sut.SaveSendAsync(send); @@ -171,7 +173,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true }); send.HideEmail = true; var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); @@ -185,7 +187,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement { DisableHideEmail = true }); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true }); send.HideEmail = false; await sutProvider.Sut.SaveSendAsync(send); @@ -200,7 +202,7 @@ public class SendServiceTests SutProvider sutProvider, [NewUserSendCustomize] Send send) { send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new SendPolicyRequirement()); + SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); send.HideEmail = true; await sutProvider.Sut.SaveSendAsync(send); From 6510f2a3e80d84263d8a077b7d584b0bca2236c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 11 Mar 2025 10:10:20 +0000 Subject: [PATCH 71/84] [PM-18088] Add unit test coverage for admin methods on CiphersController and CipherService (#5460) * Add comprehensive test coverage for CipherService restore, delete, and soft delete methods * Add comprehensive admin cipher management tests for CiphersController * Enhance CiphersController admin methods with comprehensive test coverage - Add tests for provider user scenarios in admin cipher management methods - Implement tests for custom user with edit any collection permissions - Add test coverage for RestrictProviderAccess feature flag - Improve test scenarios for delete, soft delete, and restore operations * Refactor CiphersControllerTests to simplify and optimize test methods * Optimize CiphersControllerTests with code cleanup and test method improvements * Extend CiphersControllerTests to support Admin and Owner roles * Add test cases for custom user cipher admin operations with EditAnyCollection permission checks - Extend CiphersControllerTests with scenarios for custom users without EditAnyCollection permission - Add test methods to verify NotFoundException is thrown when EditAnyCollection is false - Cover delete, soft delete, and restore operations for single and bulk cipher admin actions * Enhance CiphersControllerTests with granular access permission scenarios - Add test methods for admin and owner roles with specific cipher access scenarios - Implement tests for accessing specific and unassigned ciphers - Extend test coverage for delete, soft delete, and restore operations - Improve test method naming for clarity and precision * Add bulk admin cipher delete and soft delete tests for specific and unassigned ciphers - Implement test methods for DeleteManyAdmin and PutDeleteManyAdmin - Cover scenarios for owner and admin roles with access to specific and unassigned ciphers - Verify correct invocation of DeleteManyAsync and SoftDeleteManyAsync methods - Enhance test coverage for bulk cipher admin operations --- .../Controllers/CiphersControllerTests.cs | 1043 +++++++++++++++++ .../Vault/Services/CipherServiceTests.cs | 254 ++++ 2 files changed, 1297 insertions(+) diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 5c8de51062..14013d9c1c 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1,6 +1,8 @@ using System.Security.Claims; +using System.Text.Json; using Bit.Api.Vault.Controllers; using Bit.Api.Vault.Models.Request; +using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.Context; using Bit.Core.Entities; @@ -232,4 +234,1045 @@ public class CiphersControllerTests await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(organization.Id); } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task DeleteAdmin_WithOwnerOrAdmin_WithAccessToSpecificCipher_DeletesCipher( + OrganizationUserType organizationUserType, Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(new List + { + new() { Id = cipher.Id, OrganizationId = cipher.OrganizationId, Edit = true } + }); + + await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task DeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_DeletesCipher( + OrganizationUserType organizationUserType, Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency() + .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) + .Returns(new List { new() { Id = cipher.Id } }); + + await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task DeleteAdmin_WithAdminOrOwnerAndAccessToAllCollectionItems_DeletesCipher( + OrganizationUserType organizationUserType, Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility + { + Id = organization.Id, + AllowAdminAccessToAllCollectionItems = true + }); + + await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task DeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_DeletesCipher( + Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = true; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + + await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task DeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( + Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString())); + } + + [Theory] + [BitAutoData] + public async Task DeleteAdmin_WithProviderUser_DeletesCipher( + Cipher cipher, Guid userId, SutProvider sutProvider) + { + cipher.OrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipher.OrganizationId.Value).Returns(new List { cipher }); + + await sutProvider.Sut.DeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task DeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + Cipher cipher, Guid userId, SutProvider sutProvider) + { + cipher.OrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipher.Id.ToString())); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithAccessToSpecificCiphers_DeletesCiphers( + OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(ciphers.Select(c => new CipherDetails + { + Id = c.Id, + OrganizationId = organization.Id, + Edit = true + }).ToList()); + + await sutProvider.Sut.DeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCiphers_DeletesCiphers( + OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) + .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id }).ToList()); + + await sutProvider.Sut.DeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_DeletesCiphers( + OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility + { + Id = organization.Id, + AllowAdminAccessToAllCollectionItems = true + }); + + await sutProvider.Sut.DeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyAdmin_WithCustomUser_WithEditAnyCollectionTrue_DeletesCiphers( + CipherBulkDeleteRequestModel model, + Guid userId, List ciphers, CurrentContextOrganization organization, + SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = true; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + + await sutProvider.Sut.DeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( + CipherBulkDeleteRequestModel model, + Guid userId, List ciphers, CurrentContextOrganization organization, + SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteManyAdmin(model)); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyAdmin_WithProviderUser_DeletesCiphers( + CipherBulkDeleteRequestModel model, Guid userId, + List ciphers, SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + model.OrganizationId = organizationId.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + + foreach (var cipher in ciphers) + { + cipher.OrganizationId = organizationId; + } + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId).Returns(ciphers); + + await sutProvider.Sut.DeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organizationId, true); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + CipherBulkDeleteRequestModel model, SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + model.OrganizationId = organizationId.ToString(); + + sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteManyAdmin(model)); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToSpecificCipher_SoftDeletesCipher( + OrganizationUserType organizationUserType, Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(new List + { + new() { Id = cipher.Id, OrganizationId = cipher.OrganizationId, Edit = true } + }); + + await sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_SoftDeletesCipher( + OrganizationUserType organizationUserType, Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + sutProvider.GetDependency() + .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) + .Returns(new List { new() { Id = cipher.Id } }); + + await sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_SoftDeletesCipher( + OrganizationUserType organizationUserType, Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility + { + Id = organization.Id, + AllowAdminAccessToAllCollectionItems = true + }); + + await sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_SoftDeletesCipher( + Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = true; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + + await sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( + Cipher cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString())); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteAdmin_WithProviderUser_SoftDeletesCipher( + Cipher cipher, Guid userId, SutProvider sutProvider) + { + cipher.OrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipher.OrganizationId.Value).Returns(new List { cipher }); + + await sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString()); + + await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + Cipher cipher, Guid userId, SutProvider sutProvider) + { + cipher.OrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); + sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id.ToString())); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithAccessToSpecificCiphers_SoftDeletesCiphers( + OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(ciphers.Select(c => new CipherDetails + { + Id = c.Id, + OrganizationId = organization.Id, + Edit = true + }).ToList()); + + await sutProvider.Sut.PutDeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .SoftDeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCiphers_SoftDeletesCiphers( + OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) + .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id }).ToList()); + + await sutProvider.Sut.PutDeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .SoftDeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_SoftDeletesCiphers( + OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility + { + Id = organization.Id, + AllowAdminAccessToAllCollectionItems = true + }); + + await sutProvider.Sut.PutDeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .SoftDeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteManyAdmin_WithCustomUser_WithEditAnyCollectionTrue_SoftDeletesCiphers( + CipherBulkDeleteRequestModel model, + Guid userId, List ciphers, CurrentContextOrganization organization, + SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = true; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + + await sutProvider.Sut.PutDeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .SoftDeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteManyAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( + CipherBulkDeleteRequestModel model, + Guid userId, List ciphers, CurrentContextOrganization organization, + SutProvider sutProvider) + { + model.OrganizationId = organization.Id.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteManyAdmin(model)); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteManyAdmin_WithProviderUser_SoftDeletesCiphers( + CipherBulkDeleteRequestModel model, Guid userId, + List ciphers, SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + model.OrganizationId = organizationId.ToString(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + + foreach (var cipher in ciphers) + { + cipher.OrganizationId = organizationId; + } + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId).Returns(ciphers); + + await sutProvider.Sut.PutDeleteManyAdmin(model); + + await sutProvider.GetDependency() + .Received(1) + .SoftDeleteManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), + userId, organizationId, true); + } + + [Theory] + [BitAutoData] + public async Task PutDeleteManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + CipherBulkDeleteRequestModel model, SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + model.OrganizationId = organizationId.ToString(); + + sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteManyAdmin(model)); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToSpecificCipher_RestoresCipher( + OrganizationUserType organizationUserType, CipherDetails cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + cipher.Type = CipherType.Login; + cipher.Data = JsonSerializer.Serialize(new CipherLoginData()); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(new List + { + new() { Id = cipher.Id, OrganizationId = cipher.OrganizationId, Edit = true } + }); + + var result = await sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString()); + + Assert.NotNull(result); + Assert.IsType(result); + await sutProvider.GetDependency().Received(1).RestoreAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_RestoresCipher( + OrganizationUserType organizationUserType, CipherDetails cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + cipher.Type = CipherType.Login; + cipher.Data = JsonSerializer.Serialize(new CipherLoginData()); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency() + .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) + .Returns(new List { new() { Id = cipher.Id } }); + + var result = await sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString()); + + Assert.NotNull(result); + Assert.IsType(result); + await sutProvider.GetDependency().Received(1).RestoreAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_RestoresCipher( + OrganizationUserType organizationUserType, CipherDetails cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + cipher.Type = CipherType.Login; + cipher.Data = JsonSerializer.Serialize(new CipherLoginData()); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility + { + Id = organization.Id, + AllowAdminAccessToAllCollectionItems = true + }); + + var result = await sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString()); + + Assert.NotNull(result); + await sutProvider.GetDependency().Received(1).RestoreAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionTrue_RestoresCipher( + CipherDetails cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + cipher.Type = CipherType.Login; + cipher.Data = JsonSerializer.Serialize(new CipherLoginData()); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = true; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + + var result = await sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString()); + + Assert.NotNull(result); + Assert.IsType(result); + await sutProvider.GetDependency().Received(1).RestoreAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( + CipherDetails cipher, Guid userId, + CurrentContextOrganization organization, SutProvider sutProvider) + { + cipher.OrganizationId = organization.Id; + cipher.Type = CipherType.Login; + cipher.Data = JsonSerializer.Serialize(new CipherLoginData()); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipher }); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString())); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreAdmin_WithProviderUser_RestoresCipher( + CipherDetails cipher, Guid userId, SutProvider sutProvider) + { + cipher.OrganizationId = Guid.NewGuid(); + cipher.Type = CipherType.Login; + cipher.Data = JsonSerializer.Serialize(new CipherLoginData()); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipher.OrganizationId.Value).Returns(new List { cipher }); + + var result = await sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString()); + + Assert.NotNull(result); + Assert.IsType(result); + await sutProvider.GetDependency().Received(1).RestoreAsync(cipher, userId, true); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + CipherDetails cipher, Guid userId, SutProvider sutProvider) + { + cipher.OrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipher.Id).Returns(cipher); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipher.Id.ToString())); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithAccessToSpecificCiphers_RestoresCiphers( + OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id; + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId) + .Returns(ciphers.Select(c => new CipherDetails + { + Id = c.Id, + OrganizationId = organization.Id, + Edit = true + }).ToList()); + + var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails + { + Id = c.Id, + OrganizationId = organization.Id + }).ToList(); + + sutProvider.GetDependency() + .RestoreManyAsync(Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, organization.Id, true) + .Returns(cipherOrgDetails); + + var result = await sutProvider.Sut.PutRestoreManyAdmin(model); + + Assert.NotNull(result); + await sutProvider.GetDependency().Received(1) + .RestoreManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCiphers_RestoresCiphers( + OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, + List ciphers, CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id; + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails + { + Id = c.Id, + OrganizationId = organization.Id, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()) + }).ToList(); + + sutProvider.GetDependency() + .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) + .Returns(cipherOrgDetails); + sutProvider.GetDependency() + .RestoreManyAsync(Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString()) && ids.Count == model.Ids.Count())), + userId, organization.Id, true) + .Returns(cipherOrgDetails); + + var result = await sutProvider.Sut.PutRestoreManyAdmin(model); + + Assert.NotNull(result); + Assert.Equal(model.Ids.Count(), result.Data.Count()); + await sutProvider.GetDependency() + .Received(1) + .RestoreManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_RestoresCiphers( + OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List ciphers, + CurrentContextOrganization organization, SutProvider sutProvider) + { + model.OrganizationId = organization.Id; + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = organizationUserType; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility + { + Id = organization.Id, + AllowAdminAccessToAllCollectionItems = true + }); + + var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails + { + Id = c.Id, + OrganizationId = organization.Id, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()) + }).ToList(); + + sutProvider.GetDependency() + .RestoreManyAsync(Arg.Any>(), userId, organization.Id, true) + .Returns(cipherOrgDetails); + + var result = await sutProvider.Sut.PutRestoreManyAdmin(model); + + Assert.NotNull(result); + Assert.Equal(ciphers.Count, result.Data.Count()); + await sutProvider.GetDependency().Received(1) + .RestoreManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreManyAdmin_WithCustomUser_WithEditAnyCollectionTrue_RestoresCiphers( + CipherBulkRestoreRequestModel model, + Guid userId, List ciphers, CurrentContextOrganization organization, + SutProvider sutProvider) + { + model.OrganizationId = organization.Id; + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = true; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + + var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails + { + Id = c.Id, + OrganizationId = organization.Id, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()) + }).ToList(); + + sutProvider.GetDependency() + .RestoreManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, organization.Id, true) + .Returns(cipherOrgDetails); + + var result = await sutProvider.Sut.PutRestoreManyAdmin(model); + + Assert.NotNull(result); + Assert.Equal(ciphers.Count, result.Data.Count()); + await sutProvider.GetDependency() + .Received(1) + .RestoreManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, organization.Id, true); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreManyAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException( + CipherBulkRestoreRequestModel model, + Guid userId, List ciphers, CurrentContextOrganization organization, + SutProvider sutProvider) + { + model.OrganizationId = organization.Id; + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + organization.Type = OrganizationUserType.Custom; + organization.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreManyAdmin(model)); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreManyAdmin_WithProviderUser_RestoresCiphers( + CipherBulkRestoreRequestModel model, Guid userId, + List ciphers, SutProvider sutProvider) + { + model.OrganizationId = Guid.NewGuid(); + model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().ProviderUserForOrgAsync(model.OrganizationId).Returns(true); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(model.OrganizationId).Returns(ciphers); + + var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails + { + Id = c.Id, + OrganizationId = model.OrganizationId + }).ToList(); + + sutProvider.GetDependency() + .RestoreManyAsync( + Arg.Any>(), + userId, model.OrganizationId, true) + .Returns(cipherOrgDetails); + + var result = await sutProvider.Sut.PutRestoreManyAdmin(model); + + Assert.NotNull(result); + await sutProvider.GetDependency() + .Received(1) + .RestoreManyAsync( + Arg.Is>(ids => + ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), + userId, model.OrganizationId, true); + } + + [Theory] + [BitAutoData] + public async Task PutRestoreManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + CipherBulkRestoreRequestModel model, SutProvider sutProvider) + { + model.OrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency().ProviderUserForOrgAsync(model.OrganizationId).Returns(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreManyAdmin(model)); + } } diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 4f02d94c9c..1803c980c2 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -602,6 +602,78 @@ public class CipherServiceTests Assert.NotEqual(initialRevisionDate, cipher.RevisionDate); } + [Theory] + [BitAutoData] + public async Task RestoreAsync_WithAlreadyRestoredCipher_SkipsOperation( + Guid restoringUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.DeletedDate = null; + + await sutProvider.Sut.RestoreAsync(cipher, restoringUserId, true); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RestoreAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException( + Guid restoringUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.UserId = Guid.NewGuid(); + cipher.OrganizationId = null; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RestoreAsync(cipher, restoringUserId)); + + Assert.Contains("do not have permissions", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default); + } + + [Theory] + [OrganizationCipherCustomize] + [BitAutoData] + public async Task RestoreAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException( + Guid restoringUserId, Cipher cipher, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetCanEditByIdAsync(restoringUserId, cipher.Id) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RestoreAsync(cipher, restoringUserId)); + + Assert.Contains("do not have permissions", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushSyncCipherUpdateAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RestoreAsync_WithCipherDetailsType_RestoresCipherDetails( + Guid restoringUserId, CipherDetails cipherDetails, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetCanEditByIdAsync(restoringUserId, cipherDetails.Id) + .Returns(true); + + var initialRevisionDate = new DateTime(1970, 1, 1, 0, 0, 0); + cipherDetails.DeletedDate = initialRevisionDate; + cipherDetails.RevisionDate = initialRevisionDate; + + await sutProvider.Sut.RestoreAsync(cipherDetails, restoringUserId); + + Assert.Null(cipherDetails.DeletedDate); + Assert.NotEqual(initialRevisionDate, cipherDetails.RevisionDate); + await sutProvider.GetDependency().Received(1).UpsertAsync(cipherDetails); + await sutProvider.GetDependency().Received(1).LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored); + await sutProvider.GetDependency().Received(1).PushSyncCipherUpdateAsync(cipherDetails, null); + } + [Theory] [BitAutoData] public async Task RestoreManyAsync_UpdatesCiphers(ICollection ciphers, @@ -725,6 +797,188 @@ public class CipherServiceTests Arg.Is>(arg => !arg.Except(ciphers).Any())); } + [Theory] + [BitAutoData] + public async Task DeleteAsync_WithPersonalCipherOwner_DeletesCipher( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.UserId = deletingUserId; + cipher.OrganizationId = null; + + await sutProvider.Sut.DeleteAsync(cipher, deletingUserId); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher); + await sutProvider.GetDependency().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id); + await sutProvider.GetDependency().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted); + await sutProvider.GetDependency().Received(1).PushSyncCipherDeleteAsync(cipher); + } + + [Theory] + [OrganizationCipherCustomize] + [BitAutoData] + public async Task DeleteAsync_WithOrgCipherAndEditPermission_DeletesCipher( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(true); + + await sutProvider.Sut.DeleteAsync(cipher, deletingUserId); + + await sutProvider.GetDependency().Received(1).DeleteAsync(cipher); + await sutProvider.GetDependency().Received(1).DeleteAttachmentsForCipherAsync(cipher.Id); + await sutProvider.GetDependency().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_Deleted); + await sutProvider.GetDependency().Received(1).PushSyncCipherDeleteAsync(cipher); + } + + [Theory] + [BitAutoData] + public async Task DeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.UserId = Guid.NewGuid(); + cipher.OrganizationId = null; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(cipher, deletingUserId)); + + Assert.Contains("do not have permissions", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default); + } + + [Theory] + [OrganizationCipherCustomize] + [BitAutoData] + public async Task DeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(cipher, deletingUserId)); + + Assert.Contains("do not have permissions", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteAttachmentsForCipherAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCipherEventAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushSyncCipherDeleteAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SoftDeleteAsync_WithPersonalCipherOwner_SoftDeletesCipher( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.UserId = deletingUserId; + cipher.OrganizationId = null; + cipher.DeletedDate = null; + + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(true); + + await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId); + + Assert.NotNull(cipher.DeletedDate); + Assert.Equal(cipher.RevisionDate, cipher.DeletedDate); + await sutProvider.GetDependency().Received(1).UpsertAsync(cipher); + await sutProvider.GetDependency().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted); + await sutProvider.GetDependency().Received(1).PushSyncCipherUpdateAsync(cipher, null); + } + + [Theory] + [OrganizationCipherCustomize] + [BitAutoData] + public async Task SoftDeleteAsync_WithOrgCipherAndEditPermission_SoftDeletesCipher( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.DeletedDate = null; + + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(true); + + await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId); + + Assert.NotNull(cipher.DeletedDate); + Assert.Equal(cipher.DeletedDate, cipher.RevisionDate); + await sutProvider.GetDependency().Received(1).UpsertAsync(cipher); + await sutProvider.GetDependency().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted); + await sutProvider.GetDependency().Received(1).PushSyncCipherUpdateAsync(cipher, null); + } + + [Theory] + [BitAutoData] + public async Task SoftDeleteAsync_WithPersonalCipherBelongingToDifferentUser_ThrowsBadRequestException( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + cipher.UserId = Guid.NewGuid(); + cipher.OrganizationId = null; + + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId)); + + Assert.Contains("do not have permissions", exception.Message); + } + + [Theory] + [OrganizationCipherCustomize] + [BitAutoData] + public async Task SoftDeleteAsync_WithOrgCipherLackingEditPermission_ThrowsBadRequestException( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId)); + + Assert.Contains("do not have permissions", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SoftDeleteAsync_WithCipherDetailsType_SoftDeletesCipherDetails( + Guid deletingUserId, CipherDetails cipher, SutProvider sutProvider) + { + cipher.DeletedDate = null; + + await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId, true); + + Assert.NotNull(cipher.DeletedDate); + Assert.Equal(cipher.DeletedDate, cipher.RevisionDate); + await sutProvider.GetDependency().Received(1).UpsertAsync(cipher); + await sutProvider.GetDependency().Received(1).LogCipherEventAsync(cipher, EventType.Cipher_SoftDeleted); + await sutProvider.GetDependency().Received(1).PushSyncCipherUpdateAsync(cipher, null); + } + + [Theory] + [BitAutoData] + public async Task SoftDeleteAsync_WithAlreadySoftDeletedCipher_SkipsOperation( + Guid deletingUserId, Cipher cipher, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetCanEditByIdAsync(deletingUserId, cipher.Id) + .Returns(true); + cipher.DeletedDate = DateTime.UtcNow.AddDays(-1); + + await sutProvider.Sut.SoftDeleteAsync(cipher, deletingUserId); + + await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceive().LogCipherEventAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().DidNotReceive().PushSyncCipherUpdateAsync(Arg.Any(), Arg.Any>()); + } + private async Task AssertNoActionsAsync(SutProvider sutProvider) { await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default); From f038e8c5e4e064b469f35ccf94e676e46536b447 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:22:00 +0100 Subject: [PATCH 72/84] Create desktop-send-ui-refresh feature flag (#5487) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 6baa9227e1..0591617f5e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -114,6 +114,7 @@ public static class FeatureFlagKeys public const string ItemShare = "item-share"; public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; + public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; From 0153d9dfd9e4014bbf12f3d33a361d61da6704d9 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:01:23 -0400 Subject: [PATCH 73/84] Update DockerCompose template to point to ghcr.io registry (#5491) --- util/Setup/Templates/DockerCompose.hbs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/util/Setup/Templates/DockerCompose.hbs b/util/Setup/Templates/DockerCompose.hbs index ffe9121089..741e1085f9 100644 --- a/util/Setup/Templates/DockerCompose.hbs +++ b/util/Setup/Templates/DockerCompose.hbs @@ -15,7 +15,7 @@ services: mssql: - image: bitwarden/mssql:{{{CoreVersion}}} + image: ghcr.io/bitwarden/mssql:{{{CoreVersion}}} container_name: bitwarden-mssql restart: always stop_grace_period: 60s @@ -33,7 +33,7 @@ services: - ../env/mssql.override.env web: - image: bitwarden/web:{{{WebVersion}}} + image: ghcr.io/bitwarden/web:{{{WebVersion}}} container_name: bitwarden-web restart: always volumes: @@ -43,7 +43,7 @@ services: - ../env/uid.env attachments: - image: bitwarden/attachments:{{{CoreVersion}}} + image: ghcr.io/bitwarden/attachments:{{{CoreVersion}}} container_name: bitwarden-attachments restart: always volumes: @@ -53,7 +53,7 @@ services: - ../env/uid.env api: - image: bitwarden/api:{{{CoreVersion}}} + image: ghcr.io/bitwarden/api:{{{CoreVersion}}} container_name: bitwarden-api restart: always volumes: @@ -69,7 +69,7 @@ services: - public identity: - image: bitwarden/identity:{{{CoreVersion}}} + image: ghcr.io/bitwarden/identity:{{{CoreVersion}}} container_name: bitwarden-identity restart: always volumes: @@ -86,7 +86,7 @@ services: - public sso: - image: bitwarden/sso:{{{CoreVersion}}} + image: ghcr.io/bitwarden/sso:{{{CoreVersion}}} container_name: bitwarden-sso restart: always volumes: @@ -103,7 +103,7 @@ services: - public admin: - image: bitwarden/admin:{{{CoreVersion}}} + image: ghcr.io/bitwarden/admin:{{{CoreVersion}}} container_name: bitwarden-admin restart: always depends_on: @@ -121,7 +121,7 @@ services: - public icons: - image: bitwarden/icons:{{{CoreVersion}}} + image: ghcr.io/bitwarden/icons:{{{CoreVersion}}} container_name: bitwarden-icons restart: always volumes: @@ -135,7 +135,7 @@ services: - public notifications: - image: bitwarden/notifications:{{{CoreVersion}}} + image: ghcr.io/bitwarden/notifications:{{{CoreVersion}}} container_name: bitwarden-notifications restart: always volumes: @@ -150,7 +150,7 @@ services: - public events: - image: bitwarden/events:{{{CoreVersion}}} + image: ghcr.io/bitwarden/events:{{{CoreVersion}}} container_name: bitwarden-events restart: always volumes: @@ -165,7 +165,7 @@ services: - public nginx: - image: bitwarden/nginx:{{{CoreVersion}}} + image: ghcr.io/bitwarden/nginx:{{{CoreVersion}}} container_name: bitwarden-nginx restart: always depends_on: @@ -195,7 +195,7 @@ services: {{#if EnableKeyConnector}} key-connector: - image: bitwarden/key-connector:{{{KeyConnectorVersion}}} + image: ghcr.io/bitwarden/key-connector:{{{KeyConnectorVersion}}} container_name: bitwarden-key-connector restart: always volumes: @@ -212,7 +212,7 @@ services: {{#if EnableScim}} scim: - image: bitwarden/scim:{{{CoreVersion}}} + image: ghcr.io/bitwarden/scim:{{{CoreVersion}}} container_name: bitwarden-scim restart: always volumes: From 1b90bfe2a114e73b583100e955e57ac48b9733b9 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:01:50 -0400 Subject: [PATCH 74/84] [PM-18944] Update error response from invalid OTP (#5485) * fix(newDeviceVerification): updated error response from invalid OTP --- .../IdentityServer/RequestValidators/DeviceValidator.cs | 8 ++++---- test/Identity.Test/IdentityServer/DeviceValidatorTests.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 3ddc28c0e1..4744f4aca3 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -252,19 +252,19 @@ public class DeviceValidator( { case DeviceValidationResultType.InvalidUser: result.ErrorDescription = "Invalid user"; - customResponse.Add("ErrorModel", new ErrorResponseModel("invalid user")); + customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid user.")); break; case DeviceValidationResultType.InvalidNewDeviceOtp: result.ErrorDescription = "Invalid New Device OTP"; - customResponse.Add("ErrorModel", new ErrorResponseModel("invalid new device otp")); + customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid new device OTP. Try again.")); break; case DeviceValidationResultType.NewDeviceVerificationRequired: result.ErrorDescription = "New device verification required"; - customResponse.Add("ErrorModel", new ErrorResponseModel("new device verification required")); + customResponse.Add("ErrorModel", new ErrorResponseModel("New device verification required.")); break; case DeviceValidationResultType.NoDeviceInformationProvided: result.ErrorDescription = "No device information provided"; - customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided")); + customResponse.Add("ErrorModel", new ErrorResponseModel("No device information provided.")); break; } return (result, customResponse); diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index b71dd6c230..bcb357d640 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -172,7 +172,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); - var expectedErrorModel = new ErrorResponseModel("no device information provided"); + var expectedErrorModel = new ErrorResponseModel("No device information provided."); var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorModel.Message, actualResponse.Message); } @@ -418,7 +418,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); // PM-13340: The error message should be "invalid user" instead of "no device information provided" - var expectedErrorMessage = "no device information provided"; + var expectedErrorMessage = "No device information provided."; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); } @@ -552,7 +552,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); - var expectedErrorMessage = "invalid new device otp"; + var expectedErrorMessage = "Invalid new device OTP. Try again."; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); } @@ -604,7 +604,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); - var expectedErrorMessage = "new device verification required"; + var expectedErrorMessage = "New device verification required."; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); } From ef3b8b782a1a02fd9fccc80c1742b19092f7975f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:56:47 -0400 Subject: [PATCH 75/84] Provide plans to OrganizationEditModel for resellers (#5493) --- .../AdminConsole/Controllers/ProvidersController.cs | 10 ++++++++-- src/Admin/AdminConsole/Models/OrganizationEditModel.cs | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 9e3dc00cd6..c38bb64419 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; @@ -42,6 +43,7 @@ public class ProvidersController : Controller private readonly IFeatureService _featureService; private readonly IProviderPlanRepository _providerPlanRepository; private readonly IProviderBillingService _providerBillingService; + private readonly IPricingClient _pricingClient; private readonly string _stripeUrl; private readonly string _braintreeMerchantUrl; private readonly string _braintreeMerchantId; @@ -60,7 +62,8 @@ public class ProvidersController : Controller IFeatureService featureService, IProviderPlanRepository providerPlanRepository, IProviderBillingService providerBillingService, - IWebHostEnvironment webHostEnvironment) + IWebHostEnvironment webHostEnvironment, + IPricingClient pricingClient) { _organizationRepository = organizationRepository; _organizationService = organizationService; @@ -75,6 +78,7 @@ public class ProvidersController : Controller _featureService = featureService; _providerPlanRepository = providerPlanRepository; _providerBillingService = providerBillingService; + _pricingClient = pricingClient; _stripeUrl = webHostEnvironment.GetStripeUrl(); _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl(); _braintreeMerchantId = globalSettings.Braintree.MerchantId; @@ -415,7 +419,9 @@ public class ProvidersController : Controller return RedirectToAction("Index"); } - return View(new OrganizationEditModel(provider)); + var plans = await _pricingClient.ListPlans(); + + return View(new OrganizationEditModel(provider, plans)); } [HttpPost] diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 729b4f7990..1d23afd491 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -22,13 +22,14 @@ public class OrganizationEditModel : OrganizationViewModel public OrganizationEditModel() { } - public OrganizationEditModel(Provider provider) + public OrganizationEditModel(Provider provider, List plans) { Provider = provider; BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty; PlanType = Core.Billing.Enums.PlanType.TeamsMonthly; Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName(); LicenseKey = RandomLicenseKey; + _plans = plans; } public OrganizationEditModel( From a5c792dba9633030002fa2e993d77bfaef1f79cc Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:33:52 -0500 Subject: [PATCH 76/84] chore: organize vault team feature flag (#5494) --- src/Core/Constants.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0591617f5e..5da356faca 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -116,15 +116,21 @@ public static class FeatureFlagKeys public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; + /* Vault Team */ + public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; + public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; + public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; + public const string VaultBulkManagementAction = "vault-bulk-management-action"; + public const string RestrictProviderAccess = "restrict-provider-access"; + public const string SecurityTasks = "security-tasks"; + public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; public const string DuoRedirect = "duo-redirect"; public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string EmailVerification = "email-verification"; public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays"; - 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 InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; @@ -149,11 +155,7 @@ public static class FeatureFlagKeys public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string NewDeviceVerification = "new-device-verification"; - public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; - public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; - public const string SecurityTasks = "security-tasks"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; - public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string InlineMenuTotp = "inline-menu-totp"; public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; public const string AppReviewPrompt = "app-review-prompt"; From d40fbe32170080ab81a299681d541af486e6526b Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 13 Mar 2025 11:55:39 -0400 Subject: [PATCH 77/84] Upgrade test reporter (#5492) --- .github/workflows/test-database.yml | 3 +-- .github/workflows/test.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index a9c4737acc..26db5ea0a4 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -32,7 +32,6 @@ on: - "src/**/Entities/**/*.cs" # Database entity definitions jobs: - test: name: Run tests runs-on: ubuntu-22.04 @@ -148,7 +147,7 @@ jobs: run: 'docker logs $(docker ps --quiet --filter "name=mssql")' - name: Report test results - uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 + uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d115b39cd..e44d7aa8b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,6 @@ env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" jobs: - testing: name: Run tests if: ${{ startsWith(github.head_ref, 'version_bump_') == false }} @@ -50,7 +49,7 @@ jobs: run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" - name: Report test results - uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 + uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results From 2df4076a6b259d4dfec31c740ba99df152df1ccb Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:59:19 +0100 Subject: [PATCH 78/84] Add export-attachments feature flag (#5501) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5da356faca..72224319fb 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,6 +115,7 @@ public static class FeatureFlagKeys public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; + public const string ExportAttachments = "export-attachments"; /* Vault Team */ public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; From 7daf6cfad48260cb33f5f6b32f8f580a266d6f71 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:33:24 -0400 Subject: [PATCH 79/84] [PM-18794] Allow provider payment method (#5500) * Add PaymentSource to ProviderSubscriptionResponse * Add UpdatePaymentMethod to ProviderBillingController * Add GetTaxInformation to ProviderBillingController * Add VerifyBankAccount to ProviderBillingController * Add feature flag --- .../Billing/ProviderBillingService.cs | 13 +++ .../Controllers/ProviderBillingController.cs | 83 ++++++++++++++++++- .../Responses/ProviderSubscriptionResponse.cs | 9 +- .../Services/IProviderBillingService.cs | 11 +++ src/Core/Constants.cs | 1 + 5 files changed, 113 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 294a926022..74cfc1f916 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -628,6 +628,19 @@ public class ProviderBillingService( } } + public async Task UpdatePaymentMethod( + Provider provider, + TokenizedPaymentSource tokenizedPaymentSource, + TaxInformation taxInformation) + { + await Task.WhenAll( + subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource), + subscriberService.UpdateTaxInformation(provider, taxInformation)); + + await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, + new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically }); + } + public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command) { if (command.Configuration.Any(x => x.SeatsMinimum < 0)) diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 73c992040c..bb1fd7bb25 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -1,5 +1,6 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; @@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers; [Authorize("Application")] public class ProviderBillingController( ICurrentContext currentContext, + IFeatureService featureService, ILogger logger, IPricingClient pricingClient, IProviderBillingService providerBillingService, @@ -71,6 +73,65 @@ public class ProviderBillingController( "text/csv"); } + [HttpPut("payment-method")] + public async Task UpdatePaymentMethodAsync( + [FromRoute] Guid providerId, + [FromBody] UpdatePaymentMethodRequestBody requestBody) + { + var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); + + if (!allowProviderPaymentMethod) + { + return TypedResults.NotFound(); + } + + var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); + + if (provider == null) + { + return result; + } + + var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain(); + var taxInformation = requestBody.TaxInformation.ToDomain(); + + await providerBillingService.UpdatePaymentMethod( + provider, + tokenizedPaymentSource, + taxInformation); + + return TypedResults.Ok(); + } + + [HttpPost("payment-method/verify-bank-account")] + public async Task VerifyBankAccountAsync( + [FromRoute] Guid providerId, + [FromBody] VerifyBankAccountRequestBody requestBody) + { + var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); + + if (!allowProviderPaymentMethod) + { + return TypedResults.NotFound(); + } + + var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); + + if (provider == null) + { + return result; + } + + if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM")) + { + return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'"); + } + + await subscriberService.VerifyBankAccount(provider, requestBody.DescriptorCode); + + return TypedResults.Ok(); + } + [HttpGet("subscription")] public async Task GetSubscriptionAsync([FromRoute] Guid providerId) { @@ -102,12 +163,32 @@ public class ProviderBillingController( var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription); + var paymentSource = await subscriberService.GetPaymentSource(provider); + var response = ProviderSubscriptionResponse.From( subscription, configuredProviderPlans, taxInformation, subscriptionSuspension, - provider); + provider, + paymentSource); + + return TypedResults.Ok(response); + } + + [HttpGet("tax-information")] + public async Task GetTaxInformationAsync([FromRoute] Guid providerId) + { + var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); + + if (provider == null) + { + return result; + } + + var taxInformation = await subscriberService.GetTaxInformation(provider); + + var response = TaxInformationResponse.From(taxInformation); return TypedResults.Ok(response); } diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index 34c3817e51..ea1479c9df 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -16,7 +16,8 @@ public record ProviderSubscriptionResponse( TaxInformation TaxInformation, DateTime? CancelAt, SubscriptionSuspension Suspension, - ProviderType ProviderType) + ProviderType ProviderType, + PaymentSource PaymentSource) { private const string _annualCadence = "Annual"; private const string _monthlyCadence = "Monthly"; @@ -26,7 +27,8 @@ public record ProviderSubscriptionResponse( ICollection providerPlans, TaxInformation taxInformation, SubscriptionSuspension subscriptionSuspension, - Provider provider) + Provider provider, + PaymentSource paymentSource) { var providerPlanResponses = providerPlans .Select(providerPlan => @@ -57,7 +59,8 @@ public record ProviderSubscriptionResponse( taxInformation, subscription.CancelAt, subscriptionSuspension, - provider.Type); + provider.Type, + paymentSource); } } diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index d6983da03e..64585f3361 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -95,5 +95,16 @@ public interface IProviderBillingService Task SetupSubscription( Provider provider); + /// + /// Updates the 's payment source and tax information and then sets their subscription's collection_method to be "charge_automatically". + /// + /// The to update the payment source and tax information for. + /// The tokenized payment source (ex. Credit Card) to attach to the . + /// The 's updated tax information. + Task UpdatePaymentMethod( + Provider provider, + TokenizedPaymentSource tokenizedPaymentSource, + TaxInformation taxInformation); + Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 72224319fb..9cbf6b788a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -175,6 +175,7 @@ public static class FeatureFlagKeys public const string WebPush = "web-push"; public const string AndroidImportLoginsFlow = "import-logins-flow"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; + public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; public static List GetAllKeys() { From 488a9847ea15d977d28582bc44da110758e46ad1 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 14 Mar 2025 12:00:58 -0500 Subject: [PATCH 80/84] Partial for CommandResult (#5482) * Example of how a partial success/failure command result would look. * Fixed code. * Added Validator and ValidationResult * Moved errors into their own files. * Fixing tests * fixed import. * Forgot mock error. --- src/Api/Utilities/CommandResultExtensions.cs | 2 +- src/Core/AdminConsole/Errors/Error.cs | 3 + .../Errors/InsufficientPermissionsError.cs | 11 ++++ .../Errors/RecordNotFoundError.cs | 11 ++++ .../Shared/Validation/IValidator.cs | 6 ++ .../Shared/Validation/ValidationResult.cs | 15 +++++ src/Core/Models/Commands/CommandResult.cs | 27 ++++++--- .../AdminConsole/Shared/IValidatorTests.cs | 58 +++++++++++++++++++ .../Models/Commands/CommandResultTests.cs | 53 +++++++++++++++++ 9 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/Core/AdminConsole/Errors/Error.cs create mode 100644 src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs create mode 100644 src/Core/AdminConsole/Errors/RecordNotFoundError.cs create mode 100644 src/Core/AdminConsole/Shared/Validation/IValidator.cs create mode 100644 src/Core/AdminConsole/Shared/Validation/ValidationResult.cs create mode 100644 test/Core.Test/AdminConsole/Shared/IValidatorTests.cs create mode 100644 test/Core.Test/Models/Commands/CommandResultTests.cs diff --git a/src/Api/Utilities/CommandResultExtensions.cs b/src/Api/Utilities/CommandResultExtensions.cs index 39104db7ff..c7315a0fa0 100644 --- a/src/Api/Utilities/CommandResultExtensions.cs +++ b/src/Api/Utilities/CommandResultExtensions.cs @@ -12,7 +12,7 @@ public static class CommandResultExtensions NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Success success => new ObjectResult(success.Data) { StatusCode = StatusCodes.Status200OK }, + Success success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK }, _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") }; } diff --git a/src/Core/AdminConsole/Errors/Error.cs b/src/Core/AdminConsole/Errors/Error.cs new file mode 100644 index 0000000000..6c8eed41a4 --- /dev/null +++ b/src/Core/AdminConsole/Errors/Error.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.Errors; + +public record Error(string Message, T ErroredValue); diff --git a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs b/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs new file mode 100644 index 0000000000..d04ceba7c9 --- /dev/null +++ b/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.AdminConsole.Errors; + +public record InsufficientPermissionsError(string Message, T ErroredValue) : Error(Message, ErroredValue) +{ + public const string Code = "Insufficient Permissions"; + + public InsufficientPermissionsError(T ErroredValue) : this(Code, ErroredValue) + { + + } +} diff --git a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs b/src/Core/AdminConsole/Errors/RecordNotFoundError.cs new file mode 100644 index 0000000000..25a169efe1 --- /dev/null +++ b/src/Core/AdminConsole/Errors/RecordNotFoundError.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.AdminConsole.Errors; + +public record RecordNotFoundError(string Message, T ErroredValue) : Error(Message, ErroredValue) +{ + public const string Code = "Record Not Found"; + + public RecordNotFoundError(T ErroredValue) : this(Code, ErroredValue) + { + + } +} diff --git a/src/Core/AdminConsole/Shared/Validation/IValidator.cs b/src/Core/AdminConsole/Shared/Validation/IValidator.cs new file mode 100644 index 0000000000..d90386f00e --- /dev/null +++ b/src/Core/AdminConsole/Shared/Validation/IValidator.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.Shared.Validation; + +public interface IValidator +{ + public Task> ValidateAsync(T value); +} diff --git a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs new file mode 100644 index 0000000000..e25103e701 --- /dev/null +++ b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs @@ -0,0 +1,15 @@ +using Bit.Core.AdminConsole.Errors; + +namespace Bit.Core.AdminConsole.Shared.Validation; + +public abstract record ValidationResult; + +public record Valid : ValidationResult +{ + public T Value { get; init; } +} + +public record Invalid : ValidationResult +{ + public IEnumerable> Errors { get; init; } +} diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs index ae14b7d2f9..a8ec772fc1 100644 --- a/src/Core/Models/Commands/CommandResult.cs +++ b/src/Core/Models/Commands/CommandResult.cs @@ -1,5 +1,7 @@ #nullable enable +using Bit.Core.AdminConsole.Errors; + namespace Bit.Core.Models.Commands; public class CommandResult(IEnumerable errors) @@ -9,7 +11,6 @@ public class CommandResult(IEnumerable errors) public bool Success => ErrorMessages.Count == 0; public bool HasErrors => ErrorMessages.Count > 0; public List ErrorMessages { get; } = errors.ToList(); - public CommandResult() : this(Array.Empty()) { } } @@ -29,22 +30,30 @@ public class Success : CommandResult { } -public abstract class CommandResult -{ +public abstract class CommandResult; +public class Success(T value) : CommandResult +{ + public T Value { get; } = value; } -public class Success(T data) : CommandResult +public class Failure(IEnumerable errorMessages) : CommandResult { - public T? Data { get; init; } = data; + public List ErrorMessages { get; } = errorMessages.ToList(); + + public string ErrorMessage => string.Join(" ", ErrorMessages); + + public Failure(string error) : this([error]) { } } -public class Failure(IEnumerable errorMessage) : CommandResult +public class Partial : CommandResult { - public IEnumerable ErrorMessages { get; init; } = errorMessage; + public T[] Successes { get; set; } = []; + public Error[] Failures { get; set; } = []; - public Failure(string errorMessage) : this(new[] { errorMessage }) + public Partial(IEnumerable successfulItems, IEnumerable> failedItems) { + Successes = successfulItems.ToArray(); + Failures = failedItems.ToArray(); } } - diff --git a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs new file mode 100644 index 0000000000..abb49c25c6 --- /dev/null +++ b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs @@ -0,0 +1,58 @@ +using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Shared.Validation; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.Shared; + +public class IValidatorTests +{ + public class TestClass + { + public string Name { get; set; } = string.Empty; + } + + public record InvalidRequestError(T ErroredValue) : Error(Code, ErroredValue) + { + public const string Code = "InvalidRequest"; + } + + public class TestClassValidator : IValidator + { + public Task> ValidateAsync(TestClass value) + { + if (string.IsNullOrWhiteSpace(value.Name)) + { + return Task.FromResult>(new Invalid + { + Errors = [new InvalidRequestError(value)] + }); + } + + return Task.FromResult>(new Valid { Value = value }); + } + } + + [Fact] + public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError() + { + var example = new TestClass(); + + var result = await new TestClassValidator().ValidateAsync(example); + + Assert.IsType>(result); + var invalidResult = result as Invalid; + Assert.Equal(InvalidRequestError.Code, invalidResult.Errors.First().Message); + } + + [Fact] + public async Task ValidateAsync_WhenIsValid_ReturnsValid() + { + var example = new TestClass { Name = "Valid" }; + + var result = await new TestClassValidator().ValidateAsync(example); + + Assert.IsType>(result); + var validResult = result as Valid; + Assert.Equal(example.Name, validResult.Value.Name); + } +} diff --git a/test/Core.Test/Models/Commands/CommandResultTests.cs b/test/Core.Test/Models/Commands/CommandResultTests.cs new file mode 100644 index 0000000000..c500fef4f5 --- /dev/null +++ b/test/Core.Test/Models/Commands/CommandResultTests.cs @@ -0,0 +1,53 @@ +using Bit.Core.AdminConsole.Errors; +using Bit.Core.Models.Commands; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Models.Commands; + +public class CommandResultTests +{ + public class TestItem + { + public Guid Id { get; set; } + public string Value { get; set; } + } + + public CommandResult BulkAction(IEnumerable items) + { + var itemList = items.ToList(); + var successfulItems = items.Where(x => x.Value == "SuccessfulRequest").ToArray(); + + var failedItems = itemList.Except(successfulItems).ToArray(); + + var notFound = failedItems.First(x => x.Value == "Failed due to not found"); + var invalidPermissions = failedItems.First(x => x.Value == "Failed due to invalid permissions"); + + var notFoundError = new RecordNotFoundError(notFound); + var insufficientPermissionsError = new InsufficientPermissionsError(invalidPermissions); + + return new Partial(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]); + } + + [Theory] + [BitAutoData] + public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2) + { + var listOfRecords = new List + { + new TestItem() { Id = successId1, Value = "SuccessfulRequest" }, + new TestItem() { Id = failureId1, Value = "Failed due to not found" }, + new TestItem() { Id = failureId2, Value = "Failed due to invalid permissions" } + }; + + var result = BulkAction(listOfRecords); + + Assert.IsType>(result); + + var failures = (result as Partial).Failures.ToArray(); + var success = (result as Partial).Successes.First(); + + Assert.Equal(listOfRecords.First(), success); + Assert.Equal(2, failures.Length); + } +} From 27606e2d334f03d59571c5eb6b4c123e40f9071a Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:22:22 -0400 Subject: [PATCH 81/84] [PM-3553] Feature flag: Mobile SimpleLogin self host alias generation (#5392) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9cbf6b788a..e09422871d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -176,6 +176,7 @@ public static class FeatureFlagKeys public const string AndroidImportLoginsFlow = "import-logins-flow"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; + public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias"; public static List GetAllKeys() { From abfdf6f5cb0f1f1504dbaaaa0e04ce9cb60faf19 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:37:34 -0400 Subject: [PATCH 82/84] Revert "[PM-18944] Update error response from invalid OTP" (#5504) * Revert "[PM-18944] Update error response from invalid OTP (#5485)" This reverts commit 1b90bfe2a114e73b583100e955e57ac48b9733b9. --- .../RequestValidators/DeviceValidator.cs | 13 +++++++++---- .../IdentityServer/DeviceValidatorTests.cs | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 4744f4aca3..36a08326ab 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -250,21 +250,26 @@ public class DeviceValidator( var customResponse = new Dictionary(); switch (errorType) { + /* + * The ErrorMessage is brittle and is used to control the flow in the clients. Do not change them without updating the client as well. + * There is a backwards compatibility issue as well: if you make a change on the clients then ensure that they are backwards + * compatible. + */ case DeviceValidationResultType.InvalidUser: result.ErrorDescription = "Invalid user"; - customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid user.")); + customResponse.Add("ErrorModel", new ErrorResponseModel("invalid user")); break; case DeviceValidationResultType.InvalidNewDeviceOtp: result.ErrorDescription = "Invalid New Device OTP"; - customResponse.Add("ErrorModel", new ErrorResponseModel("Invalid new device OTP. Try again.")); + customResponse.Add("ErrorModel", new ErrorResponseModel("invalid new device otp")); break; case DeviceValidationResultType.NewDeviceVerificationRequired: result.ErrorDescription = "New device verification required"; - customResponse.Add("ErrorModel", new ErrorResponseModel("New device verification required.")); + customResponse.Add("ErrorModel", new ErrorResponseModel("new device verification required")); break; case DeviceValidationResultType.NoDeviceInformationProvided: result.ErrorDescription = "No device information provided"; - customResponse.Add("ErrorModel", new ErrorResponseModel("No device information provided.")); + customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided")); break; } return (result, customResponse); diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index bcb357d640..b71dd6c230 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -172,7 +172,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); - var expectedErrorModel = new ErrorResponseModel("No device information provided."); + var expectedErrorModel = new ErrorResponseModel("no device information provided"); var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorModel.Message, actualResponse.Message); } @@ -418,7 +418,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); // PM-13340: The error message should be "invalid user" instead of "no device information provided" - var expectedErrorMessage = "No device information provided."; + var expectedErrorMessage = "no device information provided"; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); } @@ -552,7 +552,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); - var expectedErrorMessage = "Invalid new device OTP. Try again."; + var expectedErrorMessage = "invalid new device otp"; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); } @@ -604,7 +604,7 @@ public class DeviceValidatorTests Assert.False(result); Assert.NotNull(context.CustomResponse["ErrorModel"]); - var expectedErrorMessage = "New device verification required."; + var expectedErrorMessage = "new device verification required"; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); } From d3f8a99fa6d40ea3346560eb7a5259617ed536e4 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 17 Mar 2025 16:20:51 -0400 Subject: [PATCH 83/84] [PM-18175] Remove flag check for 2FA recovery code login (#5513) * Remove server-side flagging * Linting * Linting. --- .../RequestValidators/TwoFactorAuthenticationValidator.cs | 8 ++------ .../TwoFactorAuthenticationValidatorTests.cs | 5 +---- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 856846cdd6..e733d4f410 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; @@ -155,12 +154,9 @@ public class TwoFactorAuthenticationValidator( return false; } - if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin)) + if (type is TwoFactorProviderType.RecoveryCode) { - if (type is TwoFactorProviderType.RecoveryCode) - { - return await _userService.RecoverTwoFactorAsync(user, token); - } + return await _userService.RecoverTwoFactorAsync(user, token); } // These cases we want to always return false, U2f is deprecated and OrganizationDuo diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index e59a66a9e7..fb4d7c321a 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; @@ -464,7 +463,6 @@ public class TwoFactorAuthenticationValidatorTests user.TwoFactorRecoveryCode = token; _userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(true); - _featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true); // Act var result = await _sut.VerifyTwoFactorAsync( @@ -486,7 +484,6 @@ public class TwoFactorAuthenticationValidatorTests user.TwoFactorRecoveryCode = token; _userService.RecoverTwoFactorAsync(Arg.Is(user), Arg.Is(token)).Returns(false); - _featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeLogin).Returns(true); // Act var result = await _sut.VerifyTwoFactorAsync( From 43d0f1052b4b0aeca3f57d729a42b6a4f84b9aa9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:04:54 +0100 Subject: [PATCH 84/84] [deps] Tools: Update MailKit to 4.11.0 (#5515) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 8a8de3d77d..2a3edcdc00 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -36,7 +36,7 @@ - +