diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 973405dea5..5399bed391 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -43,8 +43,16 @@ src/Core/IdentityServer @bitwarden/team-auth-dev # Key Management team **/KeyManagement @bitwarden/team-key-management-dev +# Tools team **/Tools @bitwarden/team-tools-dev +# Dirt (Data Insights & Reporting) team +src/Api/Controllers/Dirt @bitwarden/team-data-insights-and-reporting-dev +src/Core/Dirt @bitwarden/team-data-insights-and-reporting-dev +src/Infrastructure.Dapper/Dirt @bitwarden/team-data-insights-and-reporting-dev +test/Api.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev +test/Core.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev + # Vault team **/Vault @bitwarden/team-vault-dev **/Vault/AuthorizationHandlers @bitwarden/team-vault-dev @bitwarden/team-admin-console-dev # joint ownership over authorization handlers that affect organization users @@ -82,6 +90,9 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev .github/workflows/test-database.yml @bitwarden/team-platform-dev .github/workflows/test.yml @bitwarden/team-platform-dev **/*Platform* @bitwarden/team-platform-dev +**/.dockerignore @bitwarden/team-platform-dev +**/Dockerfile @bitwarden/team-platform-dev +**/entrypoint.sh @bitwarden/team-platform-dev # Multiple owners - DO NOT REMOVE (BRE) **/packages.lock.json diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 344a326519..5c1b259539 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -15,12 +15,11 @@ matchManagers: ["github-actions"], matchFileNames: [ ".github/workflows/publish.yml", - ".github/workflows/release.yml", - ".github/workflows/repository-management.yml" + ".github/workflows/release.yml" ], commitMessagePrefix: "[deps] BRE:", reviewers: ["team:dept-bre"], - addLabels: ["hold"] + addLabels: ["hold"], }, { groupName: "dockerfile minor", @@ -37,6 +36,16 @@ matchManagers: ["github-actions"], matchUpdateTypes: ["minor"], }, + { + // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates. + // This overrides the default that ignores patch updates for nuget dependencies. + matchPackageNames: [ + "/^Microsoft\\.Extensions\\./", + "/^Microsoft\\.AspNetCore\\./", + ], + matchUpdateTypes: ["patch"], + dependencyDashboardApproval: false, + }, { matchManagers: ["dockerfile", "docker-compose"], commitMessagePrefix: "[deps] BRE:", @@ -59,6 +68,7 @@ "DuoUniversal", "Fido2.AspNet", "Duende.IdentityServer", + "Microsoft.AspNetCore.Authentication.JwtBearer", "Microsoft.Extensions.Identity.Stores", "Otp.NET", "Sustainsys.Saml2.AspNetCore2", @@ -79,8 +89,6 @@ "CsvHelper", "Kralizek.AutoFixture.Extensions.MockHttp", "Microsoft.AspNetCore.Mvc.Testing", - "Microsoft.Extensions.Logging", - "Microsoft.Extensions.Logging.Console", "Newtonsoft.Json", "NSubstitute", "Sentry.Serilog", @@ -100,9 +108,9 @@ 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: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"], + groupName: "EntityFrameworkCore", + description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", }, { matchPackageNames: [ @@ -117,9 +125,6 @@ "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", ], @@ -128,8 +133,8 @@ reviewers: ["team:dept-dbops"], }, { - matchPackageNames: ["CommandDotNet", "YamlDotNet"], - description: "DevOps owned dependencies", + matchPackageNames: ["YamlDotNet"], + description: "BRE owned dependencies", commitMessagePrefix: "[deps] BRE:", reviewers: ["team:dept-bre"], }, @@ -142,56 +147,40 @@ "Azure.Messaging.ServiceBus", "Azure.Storage.Blobs", "Azure.Storage.Queues", - "Microsoft.AspNetCore.Authentication.JwtBearer", + "LaunchDarkly.ServerSdk", "Microsoft.AspNetCore.Http", + "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Console", + "Microsoft.Extensions.Caching.Cosmos", + "Microsoft.Extensions.Caching.SqlServer", + "Microsoft.Extensions.Caching.StackExchangeRedis", "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", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33edd075a0..19eea71b6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,7 @@ on: env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" + _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} jobs: lint: @@ -234,12 +235,18 @@ jobs: - name: Generate Docker image tag id: tag run: | - if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then + IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") fi + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only + IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags + fi + if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi @@ -629,7 +636,9 @@ jobs: setup-ephemeral-environment: name: Setup Ephemeral Environment - needs: build-docker + needs: + - build-artifacts + - build-docker if: | needs.build-artifacts.outputs.has_secrets == 'true' && github.event_name == 'pull_request' diff --git a/.github/workflows/build_target.yml b/.github/workflows/build_target.yml index 313446c949..d825721a7d 100644 --- a/.github/workflows/build_target.yml +++ b/.github/workflows/build_target.yml @@ -2,7 +2,9 @@ name: Build on PR Target on: pull_request_target: - types: [opened, synchronize] + types: [opened, synchronize, reopened] + branches: + - "main" defaults: run: diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index ce8cb8e467..359e64eb57 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -1,7 +1,10 @@ name: Collect code references -on: - pull_request: +on: + push: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: check-ld-secret: @@ -37,12 +40,11 @@ jobs: - name: Collect id: collect - uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0 + uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0 with: - project-key: default - environment-key: dev - access-token: ${{ secrets.LD_ACCESS_TOKEN }} - repo-token: ${{ secrets.GITHUB_TOKEN }} + accessToken: ${{ secrets.LD_ACCESS_TOKEN }} + projKey: default + allowTags: true - name: Add label if: steps.collect.outputs.any-changed == 'true' diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index fe88782e35..f24a0973fd 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -7,8 +7,14 @@ on: - "main" - "rc" - "hotfix-rc" + pull_request: + types: [opened, synchronize, reopened] + branches-ignore: + - main pull_request_target: - types: [opened, synchronize] + types: [opened, synchronize, reopened] + branches: + - "main" jobs: check-run: diff --git a/Directory.Build.props b/Directory.Build.props index 16d8d83ae0..f403c0f692 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.4.3 + 2025.5.2 Bit.$(MSBuildProjectName) enable diff --git a/README.md b/README.md index 73992785d7..c817931c67 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,6 @@ Github Workflow build on main - - DockerHub - gitter chat @@ -26,12 +23,12 @@ Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/gett ## Deploy

- + docker

-You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [Docker Hub](https://hub.docker.com/u/bitwarden/). +You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [GitHub Container Registry](https://github.com/orgs/bitwarden/packages). Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/ diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 892d2f4255..2ec8d86e0e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -325,6 +329,14 @@ Global {3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -377,6 +389,8 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs index 36a5f2c0a9..8dee75c7c2 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs @@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 9a62be8dd5..4af0e12e64 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -7,13 +7,12 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.DependencyInjection; using Stripe; namespace Bit.Commercial.Core.AdminConsole.Providers; @@ -23,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly IEventService _eventService; private readonly IMailService _mailService; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationService _organizationService; private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IStripeAdapter _stripeAdapter; private readonly IFeatureService _featureService; @@ -31,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly ISubscriberService _subscriberService; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IPricingClient _pricingClient; - private readonly IAutomaticTaxStrategy _automaticTaxStrategy; public RemoveOrganizationFromProviderCommand( IEventService eventService, IMailService mailService, IOrganizationRepository organizationRepository, - IOrganizationService organizationService, IProviderOrganizationRepository providerOrganizationRepository, IStripeAdapter stripeAdapter, IFeatureService featureService, IProviderBillingService providerBillingService, ISubscriberService subscriberService, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IPricingClient pricingClient, - [FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy) + IPricingClient pricingClient) { _eventService = eventService; _mailService = mailService; _organizationRepository = organizationRepository; - _organizationService = organizationService; _providerOrganizationRepository = providerOrganizationRepository; _stripeAdapter = stripeAdapter; _featureService = featureService; @@ -58,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv _subscriberService = subscriberService; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _pricingClient = pricingClient; - _automaticTaxStrategy = automaticTaxStrategy; } public async Task RemoveOrganizationFromProvider( @@ -76,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, - Array.Empty(), + [], includeProvider: false)) { throw new BadRequestException("Organization must have at least one confirmed owner."); @@ -101,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv /// /// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled /// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because - /// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly, + /// the provider's payment method will be removed from their Stripe customer, causing ensuing charges to fail. Lastly, /// we email the organization owners letting them know they need to add a new payment method. /// private async Task ResetOrganizationBillingAsync( @@ -141,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] }; - if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { - _automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else + else if (customer.HasRecognizedTaxLocation()) { - subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true + Enabled = customer.Address.Country == "US" || + customer.TaxIds.Any() }; } @@ -186,7 +182,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv await _mailService.SendProviderUpdatePaymentMethod( organization.Id, organization.Name, - provider.Name, + provider.Name!, organizationOwnerEmails); } } diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index fff6b5271d..ad2d2d2aa1 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -5,11 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -52,6 +54,7 @@ public class ProviderService : IProviderService private readonly IApplicationCacheService _applicationCacheService; private readonly IProviderBillingService _providerBillingService; private readonly IPricingClient _pricingClient; + private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand; public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, @@ -60,7 +63,8 @@ public class ProviderService : IProviderService IOrganizationRepository organizationRepository, GlobalSettings globalSettings, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, IDataProtectorTokenFactory providerDeleteTokenDataFactory, - IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient) + IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient, + IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand) { _providerRepository = providerRepository; _providerUserRepository = providerUserRepository; @@ -80,9 +84,10 @@ public class ProviderService : IProviderService _applicationCacheService = applicationCacheService; _providerBillingService = providerBillingService; _pricingClient = pricingClient; + _providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand; } - public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) + public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) { var owner = await _userService.GetUserByIdAsync(ownerUserId); if (owner == null) @@ -111,7 +116,20 @@ public class ProviderService : IProviderService { throw new BadRequestException("Both address and postal code are required to set up your provider."); } - var customer = await _providerBillingService.SetupCustomer(provider, taxInfo); + + var requireProviderPaymentMethodDuringSetup = + _featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); + + if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not + { + Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, + Token: not null and not "" + }) + { + throw new BadRequestException("A payment method is required to set up your provider."); + } + + var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); provider.GatewayCustomerId = customer.Id; var subscription = await _providerBillingService.SetupSubscription(provider); provider.GatewaySubscriptionId = subscription.Id; @@ -546,12 +564,12 @@ public class ProviderService : IProviderService ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan); - var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup); + var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup); var providerOrganization = new ProviderOrganization { ProviderId = providerId, - OrganizationId = organization.Id, + OrganizationId = signUpResponse.Organization.Id, Key = organizationSignup.OwnerKey, }; @@ -560,12 +578,12 @@ public class ProviderService : IProviderService // Give the owner Can Manage access over the default collection // The orgUser is not available when the org is created so we have to do it here as part of the invite - var defaultOwnerAccess = defaultCollection != null + var defaultOwnerAccess = signUpResponse.DefaultCollection != null ? [ new CollectionAccessSelection { - Id = defaultCollection.Id, + Id = signUpResponse.DefaultCollection.Id, HidePasswords = false, ReadOnly = false, Manage = true @@ -573,7 +591,7 @@ public class ProviderService : IProviderService ] : Array.Empty(); - await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null, + await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null, new (OrganizationUserInvite, string)[] { ( diff --git a/bitwarden_license/src/Commercial.Core/Billing/Models/ProviderClientInvoiceReportRow.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs similarity index 91% rename from bitwarden_license/src/Commercial.Core/Billing/Models/ProviderClientInvoiceReportRow.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs index c78e213c34..eea40577ad 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Models/ProviderClientInvoiceReportRow.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs @@ -1,8 +1,8 @@ using System.Globalization; -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using CsvHelper.Configuration.Attributes; -namespace Bit.Commercial.Core.Billing.Models; +namespace Bit.Commercial.Core.Billing.Providers.Models; public class ProviderClientInvoiceReportRow { diff --git a/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs similarity index 98% rename from bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs index 97d9377cd6..8f6eb07fe1 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs @@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging; using OneOf; using Stripe; -namespace Bit.Commercial.Core.Billing; +namespace Bit.Commercial.Core.Billing.Providers.Services; [RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)] public class BusinessUnitConverter( @@ -67,6 +68,7 @@ public class BusinessUnitConverter( organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb; organization.UsePolicies = updatedPlan.HasPolicies; organization.UseSso = updatedPlan.HasSso; + organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains; organization.UseGroups = updatedPlan.HasGroups; organization.UseEvents = updatedPlan.HasEvents; organization.UseDirectory = updatedPlan.HasDirectory; diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs similarity index 80% rename from bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs index bdfff079cf..8c90d778bc 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -1,34 +1,42 @@ using System.Globalization; -using Bit.Commercial.Core.Billing.Models; +using Bit.Commercial.Core.Billing.Providers.Models; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Braintree; using CsvHelper; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Stripe; +using static Bit.Core.Billing.Utilities; +using Customer = Stripe.Customer; +using Subscription = Stripe.Subscription; -namespace Bit.Commercial.Core.Billing; +namespace Bit.Commercial.Core.Billing.Providers.Services; public class ProviderBillingService( + IBraintreeGateway braintreeGateway, IEventService eventService, IFeatureService featureService, IGlobalSettings globalSettings, @@ -39,10 +47,10 @@ public class ProviderBillingService( IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, IProviderUserRepository providerUserRepository, + ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - ITaxService taxService, - [FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy) + ITaxService taxService) : IProviderBillingService { public async Task AddExistingOrganization( @@ -88,6 +96,7 @@ public class ProviderBillingService( organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.UsePolicies = plan.HasPolicies; organization.UseSso = plan.HasSso; + organization.UseOrganizationDomains = plan.HasOrganizationDomains; organization.UseGroups = plan.HasGroups; organization.UseEvents = plan.HasEvents; organization.UseDirectory = plan.HasDirectory; @@ -116,7 +125,7 @@ public class ProviderBillingService( /* * 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. + * row is inserted so the added organization's seats don't get double-counted. */ await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value); @@ -224,7 +233,7 @@ public class ProviderBillingService( var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions { - Expand = ["tax_ids"] + Expand = ["tax", "tax_ids"] }); var providerTaxId = providerCustomer.TaxIds.FirstOrDefault(); @@ -272,6 +281,13 @@ public class ProviderBillingService( ] }; + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" }) + { + customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; + } + var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions); organization.GatewayCustomerId = customer.Id; @@ -463,7 +479,8 @@ public class ProviderBillingService( public async Task SetupCustomer( Provider provider, - TaxInfo taxInfo) + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource = null) { if (taxInfo is not { @@ -507,6 +524,13 @@ public class ProviderBillingService( } }; + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US") + { + options.TaxExempt = StripeConstants.TaxExempt.Reverse; + } + if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber)) { var taxIdType = taxService.GetStripeTaxCode( @@ -518,6 +542,7 @@ public class ProviderBillingService( logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); + throw new BadRequestException("billingTaxIdTypeInferenceError"); } @@ -532,13 +557,97 @@ public class ProviderBillingService( options.Coupon = provider.DiscountId; } + var requireProviderPaymentMethodDuringSetup = + featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); + + var braintreeCustomerId = ""; + + if (requireProviderPaymentMethodDuringSetup) + { + if (tokenizedPaymentSource is not + { + Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, + Token: not null and not "" + }) + { + logger.LogError("Cannot create customer for provider ({ProviderID}) without a payment method", provider.Id); + throw new BillingException(); + } + + var (type, token) = tokenizedPaymentSource; + + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (type) + { + case PaymentMethodType.BankAccount: + { + var setupIntent = + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token })) + .FirstOrDefault(); + + if (setupIntent == null) + { + logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id); + throw new BillingException(); + } + + await setupIntentCache.Set(provider.Id, setupIntent.Id); + break; + } + case PaymentMethodType.Card: + { + options.PaymentMethod = token; + options.InvoiceSettings.DefaultPaymentMethod = token; + break; + } + case PaymentMethodType.PayPal: + { + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token); + options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId; + break; + } + } + } + try { return await stripeAdapter.CustomerCreateAsync(options); } - catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.TaxIdInvalid) + catch (StripeException stripeException) when (stripeException.StripeError?.Code == + StripeConstants.ErrorCodes.TaxIdInvalid) { - throw new BadRequestException("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid."); + await Revert(); + throw new BadRequestException( + "Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid."); + } + catch + { + await Revert(); + throw; + } + + async Task Revert() + { + if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null) + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (tokenizedPaymentSource.Type) + { + case PaymentMethodType.BankAccount: + { + var setupIntentId = await setupIntentCache.Get(provider.Id); + await stripeAdapter.SetupIntentCancel(setupIntentId, + new SetupIntentCancelOptions { CancellationReason = "abandoned" }); + await setupIntentCache.Remove(provider.Id); + break; + } + case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + { + await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); + break; + } + } + } } } @@ -580,34 +689,71 @@ public class ProviderBillingService( }); } + var requireProviderPaymentMethodDuringSetup = + featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); + + var setupIntentId = await setupIntentCache.Get(provider.Id); + + var setupIntent = !string.IsNullOrEmpty(setupIntentId) + ? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions + { + Expand = ["payment_method"] + }) + : null; + + var usePaymentMethod = + requireProviderPaymentMethodDuringSetup && + (!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || + customer.Metadata.ContainsKey(BraintreeCustomerIdKey) || + setupIntent.IsUnverifiedBankAccount()); + + int? trialPeriodDays = provider.Type switch + { + ProviderType.Msp when usePaymentMethod => 14, + ProviderType.BusinessUnit when usePaymentMethod => 4, + _ => null + }; + var subscriptionCreateOptions = new SubscriptionCreateOptions { - CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + CollectionMethod = usePaymentMethod ? + StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice, Customer = customer.Id, - DaysUntilDue = 30, + DaysUntilDue = usePaymentMethod ? null : 30, Items = subscriptionItemOptionsList, Metadata = new Dictionary { { "providerId", provider.Id.ToString() } }, OffSession = true, - ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations + ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, + TrialPeriodDays = trialPeriodDays }; - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); - } - else + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } + else if (customer.HasRecognizedTaxLocation()) + { + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = customer.Address.Country == "US" || + customer.TaxIds.Any() + }; + } try { var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); - if (subscription.Status == StripeConstants.SubscriptionStatus.Active) + if (subscription is + { + Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing + }) { return subscription; } diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderPriceAdapter.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs similarity index 99% rename from bitwarden_license/src/Commercial.Core/Billing/ProviderPriceAdapter.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs index a9dbb6febf..8c55d31f2c 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderPriceAdapter.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs @@ -6,7 +6,7 @@ using Bit.Core.Billing; using Bit.Core.Billing.Enums; using Stripe; -namespace Bit.Commercial.Core.Billing; +namespace Bit.Commercial.Core.Billing.Providers.Services; public static class ProviderPriceAdapter { 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 d9a7d4a2ce..7e8857e5d7 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs @@ -1,9 +1,10 @@ using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Repositories; -using Bit.Core.Utilities; +using Bit.Core.Settings; namespace Bit.Commercial.Core.SecretsManager.Queries.Projects; @@ -11,36 +12,43 @@ public class MaxProjectsQuery : IMaxProjectsQuery { private readonly IOrganizationRepository _organizationRepository; private readonly IProjectRepository _projectRepository; + private readonly IGlobalSettings _globalSettings; + private readonly IPricingClient _pricingClient; public MaxProjectsQuery( IOrganizationRepository organizationRepository, - IProjectRepository projectRepository) + IProjectRepository projectRepository, + IGlobalSettings globalSettings, + IPricingClient pricingClient) { _organizationRepository = organizationRepository; _projectRepository = projectRepository; + _globalSettings = globalSettings; + _pricingClient = pricingClient; } public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd) { + // "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted. + if (_globalSettings.SelfHosted) + { + return (null, null); + } + var org = await _organizationRepository.GetByIdAsync(organizationId); if (org == null) { throw new NotFoundException(); } - // TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122 - var plan = StaticStore.GetPlan(org.PlanType); - if (plan?.SecretsManager == null) + var plan = await _pricingClient.GetPlan(org.PlanType); + + if (plan is not { SecretsManager: not null, Type: PlanType.Free }) { - throw new BadRequestException("Existing plan not found."); + return (null, null); } - if (plan.Type == PlanType.Free) - { - var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId); - return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false)); - } - - return (null, null); + var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId); + return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false)); } } diff --git a/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs index 7f8c82e2c9..34f49e0ccc 100644 --- a/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs @@ -1,9 +1,9 @@ using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Services; -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Services; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Microsoft.Extensions.DependencyInjection; namespace Bit.Commercial.Core.Utilities; diff --git a/bitwarden_license/src/Scim/Users/PostUserCommand.cs b/bitwarden_license/src/Scim/Users/PostUserCommand.cs index 46116a46ae..5b4a0c29cd 100644 --- a/bitwarden_license/src/Scim/Users/PostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PostUserCommand.cs @@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -76,9 +76,8 @@ public class PostUserCommand( var invitedOrganizationUserId = result switch { Success success => success.Value.InvitedUser.Id, - Failure failure when failure.Errors - .Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null, - Failure failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors), + Failure { Error.Message: NoUsersToInviteError.Code } => (Guid?)null, + Failure failure => throw MapToBitException(failure.Error), _ => throw new InvalidOperationException() }; diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 0b861365bc..636d6317a1 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -9,17 +9,17 @@ "version": "0.0.0", "license": "-", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1" }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", - "sass-loader": "16.0.4", - "webpack": "5.97.1", + "sass": "1.88.0", + "sass-loader": "16.0.5", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } }, @@ -455,13 +455,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@webassemblyjs/ast": { @@ -748,9 +748,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", "funding": [ { "type": "github", @@ -781,9 +781,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -801,10 +801,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -821,9 +821,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -975,9 +975,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.128", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", - "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", "dev": true, "license": "ISC" }, @@ -1009,9 +1009,9 @@ } }, "node_modules/es-module-lexer": { - "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==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -1083,9 +1083,9 @@ } }, "node_modules/expose-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", - "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz", + "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==", "dev": true, "license": "MIT", "engines": { @@ -1106,13 +1106,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -1248,9 +1241,9 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", "dev": true, "license": "MIT" }, @@ -1754,16 +1747,6 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1877,9 +1860,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", + "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", "dev": true, "license": "MIT", "dependencies": { @@ -1898,9 +1881,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", - "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", "dev": true, "license": "MIT", "dependencies": { @@ -1939,9 +1922,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1959,9 +1942,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -2078,9 +2061,9 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -2088,14 +2071,14 @@ } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2156,9 +2139,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -2193,16 +2176,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2211,9 +2184,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", "dependencies": { @@ -2225,14 +2198,15 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -2249,9 +2223,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -2352,59 +2326,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index d9aefafef3..137f86680c 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -8,17 +8,17 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1" }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", - "sass-loader": "16.0.4", - "webpack": "5.97.1", + "sass": "1.88.0", + "sass-loader": "16.0.5", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } } 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 48eda094e8..5be18116c0 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -1,4 +1,5 @@ using Bit.Commercial.Core.AdminConsole.Providers; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -223,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Description == string.Empty && + options.Email == organization.BillingEmail && + options.Expand[0] == "tax" && + options.Expand[1] == "tax_ids")).Returns(new Customer + { + Id = "customer_id", + Address = new Address + { + Country = "US" + } + }); + stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(new Subscription { Id = "subscription_id" }); - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == organization.GatewayCustomerId && - options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && - options.DaysUntilDue == 30 && - options.Metadata["organizationId"] == organization.Id.ToString() && - options.OffSession == true && - options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && - options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId && - options.Items.First().Quantity == organization.Seats) - , Arg.Any())) - .Do(x => + await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); + + await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is(options => + options.Customer == organization.GatewayCustomerId && + options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && + options.DaysUntilDue == 30 && + options.AutomaticTax.Enabled == true && + options.Metadata["organizationId"] == organization.Id.ToString() && + options.OffSession == true && + options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId && + options.Items.First().Quantity == organization.Seats)); + + await sutProvider.GetDependency().Received(1) + .ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0); + + await organizationRepository.Received(1).ReplaceAsync(Arg.Is( + org => + org.BillingEmail == "a@example.com" && + org.GatewaySubscriptionId == "subscription_id" && + org.Status == OrganizationStatusType.Created)); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(providerOrganization); + + await sutProvider.GetDependency().Received(1) + .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed); + + await sutProvider.GetDependency().Received(1) + .SendProviderUpdatePaymentMethod( + organization.Id, + organization.Name, + provider.Name, + Arg.Is>(emails => emails.FirstOrDefault() == "a@example.com")); + } + + [Theory, BitAutoData] + public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations( + Provider provider, + ProviderOrganization providerOrganization, + Organization organization, + SutProvider sutProvider) + { + provider.Status = ProviderStatusType.Billable; + + providerOrganization.ProviderId = provider.Id; + + organization.Status = OrganizationStatusType.Managed; + + organization.PlanType = PlanType.TeamsMonthly; + + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + + sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); + + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + providerOrganization.OrganizationId, + [], + includeProvider: false) + .Returns(true); + + var organizationRepository = sutProvider.GetDependency(); + + organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([ + "a@example.com", + "b@example.com" + ]); + + var stripeAdapter = sutProvider.GetDependency(); + + stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Description == string.Empty && + options.Email == organization.BillingEmail && + options.Expand[0] == "tax" && + options.Expand[1] == "tax_ids")).Returns(new Customer { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions + Id = "customer_id", + Address = new Address { - Enabled = true - }; + Country = "US" + } }); + stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(new Subscription + { + Id = "subscription_id" + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is(options => 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 d2d82f47de..cb8a9e8c69 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -1,14 +1,17 @@ using Bit.Commercial.Core.AdminConsole.Services; using Bit.Commercial.Core.Test.AdminConsole.AutoFixture; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -38,7 +41,7 @@ public class ProviderServiceTests public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider sutProvider) { var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default)); + () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null)); Assert.Contains("Invalid owner.", exception.Message); } @@ -50,12 +53,85 @@ public class ProviderServiceTests userService.GetUserByIdAsync(user.Id).Returns(user); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default)); + () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null)); Assert.Contains("Invalid token.", exception.Message); } [Theory, BitAutoData] - public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, + public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException( + User user, + Provider provider, + string key, + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource, + [ProviderUser] ProviderUser providerUser, + SutProvider sutProvider) + { + providerUser.ProviderId = provider.Id; + providerUser.UserId = user.Id; + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(user.Id).Returns(user); + + var providerUserRepository = sutProvider.GetDependency(); + providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); + + var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); + var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); + sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") + .Returns(protector); + + sutProvider.Create(); + + var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); + + taxInfo.BillingAddressCountry = null; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource)); + + Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message); + } + + [Theory, BitAutoData] + public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException( + User user, + Provider provider, + string key, + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource, + [ProviderUser] ProviderUser providerUser, + SutProvider sutProvider) + { + providerUser.ProviderId = provider.Id; + providerUser.UserId = user.Id; + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(user.Id).Returns(user); + + var providerUserRepository = sutProvider.GetDependency(); + providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); + + var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); + var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); + sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") + .Returns(protector); + + sutProvider.Create(); + + var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource)); + + Assert.Equal("A payment method is required to set up your provider.", exception.Message); + } + + [Theory, BitAutoData] + public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource, [ProviderUser] ProviderUser providerUser, SutProvider sutProvider) { @@ -75,7 +151,7 @@ public class ProviderServiceTests var providerBillingService = sutProvider.GetDependency(); var customer = new Customer { Id = "customer_id" }; - providerBillingService.SetupCustomer(provider, taxInfo).Returns(customer); + providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer); var subscription = new Subscription { Id = "subscription_id" }; providerBillingService.SetupSubscription(provider).Returns(subscription); @@ -84,7 +160,7 @@ public class ProviderServiceTests var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo); + await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource); await sutProvider.GetDependency().Received().UpsertAsync(Arg.Is( p => @@ -642,8 +718,8 @@ public class ProviderServiceTests sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, new Collection())); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection())); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); @@ -680,8 +756,8 @@ public class ProviderServiceTests var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, new Collection())); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection())); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user)); @@ -707,8 +783,8 @@ public class ProviderServiceTests var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, new Collection())); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection())); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); @@ -746,8 +822,8 @@ public class ProviderServiceTests sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, defaultCollection)); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection)); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/BusinessUnitConverterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs similarity index 98% rename from bitwarden_license/test/Commercial.Core.Test/Billing/BusinessUnitConverterTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs index 5d2d0a2c7c..c27d990213 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/BusinessUnitConverterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs @@ -1,16 +1,16 @@ #nullable enable using System.Text; -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -25,7 +25,7 @@ using NSubstitute; using Stripe; using Xunit; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Providers; public class BusinessUnitConverterTests { diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs similarity index 62% rename from bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs index 2661a0eff6..9af9a71cce 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs @@ -1,19 +1,23 @@ using System.Globalization; using System.Net; -using Bit.Commercial.Core.Billing; -using Bit.Commercial.Core.Billing.Models; +using Bit.Commercial.Core.Billing.Providers.Models; +using Bit.Commercial.Core.Billing.Providers.Services; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; 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.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -24,13 +28,19 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Braintree; using CsvHelper; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Stripe; using Xunit; using static Bit.Core.Test.Billing.Utilities; +using Address = Stripe.Address; +using Customer = Stripe.Customer; +using PaymentMethod = Stripe.PaymentMethod; +using Subscription = Stripe.Subscription; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Providers; [SutProviderCustomize] public class ProviderBillingServiceTests @@ -252,7 +262,7 @@ public class ProviderBillingServiceTests }; sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( - options => options.Expand.FirstOrDefault() == "tax_ids")) + options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids"))) .Returns(providerCustomer); sutProvider.GetDependency().BaseServiceUri @@ -302,6 +312,91 @@ public class ProviderBillingServiceTests org => org.GatewayCustomerId == "customer_id")); } + [Theory, BitAutoData] + public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds( + Provider provider, + Organization organization, + SutProvider sutProvider) + { + organization.GatewayCustomerId = null; + organization.Name = "Name"; + organization.BusinessName = "BusinessName"; + + var providerCustomer = new Customer + { + Address = new Address + { + Country = "CA", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Unit 4", + City = "Fake Town", + State = "Fake State" + }, + TaxIds = new StripeList + { + Data = + [ + new TaxId { Type = "TYPE", Value = "VALUE" } + ] + } + }; + + sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( + options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids"))) + .Returns(providerCustomer); + + sutProvider.GetDependency().BaseServiceUri + .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings()) + { + CloudRegion = "US" + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + sutProvider.GetDependency().CustomerCreateAsync(Arg.Is( + options => + options.Address.Country == providerCustomer.Address.Country && + options.Address.PostalCode == providerCustomer.Address.PostalCode && + options.Address.Line1 == providerCustomer.Address.Line1 && + options.Address.Line2 == providerCustomer.Address.Line2 && + options.Address.City == providerCustomer.Address.City && + options.Address.State == providerCustomer.Address.State && + options.Name == organization.DisplayName() && + options.Description == $"{provider.Name} Client Organization" && + options.Email == provider.BillingEmail && + options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && + options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && + options.Metadata["region"] == "US" && + options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && + options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value && + options.TaxExempt == StripeConstants.TaxExempt.Reverse)) + .Returns(new Customer { Id = "customer_id" }); + + await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); + + await sutProvider.GetDependency().Received(1).CustomerCreateAsync(Arg.Is( + options => + options.Address.Country == providerCustomer.Address.Country && + options.Address.PostalCode == providerCustomer.Address.PostalCode && + options.Address.Line1 == providerCustomer.Address.Line1 && + options.Address.Line2 == providerCustomer.Address.Line2 && + options.Address.City == providerCustomer.Address.City && + options.Address.State == providerCustomer.Address.State && + options.Name == organization.DisplayName() && + options.Description == $"{provider.Name} Client Organization" && + options.Email == provider.BillingEmail && + options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && + options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && + options.Metadata["region"] == "US" && + options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && + options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + org => org.GatewayCustomerId == "customer_id")); + } + #endregion #region GenerateClientInvoiceReport @@ -833,7 +928,7 @@ public class ProviderBillingServiceTests } [Theory, BitAutoData] - public async Task SetupCustomer_Success( + public async Task SetupCustomer_NoPaymentMethod_Success( SutProvider sutProvider, Provider provider, TaxInfo taxInfo) @@ -877,6 +972,357 @@ public class ProviderBillingServiceTests Assert.Equivalent(expected, actual); } + [Theory, BitAutoData] + public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; + + await ThrowsBillingExceptionAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithBankAccount_Error_Reverts( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ + new SetupIntent { Id = "setup_intent_id" } + ]); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Throws(); + + sutProvider.GetDependency().Get(provider.Id).Returns("setup_intent_id"); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + + await sutProvider.GetDependency().Received(1).Set(provider.Id, "setup_intent_id"); + + await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is(options => + options.CancellationReason == "abandoned")); + + await sutProvider.GetDependency().Received(1).Remove(provider.Id); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithPayPal_Error_Reverts( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) + .Returns("braintree_customer_id"); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.Metadata["btCustomerId"] == "braintree_customer_id" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Throws(); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + + await sutProvider.GetDependency().Customer.Received(1).DeleteAsync("braintree_customer_id"); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithBankAccount_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ + new SetupIntent { Id = "setup_intent_id" } + ]); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + + await sutProvider.GetDependency().Received(1).Set(provider.Id, "setup_intent_id"); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithPayPal_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) + .Returns("braintree_customer_id"); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.Metadata["btCustomerId"] == "braintree_customer_id" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithCard_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.PaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithCard_ReverseCharge_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.PaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber && + o.TaxExempt == StripeConstants.TaxExempt.Reverse)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + } + [Theory, BitAutoData] public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( SutProvider sutProvider, @@ -1002,7 +1448,7 @@ public class ProviderBillingServiceTests .Returns(new Customer { Id = "customer_id", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + Address = new Address { Country = "US" } }); var providerPlans = new List @@ -1044,7 +1490,7 @@ public class ProviderBillingServiceTests } [Theory, BitAutoData] - public async Task SetupSubscription_Succeeds( + public async Task SetupSubscription_SendInvoice_Succeeds( SutProvider sutProvider, Provider provider) { @@ -1054,7 +1500,7 @@ public class ProviderBillingServiceTests var customer = new Customer { Id = "customer_id", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + Address = new Address { Country = "US" } }; sutProvider.GetDependency() .GetCustomerOrThrow( @@ -1094,19 +1540,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == "customer_id") - , Arg.Is(p => p == customer))) - .Do(x => - { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }; - }); - sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( sub => sub.AutomaticTax.Enabled == true && @@ -1127,6 +1560,350 @@ public class ProviderBillingServiceTests Assert.Equivalent(expected, actual); } + [Theory, BitAutoData] + public async Task SetupSubscription_ChargeAutomatically_HasCard_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + Address = new Address { Country = "US" }, + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethodId = "pm_123" + } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupSubscription_ChargeAutomatically_HasBankAccount_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + Address = new Address { Country = "US" }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + const string setupIntentId = "seti_123"; + + sutProvider.GetDependency().Get(provider.Id).Returns(setupIntentId); + + sutProvider.GetDependency().SetupIntentGet(setupIntentId, Arg.Is(options => + options.Expand.Contains("payment_method"))).Returns(new SetupIntent + { + Id = setupIntentId, + Status = "requires_action", + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + PaymentMethod = new PaymentMethod + { + UsBankAccount = new PaymentMethodUsBankAccount() + } + }); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupSubscription_ChargeAutomatically_HasPayPal_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + Address = new Address + { + Country = "US" + }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary + { + ["btCustomerId"] = "braintree_customer_id" + } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupSubscription_ReverseCharge_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + Address = new Address { Country = "CA" }, + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethodId = "pm_123" + } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + #endregion #region UpdateSeatMinimums diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderPriceAdapterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs similarity index 97% rename from bitwarden_license/test/Commercial.Core.Test/Billing/ProviderPriceAdapterTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs index 9ecb4b0511..3087d5761c 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderPriceAdapterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs @@ -1,11 +1,11 @@ -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Stripe; using Xunit; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Providers; public class ProviderPriceAdapterTests { diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs similarity index 98% rename from bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs index 3995fb9de6..f3164a14e0 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs @@ -1,9 +1,9 @@ -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Tax; [SutProviderCustomize] public class TaxServiceTests diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs index 347f5b2128..16ae8f7f2c 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs @@ -1,9 +1,12 @@ using Bit.Commercial.Core.SecretsManager.Queries.Projects; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -15,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects; [SutProviderCustomize] public class MaxProjectsQueryTests { + [Theory] + [BitAutoData] + public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider sutProvider, + Guid organizationId) + { + sutProvider.GetDependency().SelfHosted.Returns(true); + + var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1); + + Assert.Null(max); + Assert.Null(overMax); + } + [Theory] [BitAutoData] public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider sutProvider, Guid organizationId) { + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsNull(); await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1)); @@ -28,26 +46,6 @@ public class MaxProjectsQueryTests .GetProjectCountByOrganizationIdAsync(organizationId); } - [Theory] - [BitAutoData(PlanType.FamiliesAnnually2019)] - [BitAutoData(PlanType.Custom)] - [BitAutoData(PlanType.FamiliesAnnually)] - public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType, - SutProvider sutProvider, Organization organization) - { - organization.PlanType = planType; - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - - await Assert.ThrowsAsync( - async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1)); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetProjectCountByOrganizationIdAsync(organization.Id); - } - [Theory] [BitAutoData(PlanType.TeamsMonthly2019)] [BitAutoData(PlanType.TeamsMonthly2020)] @@ -65,9 +63,14 @@ public class MaxProjectsQueryTests public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType, SutProvider sutProvider, Organization organization) { + sutProvider.GetDependency().SelfHosted.Returns(false); + organization.PlanType = planType; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().GetPlan(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); Assert.Null(limit); @@ -110,6 +113,9 @@ public class MaxProjectsQueryTests sutProvider.GetDependency().GetProjectCountByOrganizationIdAsync(organization.Id) .Returns(projects); + sutProvider.GetDependency().GetPlan(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); Assert.NotNull(max); diff --git a/dev/.env.example b/dev/.env.example index f0aed83a59..7f049728d7 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -11,6 +11,7 @@ MAILCATCHER_PORT=1080 # Alternative databases POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123 MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123 +MARIADB_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123 # IdP configuration # Complete using the values from the Manage SSO page in the web vault diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 1bfbe0a9d7..601989a473 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -70,6 +70,20 @@ services: profiles: - mysql + mariadb: + image: mariadb:10 + ports: + - 4306:3306 + environment: + MARIADB_USER: maria + MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD} + MARIADB_DATABASE: vault_dev + MARIADB_RANDOM_ROOT_PASSWORD: "true" + volumes: + - mariadb_dev_data:/var/lib/mysql + profiles: + - mariadb + idp: image: kenchan0130/simplesamlphp:1.19.8 container_name: idp @@ -124,8 +138,20 @@ services: profiles: - servicebus + redis: + image: redis:alpine + container_name: bw-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + profiles: + - redis + volumes: mssql_dev_data: postgres_dev_data: mysql_dev_data: rabbitmq_data: + redis_data: diff --git a/dev/migrate.ps1 b/dev/migrate.ps1 index d129af4e6e..287a2d18ee 100755 --- a/dev/migrate.ps1 +++ b/dev/migrate.ps1 @@ -5,6 +5,7 @@ param( [switch]$all, [switch]$postgres, [switch]$mysql, + [switch]$mariadb, [switch]$mssql, [switch]$sqlite, [switch]$selfhost, @@ -15,11 +16,15 @@ param( $ErrorActionPreference = "Stop" $currentDir = Get-Location -if (!$all -and !$postgres -and !$mysql -and !$sqlite) { +function Get-IsEFDatabase { + return $postgres -or $mysql -or $mariadb -or $sqlite; +} + +if (!$all -and !$(Get-IsEFDatabase)) { $mssql = $true; } -if ($all -or $postgres -or $mysql -or $sqlite) { +if ($all -or $(Get-IsEFDatabase)) { dotnet ef *> $null if ($LASTEXITCODE -ne 0) { Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install" @@ -60,9 +65,12 @@ if ($all -or $mssql) { } Foreach ($item in @( - @($mysql, "MySQL", "MySqlMigrations", "mySql", 2), @($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0), - @($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1) + @($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1), + @($mysql, "MySQL", "MySqlMigrations", "mySql", 2), + # MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context. + # However they can still be run independently for integration tests. + @($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3) )) { if (!$item[0] -and !$all) { continue diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json index 073a44618f..b107bc6190 100644 --- a/dev/servicebusemulator_config.json +++ b/dev/servicebusemulator_config.json @@ -33,6 +33,39 @@ "Name": "events-webhook-subscription" } ] + }, + { + "Name": "event-integrations", + "Subscriptions": [ + { + "Name": "integration-slack-subscription", + "Rules": [ + { + "Name": "slack-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "slack" + } + } + } + ] + }, + { + "Name": "integration-webhook-subscription", + "Rules": [ + { + "Name": "webhook-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "webhook" + } + } + } + ] + } + ] } ] } diff --git a/global.json b/global.json index 0c1d58f410..d04c13bbb5 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,7 @@ "rollForward": "latestFeature" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "4.1.0" + "Microsoft.Build.Traversal": "4.1.0", + "Microsoft.Build.Sql": "0.1.9-preview" } } diff --git a/perf/load/helpers/auth.js b/perf/load/helpers/auth.js index 1e225d5e49..7d7fd50678 100644 --- a/perf/load/helpers/auth.js +++ b/perf/load/helpers/auth.js @@ -40,8 +40,6 @@ export function authenticate( payload["deviceName"] = "chrome"; payload["username"] = username; payload["password"] = password; - - params.headers["Auth-Email"] = encoding.b64encode(username); } else { payload["scope"] = "api.organization"; payload["grant_type"] = "client_credentials"; diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index cb163f400a..8cd2222dbf 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -11,7 +11,7 @@ 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.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.OrganizationConnectionConfigs; @@ -462,6 +462,7 @@ public class OrganizationsController : Controller organization.UsersGetPremium = model.UsersGetPremium; organization.UseSecretsManager = model.UseSecretsManager; organization.UseRiskInsights = model.UseRiskInsights; + organization.UseOrganizationDomains = model.UseOrganizationDomains; organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies; //secrets diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index dd4332358c..b4abf81ee2 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Constants; -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; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 6af6c1b50a..c79124688e 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -102,7 +102,7 @@ public class OrganizationEditModel : OrganizationViewModel MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; SmServiceAccounts = org.SmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; - + UseOrganizationDomains = org.UseOrganizationDomains; _plans = plans; } @@ -186,6 +186,8 @@ public class OrganizationEditModel : OrganizationViewModel public int? SmServiceAccounts { get; set; } [Display(Name = "Max Autoscale Machine Accounts")] public int? MaxAutoscaleSmServiceAccounts { get; set; } + [Display(Name = "Use Organization Domains")] + public bool UseOrganizationDomains { get; set; } /** * Creates a Plan[] object for use in Javascript @@ -215,6 +217,7 @@ public class OrganizationEditModel : OrganizationViewModel Has2fa = p.Has2fa, HasApi = p.HasApi, HasSso = p.HasSso, + HasOrganizationDomains = p.HasOrganizationDomains, HasKeyConnector = p.HasKeyConnector, HasScim = p.HasScim, HasResetPassword = p.HasResetPassword, @@ -315,6 +318,7 @@ public class OrganizationEditModel : OrganizationViewModel existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats; existingOrganization.SmServiceAccounts = SmServiceAccounts; existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts; + existingOrganization.UseOrganizationDomains = UseOrganizationDomains; return existingOrganization; } } diff --git a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs index 69486bdcd2..412b17b3d7 100644 --- a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs @@ -44,6 +44,8 @@ public class OrganizationViewModel orgUsers .Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus) .Select(u => u.Email)); + OwnersDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus); + AdminsDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus); SecretsCount = secretsCount; ProjectsCount = projectCount; ServiceAccountsCount = serviceAccountsCount; @@ -70,4 +72,6 @@ public class OrganizationViewModel public int OccupiedSmSeatsCount { get; set; } public bool UseSecretsManager => Organization.UseSecretsManager; public bool UseRiskInsights => Organization.UseRiskInsights; + public IEnumerable OwnersDetails { get; set; } + public IEnumerable AdminsDetails { get; set; } } diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index 44eebb8d7d..de9e25fa6f 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -2,8 +2,8 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Enums; using Bit.SharedWeb.Utilities; diff --git a/src/Admin/AdminConsole/Models/ProviderViewModel.cs b/src/Admin/AdminConsole/Models/ProviderViewModel.cs index bcb96df006..e1277f8e87 100644 --- a/src/Admin/AdminConsole/Models/ProviderViewModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderViewModel.cs @@ -2,8 +2,8 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Providers.Entities; namespace Bit.Admin.AdminConsole.Models; @@ -19,7 +19,7 @@ public class ProviderViewModel { Provider = provider; UserCount = providerUsers.Count(); - ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); + ProviderUsers = providerUsers; ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); if (Provider.Type == ProviderType.Msp) @@ -61,7 +61,7 @@ public class ProviderViewModel public int UserCount { get; set; } public Provider Provider { get; set; } - public IEnumerable ProviderAdmins { get; set; } + public IEnumerable ProviderUsers { get; set; } public IEnumerable ProviderOrganizations { get; set; } public List ProviderPlanViewModels { get; set; } = []; } diff --git a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml index a0d421235d..9b2f7d69f8 100644 --- a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml @@ -19,12 +19,6 @@ @Model.UserConfirmedCount) -
Owners
-
@(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)
- -
Admins
-
@(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)
-
Using 2FA
@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")
@@ -76,3 +70,49 @@
Secrets Manager Seats
@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )
+ +

Administrators

+
+
+
+ + + + + + + + + + @if(!Model.Admins.Any() && !Model.Owners.Any()) + { + + + + } + else + { + @foreach(var owner in Model.OwnersDetails) + { + + + + + + } + + @foreach(var admin in Model.AdminsDetails) + { + + + + + + + } + } + +
EmailRoleStatus
No results to list.
@owner.EmailOwner@owner.Status
@admin.EmailAdmin@admin.Status
+
+
+
diff --git a/src/Admin/AdminConsole/Views/Providers/Admins.cshtml b/src/Admin/AdminConsole/Views/Providers/Admins.cshtml index 86043f3a6d..29eddc8964 100644 --- a/src/Admin/AdminConsole/Views/Providers/Admins.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Admins.cshtml @@ -7,7 +7,7 @@ var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite); } -

Provider Admins

+

Administrators

@@ -15,12 +15,13 @@ Email + Role Status - @if(!Model.ProviderAdmins.Any()) + @if(!Model.ProviderUsers.Any()) { No results to list. @@ -28,29 +29,39 @@ } else { - @foreach(var admin in Model.ProviderAdmins) + @foreach(var user in Model.ProviderUsers) { - @admin.Email + @user.Email - @admin.Status + @if(@user.Type == 0) + { + Provider Admin + } + else + { + Service User + } + + + @user.Status - @if(admin.Status.Equals(ProviderUserStatusType.Confirmed) + @if(user.Status.Equals(ProviderUserStatusType.Confirmed) && @Model.Provider.Status.Equals(ProviderStatusType.Pending) && canResendEmailInvite) { - @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString()) + @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @user.UserId.Value.ToString()) { } else { Resend Setup Invite diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index 7b19b19939..267264a38f 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -124,6 +124,10 @@
+
+ + +
diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml index 98d4c0d900..ea4448d100 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml @@ -69,6 +69,7 @@ document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups; document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies; document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso; + document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = plan.hasOrganizationDomains; document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim; document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory; document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents; diff --git a/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs b/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs index 2421710d41..be3a94949f 100644 --- a/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs +++ b/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs @@ -7,7 +7,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Utilities; diff --git a/src/Admin/Billing/Controllers/MigrateProvidersController.cs b/src/Admin/Billing/Controllers/MigrateProvidersController.cs index d4ef105e34..ef5ea2312e 100644 --- a/src/Admin/Billing/Controllers/MigrateProvidersController.cs +++ b/src/Admin/Billing/Controllers/MigrateProvidersController.cs @@ -1,8 +1,8 @@ using Bit.Admin.Billing.Models; using Bit.Admin.Enums; using Bit.Admin.Utilities; -using Bit.Core.Billing.Migration.Models; -using Bit.Core.Billing.Migration.Services; +using Bit.Core.Billing.Providers.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Admin/Billing/Models/ProviderPlanViewModel.cs b/src/Admin/Billing/Models/ProviderPlanViewModel.cs index 7a50aba286..391c24d6df 100644 --- a/src/Admin/Billing/Models/ProviderPlanViewModel.cs +++ b/src/Admin/Billing/Models/ProviderPlanViewModel.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; namespace Bit.Admin.Billing.Models; diff --git a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml index 303e6d2e45..6ee0344057 100644 --- a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml +++ b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml @@ -1,5 +1,5 @@ @using System.Text.Json -@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult +@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult @{ ViewData["Title"] = "Results"; } diff --git a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml index 45611de80e..94db08db3d 100644 --- a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml +++ b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml @@ -1,4 +1,4 @@ -@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[] +@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[] @{ ViewData["Title"] = "Results"; } diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 71be19a041..b85a91719c 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -4,7 +4,6 @@ using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; -using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -89,7 +88,7 @@ public class UsersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); - var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); + var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id); return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain)); } @@ -106,7 +105,7 @@ public class UsersController : Controller 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 verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id); return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); @@ -167,7 +166,6 @@ public class UsersController : Controller [HttpPost] [ValidateAntiForgeryToken] [RequirePermission(Permission.User_NewDeviceException_Edit)] - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] public async Task ToggleNewDeviceVerification(Guid id) { var user = await _userRepository.GetByIdAsync(id); @@ -179,12 +177,4 @@ public class UsersController : Controller await _userService.ToggleNewDeviceVerificationException(user.Id); return RedirectToAction("Edit", new { id }); } - - // TODO: Feature flag to be removed in PM-14207 - private async Task AccountDeprovisioningEnabled(Guid userId) - { - return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - ? await _userService.IsClaimedByAnyOrganizationAsync(userId) - : null; - } } diff --git a/src/Admin/Models/ChargeBraintreeModel.cs b/src/Admin/Models/ChargeBraintreeModel.cs index 2ba06cb980..8c2f39e58d 100644 --- a/src/Admin/Models/ChargeBraintreeModel.cs +++ b/src/Admin/Models/ChargeBraintreeModel.cs @@ -17,7 +17,7 @@ public class ChargeBraintreeModel : IValidatableObject { if (Id != null) { - if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u') || + if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u' && Id[0] != 'p') || !Guid.TryParse(Id.Substring(1, 32), out var guid)) { yield return new ValidationResult("Customer Id is not a valid format."); diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 11f9e7ce68..5b34e13f6c 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Migration; +using Bit.Core.Billing.Providers.Migration; #if !OSS using Bit.Commercial.Core.Utilities; diff --git a/src/Admin/Tools/Jobs/DeleteSendsJob.cs b/src/Admin/Tools/Jobs/DeleteSendsJob.cs index dafce03994..7449d2ea01 100644 --- a/src/Admin/Tools/Jobs/DeleteSendsJob.cs +++ b/src/Admin/Tools/Jobs/DeleteSendsJob.cs @@ -2,7 +2,7 @@ using Bit.Core; using Bit.Core.Jobs; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Services; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Quartz; namespace Bit.Admin.Tools.Jobs; @@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob } using (var scope = _serviceProvider.CreateScope()) { - var sendService = scope.ServiceProvider.GetRequiredService(); + var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService(); foreach (var send in sends) { - await sendService.DeleteSendAsync(send); + await nonAnonymousSendCommand.DeleteSendAsync(send); } } } diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 24c2466746..e73ccfcef5 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -9,18 +9,18 @@ "version": "0.0.0", "license": "GPL-3.0", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1", "toastr": "2.1.4" }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", - "sass-loader": "16.0.4", - "webpack": "5.97.1", + "sass": "1.88.0", + "sass-loader": "16.0.5", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } }, @@ -456,13 +456,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@webassemblyjs/ast": { @@ -749,9 +749,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", "funding": [ { "type": "github", @@ -782,9 +782,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -802,10 +802,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -822,9 +822,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -976,9 +976,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.128", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", - "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", "dev": true, "license": "ISC" }, @@ -1010,9 +1010,9 @@ } }, "node_modules/es-module-lexer": { - "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==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -1084,9 +1084,9 @@ } }, "node_modules/expose-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", - "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz", + "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==", "dev": true, "license": "MIT", "engines": { @@ -1107,13 +1107,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -1249,9 +1242,9 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", "dev": true, "license": "MIT" }, @@ -1755,16 +1748,6 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1878,9 +1861,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", + "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", "dev": true, "license": "MIT", "dependencies": { @@ -1899,9 +1882,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", - "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", "dev": true, "license": "MIT", "dependencies": { @@ -1940,9 +1923,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1960,9 +1943,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -2079,9 +2062,9 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -2089,14 +2072,14 @@ } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2165,9 +2148,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -2202,16 +2185,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2220,9 +2193,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", "dependencies": { @@ -2234,14 +2207,15 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -2258,9 +2232,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -2361,59 +2335,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/Admin/package.json b/src/Admin/package.json index 7f3c8046a2..e88cd42eca 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -8,18 +8,18 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1", "toastr": "2.1.4" }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", - "sass-loader": "16.0.4", - "webpack": "5.97.1", + "sass": "1.88.0", + "sass-loader": "16.0.5", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } } diff --git a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs index ba00ea6c18..accb9539fa 100644 --- a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs +++ b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs @@ -9,7 +9,7 @@ namespace Bit.Api.AdminConsole.Authorization; public static class HttpContextExtensions { public const string NoOrgIdError = - "A route decorated with with '[Authorize]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute."; + "A route decorated with with '[Authorize]' must include a route value named 'orgId' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute."; /// /// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request. @@ -61,19 +61,27 @@ public static class HttpContextExtensions /// - /// Parses the {orgId} route parameter into a Guid, or throws if the {orgId} is not present or not a valid guid. + /// Parses the {orgId} or {organizationId} route parameter into a Guid, or throws if neither are present or are not valid guids. /// /// /// /// public static Guid GetOrganizationId(this HttpContext httpContext) { - httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam); - if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId)) + var routeValues = httpContext.GetRouteData().Values; + + routeValues.TryGetValue("orgId", out var orgIdParam); + if (orgIdParam != null && Guid.TryParse(orgIdParam.ToString(), out var orgId)) { - throw new InvalidOperationException(NoOrgIdError); + return orgId; } - return orgId; + routeValues.TryGetValue("organizationId", out var organizationIdParam); + if (organizationIdParam != null && Guid.TryParse(organizationIdParam.ToString(), out var organizationId)) + { + return organizationId; + } + + throw new InvalidOperationException(NoOrgIdError); } } diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs new file mode 100644 index 0000000000..268fee5d95 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Bit.Core.Context; +using Bit.Core.Enums; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +public class ManageAccountRecoveryRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + { Type: OrganizationUserType.Admin } => true, + { Permissions.ManageResetPassword: true } => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index b9afde2724..a8882dfaf3 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -2,13 +2,11 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller [AllowAnonymous] [HttpPost("domain/sso/verified")] - [RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)] public async Task GetVerifiedOrgDomainSsoDetailsAsync( [FromBody] OrganizationDomainSsoDetailsRequestModel model) { diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5a714943f0..6b23edf347 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,4 +1,5 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; @@ -63,6 +64,7 @@ public class OrganizationUsersController : Controller private readonly IPricingClient _pricingClient; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; + private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -89,7 +91,8 @@ public class OrganizationUsersController : Controller IFeatureService featureService, IPricingClient pricingClient, IConfirmOrganizationUserCommand confirmOrganizationUserCommand, - IRestoreOrganizationUserCommand restoreOrganizationUserCommand) + IRestoreOrganizationUserCommand restoreOrganizationUserCommand, + IInitPendingOrganizationCommand initPendingOrganizationCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -116,6 +119,7 @@ public class OrganizationUsersController : Controller _pricingClient = pricingClient; _confirmOrganizationUserCommand = confirmOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; + _initPendingOrganizationCommand = initPendingOrganizationCommand; } [HttpGet("{id}")] @@ -159,6 +163,12 @@ public class OrganizationUsersController : Controller [HttpGet("")] public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) { + + if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions)) + { + return await GetvNextAsync(orgId, includeGroups, includeCollections); + } + var authorized = (await _authorizationService.AuthorizeAsync( User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; if (!authorized) @@ -188,6 +198,37 @@ public class OrganizationUsersController : Controller return new ListResponseModel(responses); } + private async Task> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false) + { + var request = new OrganizationUserUserDetailsQueryRequest + { + OrganizationId = orgId, + IncludeGroups = includeGroups, + IncludeCollections = includeCollections, + }; + + if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded) + { + return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request)); + } + + if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded) + { + return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request)); + } + + throw new NotFoundException(); + } + + private ListResponseModel GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser, + bool TwoFactorEnabled, bool ClaimedByOrganization)> results) + { + return new ListResponseModel(results + .Select(result => new OrganizationUserUserDetailsResponseModel(result)) + .ToList()); + } + + [HttpGet("{id}/groups")] public async Task> GetGroups(string orgId, string id) { @@ -313,7 +354,7 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - await _organizationService.InitPendingOrganization(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token); + await _initPendingOrganizationCommand.InitPendingOrganizationAsync(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } @@ -575,7 +616,6 @@ public class OrganizationUsersController : Controller new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); } - [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [HttpDelete("{id}/delete-account")] [HttpPost("{id}/delete-account")] public async Task DeleteAccount(Guid orgId, Guid id) @@ -594,7 +634,6 @@ public class OrganizationUsersController : Controller await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); } - [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [HttpDelete("delete-account")] [HttpPost("delete-account")] public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) @@ -719,11 +758,6 @@ public class OrganizationUsersController : Controller private async Task> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable userIds) { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - return userIds.ToDictionary(kvp => kvp, kvp => false); - } - var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds); return usersOrganizationClaimedStatus; } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index c856c8ab91..0d498beab1 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -25,7 +25,7 @@ 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.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -279,8 +279,7 @@ public class OrganizationsController : Controller throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id)) + if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id)) { throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details."); } diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 7de6f6e730..86a1609ee6 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -2,7 +2,6 @@ using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -79,7 +78,7 @@ public class PoliciesController : Controller return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg) + if (policy.Type is PolicyType.SingleOrg) { return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery); } diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index 74d2feff3c..f226ba316e 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -2,7 +2,7 @@ using Bit.Api.Billing.Models.Requests; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Api/AdminConsole/Controllers/ProvidersController.cs b/src/Api/AdminConsole/Controllers/ProvidersController.cs index be119744b3..b6933da0c9 100644 --- a/src/Api/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Api/AdminConsole/Controllers/ProvidersController.cs @@ -84,22 +84,22 @@ public class ProvidersController : Controller var userId = _userService.GetProperUserId(User).Value; - var taxInfo = model.TaxInfo != null - ? new TaxInfo - { - BillingAddressCountry = model.TaxInfo.Country, - BillingAddressPostalCode = model.TaxInfo.PostalCode, - TaxIdNumber = model.TaxInfo.TaxId, - BillingAddressLine1 = model.TaxInfo.Line1, - BillingAddressLine2 = model.TaxInfo.Line2, - BillingAddressCity = model.TaxInfo.City, - BillingAddressState = model.TaxInfo.State - } - : null; + var taxInfo = new TaxInfo + { + BillingAddressCountry = model.TaxInfo.Country, + BillingAddressPostalCode = model.TaxInfo.PostalCode, + TaxIdNumber = model.TaxInfo.TaxId, + BillingAddressLine1 = model.TaxInfo.Line1, + BillingAddressLine2 = model.TaxInfo.Line2, + BillingAddressCity = model.TaxInfo.City, + BillingAddressState = model.TaxInfo.State + }; + + var tokenizedPaymentSource = model.PaymentSource?.ToDomain(); var response = await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key, - taxInfo); + taxInfo, tokenizedPaymentSource); return new ProviderResponseModel(response); } diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs index a8bef10dc6..c0ab5c059b 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs @@ -2,10 +2,10 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 539260a312..e18122fd2b 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject public string InitiationPath { get; set; } + public bool SkipTrial { get; set; } + public virtual OrganizationSignup ToOrganizationSignup(User user) { var orgSignup = new OrganizationSignup @@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, + SkipTrial = SkipTrial }; Keys?.ToOrganizationSignup(orgSignup); diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index 6566760e17..ccab2b36ae 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,8 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; #nullable enable diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs index 5e10807c69..697077c9b6 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Bit.Api.Billing.Models.Requests; using Bit.Api.Models.Request; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Utilities; @@ -23,7 +24,9 @@ public class ProviderSetupRequestModel public string Token { get; set; } [Required] public string Key { get; set; } + [Required] public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; } + public TokenizedPaymentSourceRequestBody PaymentSource { get; set; } public virtual Provider ToProvider(Provider provider) { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index a14e3efb51..95754598b9 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; } @@ -111,6 +112,7 @@ public class OrganizationResponseModel : ResponseModel public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index 4e869f59b1..057841c7d2 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel { + public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = "organizationUserUserDetails") + : base(data.OrgUser, obj) + { + if (data.OrgUser == null) + { + throw new ArgumentNullException(nameof(data.OrgUser)); + } + + Name = data.OrgUser.Name; + Email = data.OrgUser.Email; + AvatarColor = data.OrgUser.AvatarColor; + TwoFactorEnabled = data.TwoFactorEnabled; + SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId); + Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c)); + Groups = data.OrgUser.Groups; + // Prevent reset password when using key connector. + ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector; + ClaimedByOrganization = data.ClaimedByOrganization; + } + public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails") : base(organizationUser, obj) diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index c74599a70e..cb0ab62fd1 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel ProviderName = organization.ProviderName; ProviderType = organization.ProviderType; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; - FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && + IsAdminInitiated = organization.IsAdminInitiated ?? false; + FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) && StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); ProductTierType = organization.PlanType.GetProductTier(); @@ -72,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId); UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; if (organization.SsoConfig != null) @@ -135,7 +137,6 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool AllowAdminAccessToAllCollectionItems { get; set; } /// /// Obsolete. - /// /// See /// [Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")] @@ -145,16 +146,15 @@ public class ProfileOrganizationResponseModel : ResponseModel set => UserIsClaimedByOrganization = value; } /// - /// Indicates if the organization claims the user. + /// Indicates if the user is claimed by the organization. /// /// - /// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it. + /// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member. /// The organization must be enabled and able to have verified domains. /// - /// - /// False if the Account Deprovisioning feature flag is disabled. - /// public bool UserIsClaimedByOrganization { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool IsAdminInitiated { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 5d5e1f9b85..24b6fed704 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -50,6 +50,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; } } diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 92e5071801..6552684ca3 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -76,7 +76,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), + var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser), collections); return new JsonResult(response); } @@ -185,7 +185,7 @@ public class MembersController : Controller { var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); response = new MemberResponseModel(existingUserDetails, - await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations); + await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations); } else { diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 6505fdab5b..c490e90150 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -4,8 +4,6 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml true - - $(WarningsNotAsErrors);CS8604 diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 621524228a..2499b269f5 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities; 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.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; @@ -45,6 +46,7 @@ public class AccountsController : Controller private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; private readonly IRotationValidator, IEnumerable> _cipherValidator; @@ -68,6 +70,7 @@ public class AccountsController : Controller ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, IRotateUserKeyCommand rotateUserKeyCommand, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, IRotationValidator, IEnumerable> cipherValidator, IRotationValidator, IEnumerable> folderValidator, @@ -87,6 +90,7 @@ public class AccountsController : Controller _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _rotateUserKeyCommand = rotateUserKeyCommand; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; _cipherValidator = cipherValidator; _folderValidator = folderValidator; @@ -389,7 +393,7 @@ public class AccountsController : Controller await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed); - var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -423,7 +427,7 @@ public class AccountsController : Controller await _userService.SaveUserAsync(model.ToUser(user)); - var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -442,7 +446,7 @@ public class AccountsController : Controller } await _userService.SaveUserAsync(model.ToUser(user), true); - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -514,9 +518,8 @@ public class AccountsController : Controller } else { - // If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) + // Check if the user is claimed by any organization. + if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) { throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details."); } @@ -693,7 +696,6 @@ public class AccountsController : Controller } } - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] [AllowAnonymous] [HttpPost("resend-new-device-otp")] public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request) diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index 2fb9a67199..90b265715d 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -90,6 +90,13 @@ public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessRespons public class EmergencyAccessTakeoverResponseModel : ResponseModel { + /// + /// Creates a new instance of the class. + /// + /// Consumed for the Encrypted Key value + /// consumed for the KDF configuration + /// name of the object + /// emergencyAccess cannot be null public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj) { if (emergencyAccess == null) diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index fcb89226e7..7abcf8c357 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,7 +1,7 @@ #nullable enable using Bit.Api.Billing.Models.Responses; -using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Requests; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index bc263691a8..49ff679bb8 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -3,6 +3,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Utilities; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers; [Route("accounts")] [Authorize("Application")] public class AccountsController( - IUserService userService) : Controller + IUserService userService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller { [HttpPost("premium")] public async Task PostPremiumAsync( @@ -56,7 +58,7 @@ public class AccountsController( model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); - var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); diff --git a/src/Api/Billing/Controllers/InvoicesController.cs b/src/Api/Billing/Controllers/InvoicesController.cs index 686d9b9643..5a1d732f42 100644 --- a/src/Api/Billing/Controllers/InvoicesController.cs +++ b/src/Api/Billing/Controllers/InvoicesController.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Api.Requests.Organizations; +using Bit.Core.Billing.Tax.Requests; using Bit.Core.Context; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 1d4ebc1511..094ca0a435 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -1,12 +1,16 @@ #nullable enable +using System.Diagnostics; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Api.Billing.Queries.Organizations; using Bit.Core; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Repositories; using Bit.Core.Services; @@ -24,6 +28,7 @@ public class OrganizationBillingController( IFeatureService featureService, IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, + IOrganizationWarningsQuery organizationWarningsQuery, IPaymentService paymentService, IPricingClient pricingClient, ISubscriberService subscriberService, @@ -288,13 +293,20 @@ public class OrganizationBillingController( sale.Organization.PlanType = plan.Type; sale.Organization.Plan = plan.Name; sale.SubscriptionSetup.SkipTrial = true; - await organizationBillingService.Finalize(sale); - var org = await organizationRepository.GetByIdAsync(organizationId); - if (organizationSignup.PaymentMethodType != null) + + if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken)) { - var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); - var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); - await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation); + return Error.BadRequest("A payment method is required to restart the subscription."); + } + var org = await organizationRepository.GetByIdAsync(organizationId); + Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); + var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); + var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); + await organizationBillingService.Finalize(sale); + var updatedOrg = await organizationRepository.GetByIdAsync(organizationId); + if (updatedOrg != null) + { + await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation); } return TypedResults.Ok(); @@ -335,4 +347,28 @@ public class OrganizationBillingController( return TypedResults.Ok(providerId); } + + [HttpGet("warnings")] + public async Task GetWarningsAsync([FromRoute] Guid organizationId) + { + /* + * We'll keep these available at the User level, because we're hiding any pertinent information and + * we want to throw as few errors as possible since these are not core features. + */ + if (!await currentContext.OrganizationUser(organizationId)) + { + return Error.Unauthorized(); + } + + var organization = await organizationRepository.GetByIdAsync(organizationId); + + if (organization == null) + { + return Error.NotFound(); + } + + var response = await organizationWarningsQuery.Run(organization); + + return TypedResults.Ok(response); + } } diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index 67cd691a34..c45b34422c 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -1,4 +1,5 @@ using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; @@ -8,6 +9,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Api.Request.OrganizationSponsorships; using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -105,13 +107,16 @@ public class OrganizationSponsorshipsController : Controller model.FriendlyName, model.IsAdminInitiated.GetValueOrDefault(), model.Notes); - await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); + if (sponsorship.OfferedToEmail != null) + { + await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); + } } [Authorize("Application")] [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) + public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName) { var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId, PolicyType.FreeFamiliesSponsorshipPolicy); @@ -124,11 +129,14 @@ public class OrganizationSponsorshipsController : Controller var sponsoringOrgUser = await _organizationUserRepository .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); - await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( - await _organizationRepository.GetByIdAsync(sponsoringOrgId), - sponsoringOrgUser, - await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)); + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + var filteredSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase)); + if (filteredSponsorship != null) + { + await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( + await _organizationRepository.GetByIdAsync(sponsoringOrgId), + sponsoringOrgUser, filteredSponsorship); + } } [Authorize("Application")] @@ -214,6 +222,20 @@ public class OrganizationSponsorshipsController : Controller await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + [Authorize("Application")] + [HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName) + { + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase)); + if (existingOrgSponsorship == null) + { + throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization."); + } + await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); + } + [Authorize("Application")] [HttpDelete("sponsored/{sponsoredOrgId}")] [HttpPost("sponsored/{sponsoredOrgId}/remove")] @@ -246,5 +268,30 @@ public class OrganizationSponsorshipsController : Controller return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate); } + [Authorize("Application")] + [HttpGet("{sponsoringOrgId}/sponsored")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task> GetSponsoredOrganizations(Guid sponsoringOrgId) + { + var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId); + if (sponsoringOrg == null) + { + throw new NotFoundException(); + } + var organization = _currentContext.Organizations.First(x => x.Id == sponsoringOrg.Id); + if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id) && !await _currentContext.OrganizationAdmin(sponsoringOrg.Id) && !organization.Permissions.ManageUsers) + { + throw new UnauthorizedAccessException(); + } + + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + return new ListResponseModel( + sponsorships + .Where(s => s.IsAdminInitiated) + .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))) + ); + + } + private Task CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); } diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 510f6c2835..bd5ab8cef4 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -109,28 +109,6 @@ public class OrganizationsController( return license; } - [HttpPost("{id:guid}/payment")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model) - { - if (!await currentContext.EditPaymentMethods(id)) - { - throw new NotFoundException(); - } - - await organizationService.ReplacePaymentMethodAsync(id, model.PaymentToken, - model.PaymentMethodType.Value, new TaxInfo - { - BillingAddressLine1 = model.Line1, - BillingAddressLine2 = model.Line2, - BillingAddressState = model.State, - BillingAddressCity = model.City, - BillingAddressPostalCode = model.PostalCode, - BillingAddressCountry = model.Country, - TaxIdNumber = model.TaxId, - }); - } - [HttpPost("{id:guid}/upgrade")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model) diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index bb1fd7bb25..37130d54ce 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -1,11 +1,14 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Models.BitStripe; using Bit.Core.Services; @@ -147,13 +150,33 @@ public class ProviderBillingController( var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe); + var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan => { var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); + + decimal unitAmount; + + if (getProviderPriceFromStripe) + { + var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type); + var price = await stripeAdapter.PriceGetAsync(priceId); + + unitAmount = price.UnitAmountDecimal.HasValue + ? price.UnitAmountDecimal.Value / 100M + : plan.PasswordManager.ProviderPortalSeatPrice; + } + else + { + unitAmount = plan.PasswordManager.ProviderPortalSeatPrice; + } + return new ConfiguredProviderPlan( providerPlan.Id, providerPlan.ProviderId, plan, + unitAmount, providerPlan.SeatMinimum ?? 0, providerPlan.PurchasedSeats ?? 0, providerPlan.AllocatedSeats ?? 0); diff --git a/src/Api/Billing/Controllers/StripeController.cs b/src/Api/Billing/Controllers/StripeController.cs index f5e8253bfa..15fccd16f4 100644 --- a/src/Api/Billing/Controllers/StripeController.cs +++ b/src/Api/Billing/Controllers/StripeController.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/Api/Billing/Controllers/TaxController.cs b/src/Api/Billing/Controllers/TaxController.cs new file mode 100644 index 0000000000..7b8b9d960f --- /dev/null +++ b/src/Api/Billing/Controllers/TaxController.cs @@ -0,0 +1,36 @@ +using Bit.Api.Billing.Models.Requests; +using Bit.Core.Billing.Tax.Commands; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Billing.Controllers; + +[Authorize("Application")] +[Route("tax")] +public class TaxController( + IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController +{ + [HttpPost("preview-amount/organization-trial")] + public async Task PreviewTaxAmountForOrganizationTrialAsync( + [FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody) + { + var parameters = new OrganizationTrialParameters + { + PlanType = requestBody.PlanType, + ProductType = requestBody.ProductType, + TaxInformation = new OrganizationTrialParameters.TaxInformationDTO + { + Country = requestBody.TaxInformation.Country, + PostalCode = requestBody.TaxInformation.PostalCode, + TaxId = requestBody.TaxInformation.TaxId + } + }; + + var result = await previewTaxAmountCommand.Run(parameters); + + return result.Match( + taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }), + badRequest => Error.BadRequest(badRequest.TranslationKey), + unhandled => Error.ServerError(unhandled.TranslationKey)); + } +} diff --git a/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs b/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs new file mode 100644 index 0000000000..a3fda0fd6c --- /dev/null +++ b/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs @@ -0,0 +1,27 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Enums; + +namespace Bit.Api.Billing.Models.Requests; + +public class PreviewTaxAmountForOrganizationTrialRequestBody +{ + [Required] + public PlanType PlanType { get; set; } + + [Required] + public ProductType ProductType { get; set; } + + [Required] public TaxInformationDTO TaxInformation { get; set; } = null!; + + public class TaxInformationDTO + { + [Required] + public string Country { get; set; } = null!; + + [Required] + public string PostalCode { get; set; } = null!; + + public string? TaxId { get; set; } + } +} diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index 32ba2effb2..edc45ce483 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 1dfc79be21..341dbceadf 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -12,7 +12,8 @@ public record OrganizationMetadataResponse( bool IsSubscriptionCanceled, DateTime? InvoiceDueDate, DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate) + DateTime? SubPeriodEndDate, + int OrganizationOccupiedSeats) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) => new( @@ -25,5 +26,6 @@ public record OrganizationMetadataResponse( metadata.IsSubscriptionCanceled, metadata.InvoiceDueDate, metadata.InvoiceCreatedDate, - metadata.SubPeriodEndDate); + metadata.SubPeriodEndDate, + metadata.OrganizationOccupiedSeats); } diff --git a/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs b/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs new file mode 100644 index 0000000000..e124bdc318 --- /dev/null +++ b/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace Bit.Api.Billing.Models.Responses.Organizations; + +public record OrganizationWarningsResponse +{ + public FreeTrialWarning? FreeTrial { get; set; } + public InactiveSubscriptionWarning? InactiveSubscription { get; set; } + public ResellerRenewalWarning? ResellerRenewal { get; set; } + + public record FreeTrialWarning + { + public int RemainingTrialDays { get; set; } + } + + public record InactiveSubscriptionWarning + { + public required string Resolution { get; set; } + } + + public record ResellerRenewalWarning + { + public required string Type { get; set; } + public UpcomingRenewal? Upcoming { get; set; } + public IssuedRenewal? Issued { get; set; } + public PastDueRenewal? PastDue { get; set; } + + public record UpcomingRenewal + { + public required DateTime RenewalDate { get; set; } + } + + public record IssuedRenewal + { + public required DateTime IssuedDate { get; set; } + public required DateTime DueDate { get; set; } + } + + public record PastDueRenewal + { + public required DateTime SuspensionDate { get; set; } + } + } +} diff --git a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs index b89c1e9db9..fd248a0a00 100644 --- a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs +++ b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index ea1479c9df..e5b868af9a 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -2,6 +2,8 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Tax.Models; using Stripe; namespace Bit.Api.Billing.Models.Responses; @@ -34,7 +36,7 @@ public record ProviderSubscriptionResponse( .Select(providerPlan => { var plan = providerPlan.Plan; - var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; + var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * providerPlan.Price; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; return new ProviderPlanResponse( plan.Name, diff --git a/src/Api/Billing/Models/Responses/TaxInformationResponse.cs b/src/Api/Billing/Models/Responses/TaxInformationResponse.cs index 02349d74f7..59e4934751 100644 --- a/src/Api/Billing/Models/Responses/TaxInformationResponse.cs +++ b/src/Api/Billing/Models/Responses/TaxInformationResponse.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs b/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs new file mode 100644 index 0000000000..f6a0e5b1e6 --- /dev/null +++ b/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs @@ -0,0 +1,214 @@ +// ReSharper disable InconsistentNaming + +#nullable enable + +using Bit.Api.Billing.Models.Responses.Organizations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Services; +using Stripe; +using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning; +using InactiveSubscriptionWarning = + Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning; +using ResellerRenewalWarning = + Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning; + +namespace Bit.Api.Billing.Queries.Organizations; + +public interface IOrganizationWarningsQuery +{ + Task Run( + Organization organization); +} + +public class OrganizationWarningsQuery( + ICurrentContext currentContext, + IProviderRepository providerRepository, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService) : IOrganizationWarningsQuery +{ + public async Task Run( + Organization organization) + { + var response = new OrganizationWarningsResponse(); + + var subscription = + await subscriberService.GetSubscription(organization, + new SubscriptionGetOptions { Expand = ["customer", "latest_invoice", "test_clock"] }); + + if (subscription == null) + { + return response; + } + + response.FreeTrial = await GetFreeTrialWarning(organization, subscription); + + var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id); + + response.InactiveSubscription = await GetInactiveSubscriptionWarning(organization, provider, subscription); + + response.ResellerRenewal = await GetResellerRenewalWarning(provider, subscription); + + return response; + } + + private async Task GetFreeTrialWarning( + Organization organization, + Subscription subscription) + { + if (!await currentContext.EditSubscription(organization.Id)) + { + return null; + } + + if (subscription is not + { + Status: StripeConstants.SubscriptionStatus.Trialing, + TrialEnd: not null, + Customer: not null + }) + { + return null; + } + + var customer = subscription.Customer; + + var hasPaymentMethod = + !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || + !string.IsNullOrEmpty(customer.DefaultSourceId) || + customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId); + + if (hasPaymentMethod) + { + return null; + } + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + var remainingTrialDays = (int)Math.Ceiling((subscription.TrialEnd.Value - now).TotalDays); + + return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays }; + } + + private async Task GetInactiveSubscriptionWarning( + Organization organization, + Provider? provider, + Subscription subscription) + { + if (organization.Enabled || + subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid + and not StripeConstants.SubscriptionStatus.Canceled) + { + return null; + } + + if (provider != null) + { + return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; + } + + if (await currentContext.OrganizationOwner(organization.Id)) + { + return subscription.Status switch + { + StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning + { + Resolution = "add_payment_method" + }, + StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning + { + Resolution = "resubscribe" + }, + _ => null + }; + } + + return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; + } + + private async Task GetResellerRenewalWarning( + Provider? provider, + Subscription subscription) + { + if (provider is not + { + Type: ProviderType.Reseller + }) + { + return null; + } + + if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice) + { + return null; + } + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (subscription is + { + Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active, + LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid } + } && (subscription.CurrentPeriodEnd - now).TotalDays <= 14) + { + return new ResellerRenewalWarning + { + Type = "upcoming", + Upcoming = new ResellerRenewalWarning.UpcomingRenewal + { + RenewalDate = subscription.CurrentPeriodEnd + } + }; + } + + if (subscription is + { + Status: StripeConstants.SubscriptionStatus.Active, + LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null } + } && subscription.LatestInvoice.DueDate > now) + { + return new ResellerRenewalWarning + { + Type = "issued", + Issued = new ResellerRenewalWarning.IssuedRenewal + { + IssuedDate = subscription.LatestInvoice.Created, + DueDate = subscription.LatestInvoice.DueDate.Value + } + }; + } + + // ReSharper disable once InvertIf + if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue) + { + var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions + { + Query = $"subscription:'{subscription.Id}' status:'open'" + }); + + var earliestOverdueInvoice = openInvoices + .Where(invoice => invoice.DueDate != null && invoice.DueDate < now) + .MinBy(invoice => invoice.Created); + + if (earliestOverdueInvoice != null) + { + return new ResellerRenewalWarning + { + Type = "past_due", + PastDue = new ResellerRenewalWarning.PastDueRenewal + { + SuspensionDate = earliestOverdueInvoice.DueDate!.Value.AddDays(30) + } + }; + } + } + + return null; + } +} diff --git a/src/Api/Billing/Registrations.cs b/src/Api/Billing/Registrations.cs new file mode 100644 index 0000000000..cb92098333 --- /dev/null +++ b/src/Api/Billing/Registrations.cs @@ -0,0 +1,11 @@ +using Bit.Api.Billing.Queries.Organizations; + +namespace Bit.Api.Billing; + +public static class Registrations +{ + public static void AddBillingQueries(this IServiceCollection services) + { + services.AddTransient(); + } +} diff --git a/src/Api/Controllers/PhishingDomainsController.cs b/src/Api/Controllers/PhishingDomainsController.cs new file mode 100644 index 0000000000..f0c1a65648 --- /dev/null +++ b/src/Api/Controllers/PhishingDomainsController.cs @@ -0,0 +1,34 @@ +using Bit.Core; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers; + +[Route("phishing-domains")] +public class PhishingDomainsController(IPhishingDomainRepository phishingDomainRepository, IFeatureService featureService) : Controller +{ + [HttpGet] + public async Task>> GetPhishingDomainsAsync() + { + if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) + { + return NotFound(); + } + + var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync(); + return Ok(domains); + } + + [HttpGet("checksum")] + public async Task> GetChecksumAsync() + { + if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) + { + return NotFound(); + } + + var checksum = await phishingDomainRepository.GetCurrentChecksumAsync(); + return Ok(checksum); + } +} diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs index e328b7c3e4..371b321a4c 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -1,6 +1,10 @@ -using Bit.Api.Models.Request.Organizations; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response; using Bit.Core.Context; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; private readonly ICurrentContext _currentContext; private readonly IFeatureService _featureService; + private readonly IAuthorizationService _authorizationService; public SelfHostedOrganizationSponsorshipsController( ICreateSponsorshipCommand offerSponsorshipCommand, @@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationUserRepository organizationUserRepository, ICurrentContext currentContext, - IFeatureService featureService + IFeatureService featureService, + IAuthorizationService authorizationService ) { _offerSponsorshipCommand = offerSponsorshipCommand; @@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller _organizationUserRepository = organizationUserRepository; _currentContext = currentContext; _featureService = featureService; + _authorizationService = authorizationService; } [HttpPost("{sponsoringOrgId}/families-for-enterprise")] @@ -84,4 +91,41 @@ public class SelfHostedOrganizationSponsorshipsController : Controller await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + + [HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")] + public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName) + { + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase)); + if (existingOrgSponsorship == null) + { + throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization."); + } + await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); + } + + [Authorize("Application")] + [HttpGet("{orgId}/sponsored")] + public async Task> GetSponsoredOrganizations(Guid orgId) + { + var sponsoringOrg = await _organizationRepository.GetByIdAsync(orgId); + if (sponsoringOrg == null) + { + throw new NotFoundException(); + } + + var authorizationResult = await _authorizationService.AuthorizeAsync(User, orgId, new ManageUsersRequirement()); + if (!authorizationResult.Succeeded) + { + throw new UnauthorizedAccessException(); + } + + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(orgId); + return new ListResponseModel( + sponsorships + .Where(s => s.IsAdminInitiated) + .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))) + ); + + } } diff --git a/src/Api/Tools/Controllers/HibpController.cs b/src/Api/Dirt/Controllers/HibpController.cs similarity index 100% rename from src/Api/Tools/Controllers/HibpController.cs rename to src/Api/Dirt/Controllers/HibpController.cs diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs similarity index 100% rename from src/Api/Tools/Controllers/ReportsController.cs rename to src/Api/Dirt/Controllers/ReportsController.cs diff --git a/src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs b/src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs similarity index 100% rename from src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs rename to src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs diff --git a/src/Api/Tools/Models/Response/MemberAccessReportModel.cs b/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs similarity index 100% rename from src/Api/Tools/Models/Response/MemberAccessReportModel.cs rename to src/Api/Dirt/Models/Response/MemberAccessReportModel.cs diff --git a/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs similarity index 82% rename from src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs rename to src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs index 5c87264c51..d927da8123 100644 --- a/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs +++ b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs @@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response; public class MemberCipherDetailsResponseModel { + public Guid? UserGuid { get; set; } public string UserName { get; set; } public string Email { get; set; } public bool UsesKeyConnector { get; set; } /// - /// A distinct list of the cipher ids associated with + /// A distinct list of the cipher ids associated with /// the organization member /// public IEnumerable CipherIds { get; set; } public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) { + this.UserGuid = memberAccessCipherDetails.UserGuid; this.UserName = memberAccessCipherDetails.UserName; this.Email = memberAccessCipherDetails.Email; this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; diff --git a/src/Api/Jobs/JobsHostedService.cs b/src/Api/Jobs/JobsHostedService.cs index acd95a0213..57b827a8be 100644 --- a/src/Api/Jobs/JobsHostedService.cs +++ b/src/Api/Jobs/JobsHostedService.cs @@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService .StartNow() .WithCronSchedule("0 0 * * * ?") .Build(); + var updatePhishingDomainsTrigger = TriggerBuilder.Create() + .WithIdentity("UpdatePhishingDomainsTrigger") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(24) + .RepeatForever()) + .Build(); var jobs = new List> @@ -68,6 +75,7 @@ public class JobsHostedService : BaseJobsHostedService new Tuple(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger), new Tuple(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger), new Tuple(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger), + new Tuple(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger), }; if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication) @@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void AddCommercialSecretsManagerJobServices(IServiceCollection services) diff --git a/src/Api/Jobs/UpdatePhishingDomainsJob.cs b/src/Api/Jobs/UpdatePhishingDomainsJob.cs new file mode 100644 index 0000000000..355f2af69b --- /dev/null +++ b/src/Api/Jobs/UpdatePhishingDomainsJob.cs @@ -0,0 +1,97 @@ +using Bit.Core; +using Bit.Core.Jobs; +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Quartz; + +namespace Bit.Api.Jobs; + +public class UpdatePhishingDomainsJob : BaseJob +{ + private readonly GlobalSettings _globalSettings; + private readonly IPhishingDomainRepository _phishingDomainRepository; + private readonly ICloudPhishingDomainQuery _cloudPhishingDomainQuery; + private readonly IFeatureService _featureService; + public UpdatePhishingDomainsJob( + GlobalSettings globalSettings, + IPhishingDomainRepository phishingDomainRepository, + ICloudPhishingDomainQuery cloudPhishingDomainQuery, + IFeatureService featureService, + ILogger logger) + : base(logger) + { + _globalSettings = globalSettings; + _phishingDomainRepository = phishingDomainRepository; + _cloudPhishingDomainQuery = cloudPhishingDomainQuery; + _featureService = featureService; + } + + protected override async Task ExecuteJobAsync(IJobExecutionContext context) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Feature flag is disabled."); + return; + } + + if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl)) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. No URL configured."); + return; + } + + if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Cloud communication is disabled in global settings."); + return; + } + + var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync(); + if (string.IsNullOrWhiteSpace(remoteChecksum)) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update."); + return; + } + + var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync(); + + if (string.Equals(currentChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation(Constants.BypassFiltersEventId, + "Phishing domains list is up to date (checksum: {Checksum}). Skipping update.", + currentChecksum); + return; + } + + _logger.LogInformation(Constants.BypassFiltersEventId, + "Checksums differ (current: {CurrentChecksum}, remote: {RemoteChecksum}). Fetching updated domains from {Source}.", + currentChecksum, remoteChecksum, _globalSettings.SelfHosted ? "Bitwarden cloud API" : "external source"); + + try + { + var domains = await _cloudPhishingDomainQuery.GetPhishingDomainsAsync(); + if (!domains.Contains("phishing.testcategory.com", StringComparer.OrdinalIgnoreCase)) + { + domains.Add("phishing.testcategory.com"); + } + + if (domains.Count > 0) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Updating {Count} phishing domains with checksum {Checksum}.", + domains.Count, remoteChecksum); + await _phishingDomainRepository.UpdatePhishingDomainsAsync(domains, remoteChecksum); + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated phishing domains."); + } + else + { + _logger.LogWarning(Constants.BypassFiltersEventId, "No valid domains found in the response. Skipping update."); + } + } + catch (Exception ex) + { + _logger.LogError(Constants.BypassFiltersEventId, ex, "Error updating phishing domains."); + } + } +} diff --git a/src/Api/KeyManagement/Validators/SendRotationValidator.cs b/src/Api/KeyManagement/Validators/SendRotationValidator.cs index c39f563b51..10a5d996b7 100644 --- a/src/Api/KeyManagement/Validators/SendRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/SendRotationValidator.cs @@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators; /// public class SendRotationValidator : IRotationValidator, IReadOnlyList> { - private readonly ISendService _sendService; + private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendRepository _sendRepository; /// /// Instantiates a new /// - /// Enables conversion of to + /// Enables conversion of to /// Retrieves all user s - public SendRotationValidator(ISendService sendService, ISendRepository sendRepository) + public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository) { - _sendService = sendService; + _sendAuthorizationService = sendAuthorizationService; _sendRepository = sendRepository; } @@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator(); } - // Slack - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } + // Add SlackService for OAuth API requests - if configured + services.AddSlackService(globalSettings); } public void Configure( diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 3b5534bed0..b18e603c0f 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -12,6 +12,8 @@ using Bit.Core.Settings; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -25,8 +27,10 @@ public class SendsController : Controller { private readonly ISendRepository _sendRepository; private readonly IUserService _userService; - private readonly ISendService _sendService; + private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; + private readonly IAnonymousSendCommand _anonymousSendCommand; + private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; @@ -34,7 +38,9 @@ public class SendsController : Controller public SendsController( ISendRepository sendRepository, IUserService userService, - ISendService sendService, + ISendAuthorizationService sendAuthorizationService, + IAnonymousSendCommand anonymousSendCommand, + INonAnonymousSendCommand nonAnonymousSendCommand, ISendFileStorageService sendFileStorageService, ILogger logger, GlobalSettings globalSettings, @@ -42,13 +48,16 @@ public class SendsController : Controller { _sendRepository = sendRepository; _userService = userService; - _sendService = sendService; + _sendAuthorizationService = sendAuthorizationService; + _anonymousSendCommand = anonymousSendCommand; + _nonAnonymousSendCommand = nonAnonymousSendCommand; _sendFileStorageService = sendFileStorageService; _logger = logger; _globalSettings = globalSettings; _currentContext = currentContext; } + #region Anonymous endpoints [AllowAnonymous] [HttpPost("access/{id}")] public async Task Access(string id, [FromBody] SendAccessRequestModel model) @@ -61,18 +70,19 @@ public class SendsController : Controller //} var guid = new Guid(CoreHelpers.Base64UrlDecode(id)); - var (send, passwordRequired, passwordInvalid) = - await _sendService.AccessAsync(guid, model.Password); - if (passwordRequired) + var send = await _sendRepository.GetByIdAsync(guid); + SendAccessResult sendAuthResult = + await _sendAuthorizationService.AccessAsync(send, model.Password); + if (sendAuthResult.Equals(SendAccessResult.PasswordRequired)) { return new UnauthorizedResult(); } - if (passwordInvalid) + if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } - if (send == null) + if (sendAuthResult.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); } @@ -106,19 +116,19 @@ public class SendsController : Controller throw new BadRequestException("Could not locate send"); } - var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId, + var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, model.Password); - if (passwordRequired) + if (result.Equals(SendAccessResult.PasswordRequired)) { return new UnauthorizedResult(); } - if (passwordInvalid) + if (result.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } - if (send == null) + if (result.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); } @@ -130,6 +140,45 @@ public class SendsController : Controller }); } + [AllowAnonymous] + [HttpPost("file/validate/azure")] + public async Task AzureValidateFile() + { + return await ApiHelpers.HandleAzureEvents(Request, new Dictionary> + { + { + "Microsoft.Storage.BlobCreated", async (eventGridEvent) => + { + try + { + var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; + var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName); + var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); + if (send == null) + { + if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService) + { + await azureSendFileStorageService.DeleteBlobAsync(blobName); + } + return; + } + + await _nonAnonymousSendCommand.ConfirmFileSize(send); + } + catch (Exception e) + { + _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}"); + return; + } + } + } + }); + } + + #endregion + + #region Non-anonymous endpoints + [HttpGet("{id}")] public async Task Get(string id) { @@ -157,8 +206,8 @@ public class SendsController : Controller { model.ValidateCreation(); var userId = _userService.GetProperUserId(User).Value; - var send = model.ToSend(userId, _sendService); - await _sendService.SaveSendAsync(send); + var send = model.ToSend(userId, _sendAuthorizationService); + await _nonAnonymousSendCommand.SaveSendAsync(send); return new SendResponseModel(send, _globalSettings); } @@ -175,15 +224,15 @@ public class SendsController : Controller throw new BadRequestException("Invalid content. File size hint is required."); } - if (model.FileLength.Value > SendService.MAX_FILE_SIZE) + if (model.FileLength.Value > Constants.FileSize501mb) { - throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}."); + throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}."); } model.ValidateCreation(); var userId = _userService.GetProperUserId(User).Value; - var (send, data) = model.ToSend(userId, model.File.FileName, _sendService); - var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value); + var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService); + var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value); return new SendFileUploadDataResponseModel { Url = uploadUrl, @@ -230,41 +279,7 @@ public class SendsController : Controller var send = await _sendRepository.GetByIdAsync(new Guid(id)); await Request.GetFileAsync(async (stream) => { - await _sendService.UploadFileToExistingSendAsync(stream, send); - }); - } - - [AllowAnonymous] - [HttpPost("file/validate/azure")] - public async Task AzureValidateFile() - { - return await ApiHelpers.HandleAzureEvents(Request, new Dictionary> - { - { - "Microsoft.Storage.BlobCreated", async (eventGridEvent) => - { - try - { - var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; - var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName); - var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); - if (send == null) - { - if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService) - { - await azureSendFileStorageService.DeleteBlobAsync(blobName); - } - return; - } - await _sendService.ValidateSendFile(send); - } - catch (Exception e) - { - _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}"); - return; - } - } - } + await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); }); } @@ -279,7 +294,7 @@ public class SendsController : Controller throw new NotFoundException(); } - await _sendService.SaveSendAsync(model.ToSend(send, _sendService)); + await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService)); return new SendResponseModel(send, _globalSettings); } @@ -294,7 +309,7 @@ public class SendsController : Controller } send.Password = null; - await _sendService.SaveSendAsync(send); + await _nonAnonymousSendCommand.SaveSendAsync(send); return new SendResponseModel(send, _globalSettings); } @@ -308,6 +323,8 @@ public class SendsController : Controller throw new NotFoundException(); } - await _sendService.DeleteSendAsync(send); + await _nonAnonymousSendCommand.DeleteSendAsync(send); } + + #endregion } diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index 660ff41e3a..5b3fd7ba31 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -36,31 +36,31 @@ public class SendRequestModel public bool? Disabled { get; set; } public bool? HideEmail { get; set; } - public Send ToSend(Guid userId, ISendService sendService) + public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService) { var send = new Send { Type = Type, UserId = (Guid?)userId }; - ToSend(send, sendService); + ToSend(send, sendAuthorizationService); return send; } - public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendService sendService) + public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService) { var send = ToSendBase(new Send { Type = Type, UserId = (Guid?)userId - }, sendService); + }, sendAuthorizationService); var data = new SendFileData(Name, Notes, fileName); return (send, data); } - public Send ToSend(Send existingSend, ISendService sendService) + public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService) { - existingSend = ToSendBase(existingSend, sendService); + existingSend = ToSendBase(existingSend, sendAuthorizationService); switch (existingSend.Type) { case SendType.File: @@ -125,7 +125,7 @@ public class SendRequestModel } } - private Send ToSendBase(Send existingSend, ISendService sendService) + private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService) { existingSend.Key = Key; existingSend.ExpirationDate = ExpirationDate; @@ -133,7 +133,7 @@ public class SendRequestModel existingSend.MaxAccessCount = MaxAccessCount; if (!string.IsNullOrWhiteSpace(Password)) { - existingSend.Password = sendService.HashPassword(Password); + existingSend.Password = authorizationService.HashPassword(Password); } existingSend.Disabled = Disabled.GetValueOrDefault(); existingSend.HideEmail = HideEmail.GetValueOrDefault(); diff --git a/src/Api/Utilities/CommandResultExtensions.cs b/src/Api/Utilities/CommandResultExtensions.cs deleted file mode 100644 index c7315a0fa0..0000000000 --- a/src/Api/Utilities/CommandResultExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -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.Value) { 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/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 741297259b..51ac909e22 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -3,6 +3,10 @@ using Bit.Api.Tools.Authorization; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.IdentityServer; +using Bit.Core.PhishingDomainFeatures; +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Repositories.Implementations; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Core.Vault.Authorization.SecurityTasks; @@ -110,4 +114,25 @@ public static class ServiceCollectionExtensions services.AddScoped(); // services.AddScoped(); } + + public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddHttpClient("PhishingDomains", client => + { + client.DefaultRequestHeaders.Add("User-Agent", globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden"); + client.Timeout = TimeSpan.FromSeconds(1000); // the source list is very slow + }); + + services.AddSingleton(); + services.AddSingleton(); + + if (globalSettings.SelfHosted) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } + } } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 3bdb6c4bf0..251362589e 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -151,6 +151,16 @@ public class CiphersController : Controller public async Task Post([FromBody] CipherRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); + + // Validate the model was encrypted for the posting user + if (model.EncryptedFor != null) + { + if (model.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + var cipher = model.ToCipherDetails(user.Id); if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { @@ -170,6 +180,16 @@ public class CiphersController : Controller public async Task PostCreate([FromBody] CipherCreateRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); + + // Validate the model was encrypted for the posting user + if (model.Cipher.EncryptedFor != null) + { + if (model.Cipher.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + var cipher = model.Cipher.ToCipherDetails(user.Id); if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { @@ -192,6 +212,16 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + + // Validate the model was encrypted for the posting user + if (model.Cipher.EncryptedFor != null) + { + if (model.Cipher.EncryptedFor != userId) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false); var response = new CipherMiniResponseModel(cipher, _globalSettings, false); @@ -209,6 +239,15 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Validate the model was encrypted for the posting user + if (model.EncryptedFor != null) + { + if (model.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + ValidateClientVersionForFido2CredentialSupport(cipher); var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList(); @@ -237,6 +276,15 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); + // Validate the model was encrypted for the posting user + if (model.EncryptedFor != null) + { + if (model.EncryptedFor != userId) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + ValidateClientVersionForFido2CredentialSupport(cipher); if (cipher == null || !cipher.OrganizationId.HasValue || @@ -315,26 +363,10 @@ public class CiphersController : Controller { var org = _currentContext.GetOrganization(organizationId); - // If we're not an "admin", we don't need to check the ciphers - if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })) + // If we're not an "admin" or if we're not a provider user we don't need to check the ciphers + if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId)) { - // Are we a provider user? If so, we need to be sure we're not restricted - // Once the feature flag is removed, this check can be combined with the above - if (await _currentContext.ProviderUserForOrgAsync(organizationId)) - { - // Provider is restricted from editing ciphers, so we're not an "admin" - if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess)) - { - return false; - } - - // Provider is unrestricted, so we're an "admin", don't return early - } - else - { - // Not a provider or admin - return false; - } + return false; } // We know we're an "admin", now check the ciphers explicitly (in case admins are restricted) @@ -350,26 +382,10 @@ public class CiphersController : Controller var org = _currentContext.GetOrganization(organizationId); - // If we're not an "admin", we don't need to check the ciphers - if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })) + // If we're not an "admin" or if we're a provider user we don't need to check the ciphers + if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId)) { - // Are we a provider user? If so, we need to be sure we're not restricted - // Once the feature flag is removed, this check can be combined with the above - if (await _currentContext.ProviderUserForOrgAsync(organizationId)) - { - // Provider is restricted from editing ciphers, so we're not an "admin" - if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess)) - { - return false; - } - - // Provider is unrestricted, so we're an "admin", don't return early - } - else - { - // Not a provider or admin - return false; - } + return false; } // If the user can edit all ciphers for the organization, just check they all belong to the org @@ -462,10 +478,10 @@ public class CiphersController : Controller return true; } - // Provider users can edit all ciphers if RestrictProviderAccess is disabled + // Provider users cannot edit ciphers if (await _currentContext.ProviderUserForOrgAsync(organizationId)) { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + return false; } return false; @@ -485,10 +501,10 @@ public class CiphersController : Controller return true; } - // Provider users can only access organization ciphers if RestrictProviderAccess is disabled + // Provider users cannot access organization ciphers if (await _currentContext.ProviderUserForOrgAsync(organizationId)) { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + return false; } return false; @@ -508,10 +524,10 @@ public class CiphersController : Controller return true; } - // Provider users can only access all ciphers if RestrictProviderAccess is disabled + // Provider users cannot access ciphers if (await _currentContext.ProviderUserForOrgAsync(organizationId)) { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + return false; } return false; @@ -690,6 +706,15 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Validate the model was encrypted for the posting user + if (model.Cipher.EncryptedFor != null) + { + if (model.Cipher.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + ValidateClientVersionForFido2CredentialSupport(cipher); var original = cipher.Clone(); @@ -1051,6 +1076,18 @@ public class CiphersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphersDict = ciphers.ToDictionary(c => c.Id); + // Validate the model was encrypted for the posting user + foreach (var cipher in model.Ciphers) + { + if (cipher.EncryptedFor != null) + { + if (cipher.EncryptedFor != userId) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + } + var shareCiphers = new List<(Cipher, DateTime?)>(); foreach (var cipher in model.Ciphers) { @@ -1086,9 +1123,8 @@ public class CiphersController : Controller throw new BadRequestException(ModelState); } - // If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) + // Check if the user is claimed by any organization. + if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) { throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); } @@ -1241,6 +1277,20 @@ public class CiphersController : Controller return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); } + [HttpGet("{id}/attachment/{attachmentId}/admin")] + public async Task GetAttachmentDataAdmin(Guid id, string attachmentId) + { + var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); + if (cipher == null || !cipher.OrganizationId.HasValue || + !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) + { + throw new NotFoundException(); + } + + var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId); + return new AttachmentResponseModel(result); + } + [HttpGet("{id}/attachment/{attachmentId}")] public async Task GetAttachmentData(Guid id, string attachmentId) { @@ -1287,18 +1337,17 @@ public class CiphersController : Controller [HttpDelete("{id}/attachment/{attachmentId}/admin")] [HttpPost("{id}/attachment/{attachmentId}/delete-admin")] - public async Task DeleteAttachmentAdmin(string id, string attachmentId) + public async Task DeleteAttachmentAdmin(Guid id, string attachmentId) { - var idGuid = new Guid(id); var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(idGuid); + var cipher = await _cipherRepository.GetByIdAsync(id); if (cipher == null || !cipher.OrganizationId.HasValue || !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) { throw new NotFoundException(); } - await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true); + return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true); } [AllowAnonymous] diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 4b66c7f2bd..568c05d651 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -3,6 +3,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -37,6 +38,7 @@ public class SyncController : Controller private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; public SyncController( IUserService userService, @@ -51,7 +53,8 @@ public class SyncController : Controller GlobalSettings globalSettings, ICurrentContext currentContext, IFeatureService featureService, - IApplicationCacheService applicationCacheService) + IApplicationCacheService applicationCacheService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) { _userService = userService; _folderRepository = folderRepository; @@ -66,6 +69,7 @@ public class SyncController : Controller _currentContext = currentContext; _featureService = featureService; _applicationCacheService = applicationCacheService; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; } [HttpGet("")] @@ -102,7 +106,7 @@ public class SyncController : Controller collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); } - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 89eda415b1..5c288ab66d 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request; public class CipherRequestModel { + /// + /// The Id of the user that encrypted the cipher. It should always represent a UserId. + /// + public Guid? EncryptedFor { get; set; } public CipherType Type { get; set; } [StringLength(36)] diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 2f33d87ae8..82fb951261 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -37,6 +37,10 @@ }, "storage": { "connectionString": "UseDevelopmentStorage=true" + }, + "phishingDomain": { + "updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt", + "checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256" } } } diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json index 98b210cb1e..f8a69dcfac 100644 --- a/src/Api/appsettings.json +++ b/src/Api/appsettings.json @@ -71,6 +71,9 @@ "accessKeySecret": "SECRET", "region": "SECRET" }, + "phishingDomain": { + "updateUrl": "SECRET" + }, "distributedIpRateLimiting": { "enabled": true, "maxRedisTimeoutsThreshold": 10, diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 4bf6b7bad4..1fb0fb7ac7 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -63,6 +63,12 @@ public class FreshdeskController : Controller note += $"
  • Region: {_billingSettings.FreshDesk.Region}
  • "; var customFields = new Dictionary(); var user = await _userRepository.GetByEmailAsync(ticketContactEmail); + if (user == null) + { + note += $"
  • No user found: {ticketContactEmail}
  • "; + await CreateNote(ticketId, note); + } + if (user != null) { var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"; @@ -121,18 +127,7 @@ public class FreshdeskController : Controller Content = JsonContent.Create(updateBody), }; await CallFreshdeskApiAsync(updateRequest); - - var noteBody = new Dictionary - { - { "body", $"
      {note}
    " }, - { "private", true } - }; - var noteRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) - { - Content = JsonContent.Create(noteBody), - }; - await CallFreshdeskApiAsync(noteRequest); + await CreateNote(ticketId, note); } return new OkResult(); @@ -208,6 +203,21 @@ public class FreshdeskController : Controller return true; } + private async Task CreateNote(string ticketId, string note) + { + var noteBody = new Dictionary + { + { "body", $"
      {note}
    " }, + { "private", true } + }; + var noteRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) + { + Content = JsonContent.Create(noteBody), + }; + await CallFreshdeskApiAsync(noteRequest); + } + private async Task AddAnswerNoteToTicketAsync(string note, string ticketId) { // if there is no content, then we don't need to add a note diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 4e35a6c894..1f6ef741df 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,8 +1,8 @@ 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.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Stripe; diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index f75cbf8a8b..e31d1dceb7 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,11 +1,11 @@ using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler( IStripeEventService stripeEventService, IStripeEventUtilityService stripeEventUtilityService, IUserRepository userRepository, - IValidateSponsorshipCommand validateSponsorshipCommand, - IAutomaticTaxFactory automaticTaxFactory) + IValidateSponsorshipCommand validateSponsorshipCommand) : IUpcomingInvoiceHandler { public async Task HandleAsync(Event parsedEvent) @@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler( var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + if (organizationId.HasValue) { var organization = await organizationRepository.GetByIdAsync(organizationId.Value); @@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler( return; } - await TryEnableAutomaticTaxAsync(subscription); + await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); @@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler( return; } - await TryEnableAutomaticTaxAsync(subscription); + if (!subscription.AutomaticTax.Enabled && subscription.Customer.HasRecognizedTaxLocation()) + { + try + { + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set user's ({UserID}) subscription to automatic tax while processing event with ID {EventID}", + user.Id, + parsedEvent.Id); + } + } if (user.Premium) { @@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler( return; } - await TryEnableAutomaticTaxAsync(subscription); + await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge); await SendUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice); } @@ -139,50 +158,123 @@ public class UpcomingInvoiceHandler( } } - private async Task TryEnableAutomaticTaxAsync(Subscription subscription) + private async Task AlignOrganizationTaxConcernsAsync( + Organization organization, + Subscription subscription, + string eventId, + bool setNonUSBusinessUseToReverseCharge) { - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id)); - var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters); - var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription); + var nonUSBusinessUse = + organization.PlanType.GetProductTier() != ProductTierType.Families && + subscription.Customer.Address.Country != "US"; - if (updateOptions == null) + bool setAutomaticTaxToEnabled; + + if (setNonUSBusinessUseToReverseCharge) + { + if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { - return; + try + { + await stripeFacade.UpdateCustomer(subscription.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}", + organization.Id, + eventId); + } } - await stripeFacade.UpdateSubscription(subscription.Id, updateOptions); - return; + setAutomaticTaxToEnabled = true; } - - if (subscription.AutomaticTax.Enabled || - !subscription.Customer.HasBillingLocation() || - await IsNonTaxableNonUSBusinessUseSubscription(subscription)) + else { - return; + setAutomaticTaxToEnabled = + subscription.Customer.HasRecognizedTaxLocation() && + (subscription.Customer.Address.Country == "US" || + (nonUSBusinessUse && subscription.Customer.TaxIds.Any())); } - await stripeFacade.UpdateSubscription(subscription.Id, - new SubscriptionUpdateOptions + if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled) + { + try { - DefaultTaxRates = [], - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }); + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set organization's ({OrganizationID}) subscription to automatic tax while processing event with ID {EventID}", + organization.Id, + eventId); + } + } + } - return; + private async Task AlignProviderTaxConcernsAsync( + Provider provider, + Subscription subscription, + string eventId, + bool setNonUSBusinessUseToReverseCharge) + { + bool setAutomaticTaxToEnabled; - async Task IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) + if (setNonUSBusinessUseToReverseCharge) { - var familyPriceIds = (await Task.WhenAll( - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) - .Select(plan => plan.PasswordManager.StripePlanId); + if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) + { + try + { + await stripeFacade.UpdateCustomer(subscription.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}", + provider.Id, + eventId); + } + } - 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(); + setAutomaticTaxToEnabled = true; + } + else + { + setAutomaticTaxToEnabled = + subscription.Customer.HasRecognizedTaxLocation() && + (subscription.Customer.Address.Country == "US" || + subscription.Customer.TaxIds.Any()); + } + + if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled) + { + try + { + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set provider's ({ProviderID}) subscription to automatic tax while processing event with ID {EventID}", + provider.Id, + eventId); + } } } } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 17d9847574..e649406bb0 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -114,6 +114,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, /// public bool UseRiskInsights { get; set; } + /// + /// If true, the organization can claim domains, which unlocks additional enterprise features + /// + public bool UseOrganizationDomains { get; set; } + /// /// If set to true, admins can initiate organization-issued sponsorships. /// @@ -319,5 +324,7 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, SmSeats = license.SmSeats; SmServiceAccounts = license.SmServiceAccounts; UseRiskInsights = license.UseRiskInsights; + UseOrganizationDomains = license.UseOrganizationDomains; + UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies; } } diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs index 0f5123554e..5edd54df23 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/AdminConsole/Enums/IntegrationType.cs @@ -7,3 +7,19 @@ public enum IntegrationType : int Slack = 3, Webhook = 4, } + +public static class IntegrationTypeExtensions +{ + public static string ToRoutingKey(this IntegrationType type) + { + switch (type) + { + case IntegrationType.Slack: + return "slack"; + case IntegrationType.Webhook: + return "webhook"; + default: + throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}"); + } + } +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs new file mode 100644 index 0000000000..bd1f280cad --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public interface IIntegrationMessage +{ + IntegrationType IntegrationType { get; } + int RetryCount { get; set; } + DateTime? DelayUntilDate { get; set; } + void ApplyRetry(DateTime? handlerDelayUntilDate); + string ToJson(); +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs new file mode 100644 index 0000000000..d2f0bde693 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs @@ -0,0 +1,16 @@ +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public class IntegrationHandlerResult +{ + public IntegrationHandlerResult(bool success, IIntegrationMessage message) + { + Success = success; + Message = message; + } + + public bool Success { get; set; } = false; + public bool Retryable { get; set; } = false; + public IIntegrationMessage Message { get; set; } + public DateTime? DelayUntilDate { get; set; } + public string FailureReason { get; set; } = string.Empty; +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs new file mode 100644 index 0000000000..1f288914d0 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public class IntegrationMessage : IIntegrationMessage +{ + public IntegrationType IntegrationType { get; set; } + public T Configuration { get; set; } + public string RenderedTemplate { get; set; } + public int RetryCount { get; set; } = 0; + public DateTime? DelayUntilDate { get; set; } + + public void ApplyRetry(DateTime? handlerDelayUntilDate) + { + RetryCount++; + + var baseTime = handlerDelayUntilDate ?? DateTime.UtcNow; + var backoffSeconds = Math.Pow(2, RetryCount); + var jitterSeconds = Random.Shared.Next(0, 3); + + DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds); + } + + public string ToJson() + { + return JsonSerializer.Serialize(this); + } + + public static IntegrationMessage FromJson(string json) + { + return JsonSerializer.Deserialize>(json); + } +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs new file mode 100644 index 0000000000..338c2b963d --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs @@ -0,0 +1,38 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public class IntegrationTemplateContext(EventMessage eventMessage) +{ + public EventMessage Event { get; } = eventMessage; + + public string DomainName => Event.DomainName; + public string IpAddress => Event.IpAddress; + public DeviceType? DeviceType => Event.DeviceType; + public Guid? ActingUserId => Event.ActingUserId; + public Guid? OrganizationUserId => Event.OrganizationUserId; + public DateTime Date => Event.Date; + public EventType Type => Event.Type; + public Guid? UserId => Event.UserId; + public Guid? OrganizationId => Event.OrganizationId; + public Guid? CipherId => Event.CipherId; + public Guid? CollectionId => Event.CollectionId; + public Guid? GroupId => Event.GroupId; + public Guid? PolicyId => Event.PolicyId; + + public User? User { get; set; } + public string? UserName => User?.Name; + public string? UserEmail => User?.Email; + + public User? ActingUser { get; set; } + public string? ActingUserName => ActingUser?.Name; + public string? ActingUserEmail => ActingUser?.Email; + + public Organization? Organization { get; set; } + public string? OrganizationName => Organization?.DisplayName(); +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs index e6fc1440ea..4fcce542ce 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegration(string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs index ad25d35e7e..2930004cbf 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfiguration(string channelId); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs index 49ca9df4e0..b81e50d403 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfigurationDetails(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs index 9a7591f24b..e8217d3ad3 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfiguration(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs new file mode 100644 index 0000000000..e3e92c900f --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public record WebhookIntegrationConfigurationDetails(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs deleted file mode 100644 index f165828de0..0000000000 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.Models.Data.Integrations; - -public record WebhookIntegrationConfigurationDetils(string url); diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index d27bf40994..ae91f204e3 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -26,6 +26,7 @@ public class OrganizationAbility LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; } @@ -46,5 +47,6 @@ public class OrganizationAbility public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 0771457d0a..8de422ee31 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -59,5 +59,7 @@ public class OrganizationUserOrganizationDetails public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool? IsAdminInitiated { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index ab2dfd7e0e..a6ad47f829 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -150,6 +150,7 @@ public class SelfHostedOrganizationDetails : Organization AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, Status = Status, UseRiskInsights = UseRiskInsights, + UseAdminSponsoredFamilies = UseAdminSponsoredFamilies, }; } } diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index 8717a8f008..4621de8268 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -45,6 +45,7 @@ public class ProviderUserOrganizationDetails public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public ProviderType ProviderType { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index ec635282f7..43a3120ffd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand( IDnsResolverService dnsResolverService, IEventService eventService, IGlobalSettings globalSettings, - IFeatureService featureService, ICurrentContext currentContext, ISavePolicyCommand savePolicyCommand, IMailService mailService, @@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand( private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser) { - if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); - await SendVerifiedDomainUserEmailAsync(domain); - } + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); + await SendVerifiedDomainUserEmailAsync(domain); } private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) => diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 756bd2ae46..3770d867cf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,6 +1,9 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -24,7 +27,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IPolicyService _policyService; private readonly IMailService _mailService; private readonly IUserRepository _userRepository; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IFeatureService _featureService; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public AcceptOrgUserCommand( IDataProtectionProvider dataProtectionProvider, @@ -34,9 +40,11 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand IPolicyService policyService, IMailService mailService, IUserRepository userRepository, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IFeatureService featureService, + IPolicyRequirementQuery policyRequirementQuery) { - // TODO: remove data protector when old token validation removed _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); _globalSettings = globalSettings; @@ -45,7 +53,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand _policyService = policyService; _mailService = mailService; _userRepository = userRepository; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _featureService = featureService; + _policyRequirementQuery = policyRequirementQuery; } public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, @@ -192,15 +203,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand } // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!await userService.TwoFactorIsEnabledAsync(user)) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); - } - } + await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId); orgUser.Status = OrganizationUserStatusType.Accepted; orgUser.UserId = user.Id; @@ -220,4 +223,33 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand return orgUser; } + private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId) + { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) + { + // If the user has two-step login enabled, we skip checking the 2FA policy + return; + } + + var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync(user.Id); + if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + + return; + } + + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) + { + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); + if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + } + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 9bfe8f791e..806cf5a533 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -24,6 +26,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand private readonly IPushRegistrationService _pushRegistrationService; private readonly IPolicyService _policyService; private readonly IDeviceRepository _deviceRepository; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; public ConfirmOrganizationUserCommand( IOrganizationRepository organizationRepository, @@ -35,7 +39,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand IPushNotificationService pushNotificationService, IPushRegistrationService pushRegistrationService, IPolicyService policyService, - IDeviceRepository deviceRepository) + IDeviceRepository deviceRepository, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -47,6 +53,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand _pushRegistrationService = pushRegistrationService; _policyService = policyService; _deviceRepository = deviceRepository; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, @@ -118,8 +126,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } } - var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; - await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled); + var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; + await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled); orgUser.Status = OrganizationUserStatusType.Confirmed; orgUser.Key = keys[orgUser.Id]; orgUser.Email = null; @@ -142,15 +150,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } private async Task CheckPoliciesAsync(Guid organizationId, User user, - ICollection userOrgs, bool twoFactorEnabled) + ICollection userOrgs, bool userTwoFactorEnabled) { // Enforce Two Factor Authentication Policy for this organization - var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)) - .Any(p => p.OrganizationId == organizationId); - if (orgRequiresTwoFactor && !twoFactorEnabled) - { - throw new BadRequestException("User does not have two-step login enabled."); - } + await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled); var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId); var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); @@ -168,6 +171,33 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } } + private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled) + { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + if (userTwoFactorEnabled) + { + // If the user has two-step login enabled, we skip checking the 2FA policy + return; + } + + var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync(user.Id); + if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId)) + { + throw new BadRequestException("User does not have two-step login enabled."); + } + + return; + } + + var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)) + .Any(p => p.OrganizationId == organizationId); + if (orgRequiresTwoFactor && !userTwoFactorEnabled) + { + throw new BadRequestException("User does not have two-step login enabled."); + } + } + private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) { var devices = await GetUserDeviceIdsAsync(userId); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs index 1dda9483cd..d8c510119a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs @@ -24,9 +24,7 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim // Users can only be claimed by an Organization that is enabled and can have organization domains var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId); - // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). - // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - if (organizationAbility is { Enabled: true, UseSso: true }) + if (organizationAbility is { Enabled: true, UseOrganizationDomains: true }) { // Get all organization users with claimed domains by the organization var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs index 302ee0901d..e574d29e48 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs index 8494a6d4ca..59162230da 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs @@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; public interface IOrganizationUserUserDetailsQuery { Task> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request); + + Task> Get(OrganizationUserUserDetailsQueryRequest request); + + Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs index c9768a8905..024d56e8c3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs index c66d366de5..38fa35b29a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs index 810ef744c9..48faf4cac0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs index 52697572e6..8cd70391a2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs index 475ad4a886..4fbb8f2bad 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs index 3e4c7652a5..7e0a8dc3cd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 662ed314ce..072bc5fc05 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -1,17 +1,17 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Commands; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -50,11 +50,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService, { case Failure failure: return new Failure( - failure.Errors.Select(error => new Error(error.Message, + new Error(failure.Error.Message, new ScimInviteOrganizationUsersResponse { - InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault() - }))); + InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault() + })); case Success success when success.Value.InvitedUsers.Any(): var user = success.Value.InvitedUsers.First(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs index 0624ffe027..e7e331686d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs index fd0441753a..fb50fd58dd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 79a3487d19..54f26cb46a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -1,7 +1,7 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs index 5d072ca17d..f9e9f4eebf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs index 9e2ca8d9a6..ce617a2db3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Models.Business; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs index 6ff7181456..40afa5e9d0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs index 6a8ec8e6d3..a1536ad439 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs @@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs index c74d1048ad..865a3cb83a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs index cc17a673f9..496dddc916 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs index 104ce5cc7e..759ac1b780 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs index f84b25f76f..eeb19eec98 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Extensions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs index 22fce08021..587e04826b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs @@ -1,5 +1,9 @@ -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Utilities; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; @@ -9,12 +13,21 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery { private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; public OrganizationUserUserDetailsQuery( - IOrganizationUserRepository organizationUserRepository + IOrganizationUserRepository organizationUserRepository, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IFeatureService featureService, + IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery ) { _organizationUserRepository = organizationUserRepository; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; } /// @@ -37,4 +50,42 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer return o; }); } + + /// + /// Get the organization user user details, two factor enabled status, and + /// claimed status for the provided request. + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> Get(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = await GetOrganizationUserUserDetails(request); + + var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); + var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers.Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); + + + return responses; + } + + /// + /// Get the organization users user details, two factor enabled status, and + /// claimed status for confirmed users that are enrolled in account recovery + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = (await GetOrganizationUserUserDetails(request)) + .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey)); + + var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); + var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers + .Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); + + return responses; + } + } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index 4de2cd0ea5..00d3ebb533 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand throw new BadRequestException(RemoveAdminByCustomUserErrorMessage); } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null) + if (deletingUserId.HasValue && eventSystemUser == null) { var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id }); if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed) @@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); } - var claimedStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null + var claimedStatus = deletingUserId.HasValue && eventSystemUser == null ? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id)) : filteredUsers.ToDictionary(u => u.Id, u => false); var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs index 74165a5a71..fe19cd1389 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -22,7 +24,9 @@ public class RestoreOrganizationUserCommand( ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IPolicyService policyService, IUserRepository userRepository, - IOrganizationService organizationService) : IRestoreOrganizationUserCommand + IOrganizationService organizationService, + IFeatureService featureService, + IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand { public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId) { @@ -270,12 +274,7 @@ public class RestoreOrganizationUserCommand( // Enforce 2FA Policy of organization user is trying to join if (!userHasTwoFactorEnabled) { - var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - twoFactorCompliant = false; - } + twoFactorCompliant = !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId); } var user = await userRepository.GetByIdAsync(userId); @@ -299,4 +298,17 @@ public class RestoreOrganizationUserCommand( throw new BadRequestException(user.Email + " is not compliant with the two-step login policy"); } } + + private async Task IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId) + { + if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var requirement = await policyRequirementQuery.GetAsync(userId); + return requirement.IsTwoFactorRequiredForOrganization(organizationId); + } + + var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); + return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs index 971ed02b29..0773cf4f9c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Enums; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 60e090de2a..7449628ed0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -104,7 +104,8 @@ public class CloudOrganizationSignUpCommand( RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, UsePasswordManager = true, - UseSecretsManager = signup.UseSecretsManager + UseSecretsManager = signup.UseSecretsManager, + UseOrganizationDomains = plan.HasOrganizationDomains, }; if (signup.UseSecretsManager) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs new file mode 100644 index 0000000000..3e060c66a5 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs @@ -0,0 +1,128 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.DataProtection; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand +{ + + private readonly IOrganizationService _organizationService; + private readonly ICollectionRepository _collectionRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IDataProtector _dataProtector; + private readonly IGlobalSettings _globalSettings; + private readonly IPolicyService _policyService; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public InitPendingOrganizationCommand( + IOrganizationService organizationService, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IDataProtectionProvider dataProtectionProvider, + IGlobalSettings globalSettings, + IPolicyService policyService, + IOrganizationUserRepository organizationUserRepository + ) + { + _organizationService = organizationService; + _collectionRepository = collectionRepository; + _organizationRepository = organizationRepository; + _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); + _globalSettings = globalSettings; + _policyService = policyService; + _organizationUserRepository = organizationUserRepository; + } + + public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken) + { + await ValidateSignUpPoliciesAsync(user.Id); + + var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + if (orgUser == null) + { + throw new BadRequestException("User invalid."); + } + + var tokenValid = ValidateInviteToken(orgUser, user, emailToken); + + if (!tokenValid) + { + throw new BadRequestException("Invalid token"); + } + + var org = await _organizationRepository.GetByIdAsync(organizationId); + + if (org.Enabled) + { + throw new BadRequestException("Organization is already enabled."); + } + + if (org.Status != OrganizationStatusType.Pending) + { + throw new BadRequestException("Organization is not on a Pending status."); + } + + if (!string.IsNullOrEmpty(org.PublicKey)) + { + throw new BadRequestException("Organization already has a Public Key."); + } + + if (!string.IsNullOrEmpty(org.PrivateKey)) + { + throw new BadRequestException("Organization already has a Private Key."); + } + + org.Enabled = true; + org.Status = OrganizationStatusType.Created; + org.PublicKey = publicKey; + org.PrivateKey = privateKey; + + await _organizationService.UpdateAsync(org); + + if (!string.IsNullOrWhiteSpace(collectionName)) + { + // give the owner Can Manage access over the default collection + List defaultOwnerAccess = + [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]; + + var defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = org.Id + }; + await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + } + + private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + { + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."); + } + } + + private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken) + { + var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( + _orgUserInviteTokenDataFactory, emailToken, orgUser); + + return tokenValid; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs new file mode 100644 index 0000000000..273182664e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.Entities; +namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IInitPendingOrganizationCommand +{ + /// + /// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'. + /// + /// + /// This method must target a disabled Organization that has null keys and status as 'Pending'. + /// + Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..b8802ffd0c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -0,0 +1,187 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Pricing; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +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.AdminConsole.OrganizationFeatures.Organizations; + +public record ProviderClientOrganizationSignUpResponse( + Organization Organization, + Collection DefaultCollection); + +public interface IProviderClientOrganizationSignUpCommand +{ + /// + /// Sign up a new client organization for a provider. + /// + /// The signup information. + /// A tuple containing the new organization and its default collection. + Task SignUpClientOrganizationAsync(OrganizationSignup signup); +} + +public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizationSignUpCommand +{ + public const string PlanNullErrorMessage = "Password Manager Plan was null."; + public const string PlanDisabledErrorMessage = "Password Manager Plan is disabled."; + public const string AdditionalSeatsNegativeErrorMessage = "You can't subtract Password Manager seats!"; + + private readonly ICurrentContext _currentContext; + private readonly IPricingClient _pricingClient; + private readonly IReferenceEventService _referenceEventService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly IApplicationCacheService _applicationCacheService; + private readonly ICollectionRepository _collectionRepository; + + public ProviderClientOrganizationSignUpCommand( + ICurrentContext currentContext, + IPricingClient pricingClient, + IReferenceEventService referenceEventService, + IOrganizationRepository organizationRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService, + ICollectionRepository collectionRepository) + { + _currentContext = currentContext; + _pricingClient = pricingClient; + _referenceEventService = referenceEventService; + _organizationRepository = organizationRepository; + _organizationApiKeyRepository = organizationApiKeyRepository; + _applicationCacheService = applicationCacheService; + _collectionRepository = collectionRepository; + } + + public async Task SignUpClientOrganizationAsync(OrganizationSignup signup) + { + var plan = await _pricingClient.GetPlanOrThrow(signup.Plan); + + ValidatePlan(plan, signup.AdditionalSeats); + + var organization = new Organization + { + // Pre-generate the org id so that we can save it with the Stripe subscription. + Id = CoreHelpers.GenerateComb(), + Name = signup.Name, + BillingEmail = signup.BillingEmail, + PlanType = plan!.Type, + Seats = signup.AdditionalSeats, + MaxCollections = plan.PasswordManager.MaxCollections, + MaxStorageGb = 1, + UsePolicies = plan.HasPolicies, + UseSso = plan.HasSso, + UseOrganizationDomains = plan.HasOrganizationDomains, + UseGroups = plan.HasGroups, + UseEvents = plan.HasEvents, + UseDirectory = plan.HasDirectory, + UseTotp = plan.HasTotp, + Use2fa = plan.Has2fa, + UseApi = plan.HasApi, + UseResetPassword = plan.HasResetPassword, + SelfHost = plan.HasSelfHost, + UsersGetPremium = plan.UsersGetPremium, + UseCustomPermissions = plan.HasCustomPermissions, + UseScim = plan.HasScim, + Plan = plan.Name, + Gateway = GatewayType.Stripe, + ReferenceData = signup.Owner.ReferenceData, + Enabled = true, + LicenseKey = CoreHelpers.SecureRandomString(20), + PublicKey = signup.PublicKey, + PrivateKey = signup.PrivateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = true, + // Secrets Manager not available for purchase with Consolidated Billing. + UseSecretsManager = false, + }; + + var returnValue = await SignUpAsync(organization, signup.CollectionName); + + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) + { + PlanName = plan.Name, + PlanType = plan.Type, + Seats = returnValue.Organization.Seats, + SignupInitiationPath = signup.InitiationPath, + Storage = returnValue.Organization.MaxStorageGb, + }); + + return returnValue; + } + + private static void ValidatePlan(Plan plan, int additionalSeats) + { + if (plan is null) + { + throw new BadRequestException(PlanNullErrorMessage); + } + + if (plan.Disabled) + { + throw new BadRequestException(PlanDisabledErrorMessage); + } + + if (additionalSeats < 0) + { + throw new BadRequestException(AdditionalSeatsNegativeErrorMessage); + } + } + + /// + /// Private helper method to create a new organization. + /// + private async Task SignUpAsync( + Organization organization, string collectionName) + { + try + { + await _organizationRepository.CreateAsync(organization); + await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + + Collection defaultCollection = null; + if (!string.IsNullOrWhiteSpace(collectionName)) + { + defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + + await _collectionRepository.CreateAsync(defaultCollection, null, null); + } + + return new ProviderClientOrganizationSignUpResponse(organization, defaultCollection); + } + catch + { + if (organization.Id != default) + { + await _organizationRepository.DeleteAsync(organization); + await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + throw; + } + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs new file mode 100644 index 0000000000..bbc997a83d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs @@ -0,0 +1,52 @@ +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 Require Two-Factor Authentication policy. +/// +public class RequireTwoFactorPolicyRequirement : IPolicyRequirement +{ + private readonly IEnumerable _policyDetails; + + public RequireTwoFactorPolicyRequirement(IEnumerable policyDetails) + { + _policyDetails = policyDetails; + } + + /// + /// Checks if two-factor authentication is required for the organization due to an active policy. + /// + /// The ID of the organization to check. + /// True if two-factor authentication is required for the organization, false otherwise. + /// + /// This should be used to check whether the member needs to have 2FA enabled before being + /// accepted, confirmed, or restored to the organization. + /// + public bool IsTwoFactorRequiredForOrganization(Guid organizationId) => + _policyDetails.Any(p => p.OrganizationId == organizationId); + + /// + /// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required. + /// Users should be revoked from these organizations if they disable all 2FA methods. + /// + public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor => + _policyDetails + .Where(p => p.OrganizationUserStatus is + OrganizationUserStatusType.Accepted or + OrganizationUserStatusType.Confirmed) + .Select(p => (p.OrganizationId, p.OrganizationUserId)); +} + +public class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication; + protected override IEnumerable ExemptStatuses => []; + + public override RequireTwoFactorPolicyRequirement Create(IEnumerable policyDetails) + { + return new RequireTwoFactorPolicyRequirement(policyDetails); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 1be0e61af7..f98135b70d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -36,5 +36,6 @@ public static class PolicyServiceCollectionExtensions services.AddScoped, ResetPasswordPolicyRequirementFactory>(); services.AddScoped, PersonalOwnershipPolicyRequirementFactory>(); services.AddScoped, RequireSsoPolicyRequirementFactory>(); + services.AddScoped, RequireTwoFactorPolicyRequirementFactory>(); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index a37deef3eb..49467eaae4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - var currentUser = _currentContext.UserId ?? Guid.Empty; - var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); - await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); - } - else - { - await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); - } + var currentUser = _currentContext.UserId ?? Guid.Empty; + var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); + await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); } } @@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); } - private async Task RemoveNonCompliantUsersAsync(Guid organizationId) - { - // Remove non-compliant users - var savingUserId = _currentContext.UserId; - // Note: must get OrganizationUserUserDetails so that Email is always populated from the User object - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - var org = await _organizationRepository.GetByIdAsync(organizationId); - if (org == null) - { - throw new NotFoundException(OrganizationNotFoundErrorMessage); - } - - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && - ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && - ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId - ).ToList(); - - var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( - removableOrgUsers.Select(ou => ou.UserId!.Value)); - foreach (var orgUser in removableOrgUsers) - { - if (userOrgs.Any(ou => ou.UserId == orgUser.UserId - && ou.OrganizationId != org.Id - && ou.Status != OrganizationUserStatusType.Invited)) - { - await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId); - - await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - } - public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (policyUpdate is not { Enabled: true }) @@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator return validateDecryptionErrorMessage; } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) + if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) { return ClaimedDomainSingleOrganizationRequiredErrorMessage; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs index c757a65913..13cc935eb9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator private readonly IOrganizationRepository _organizationRepository; private readonly ICurrentContext _currentContext; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; - private readonly IFeatureService _featureService; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."; @@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator IOrganizationRepository organizationRepository, ICurrentContext currentContext, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IRemoveOrganizationUserCommand removeOrganizationUserCommand, - IFeatureService featureService, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; @@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator _organizationRepository = organizationRepository; _currentContext = currentContext; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - _removeOrganizationUserCommand = removeOrganizationUserCommand; - _featureService = featureService; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } @@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - var currentUser = _currentContext.UserId ?? Guid.Empty; - var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); - await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); - } - else - { - await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); - } + var currentUser = _currentContext.UserId ?? Guid.Empty; + var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); + await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); } } @@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email))); } - private async Task RemoveNonCompliantUsersAsync(Guid organizationId) - { - var org = await _organizationRepository.GetByIdAsync(organizationId); - var savingUserId = _currentContext.UserId; - - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId); - - // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled - foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword)) - { - var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id) - .twoFactorIsEnabled; - if (!userTwoFactorEnabled) - { - if (!orgUser.HasMasterPassword) - { - throw new BadRequestException( - "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); - } - - await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, - savingUserId); - - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - org!.DisplayName(), orgUser.Email); - } - } - } - private static bool MembersWithNoMasterPasswordWillLoseAccess( IEnumerable orgUserDetails, IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) => diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs new file mode 100644 index 0000000000..bf6e6791cf --- /dev/null +++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs @@ -0,0 +1,24 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IIntegrationHandler +{ + Task HandleAsync(string json); +} + +public interface IIntegrationHandler : IIntegrationHandler +{ + Task HandleAsync(IntegrationMessage message); +} + +public abstract class IntegrationHandlerBase : IIntegrationHandler +{ + public async Task HandleAsync(string json) + { + var message = IntegrationMessage.FromJson(json); + return await HandleAsync(message); + } + + public abstract Task HandleAsync(IntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IIntegrationPublisher.cs new file mode 100644 index 0000000000..986ea776e1 --- /dev/null +++ b/src/Core/AdminConsole/Services/IIntegrationPublisher.cs @@ -0,0 +1,8 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IIntegrationPublisher +{ + Task PublishAsync(IIntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 228c2b522c..5fe68bd22e 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -11,8 +11,6 @@ namespace Bit.Core.Services; public interface IOrganizationService { - Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType, - TaxInfo taxInfo); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task ReinstateSubscriptionAsync(Guid organizationId); Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); @@ -20,9 +18,6 @@ public interface IOrganizationService Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); -#nullable enable - Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup); -#nullable disable /// /// Create a new organization on a self-hosted instance /// @@ -48,14 +43,8 @@ public interface IOrganizationService Task>> RevokeUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? revokingUserId); Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); - /// - /// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'. - /// - /// - /// This method must target a disabled Organization that has null keys and status as 'Pending'. - /// - Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); + Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd); void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); diff --git a/src/Core/AdminConsole/Services/IProviderService.cs b/src/Core/AdminConsole/Services/IProviderService.cs index 8999b3cb81..e4b6f3aabd 100644 --- a/src/Core/AdminConsole/Services/IProviderService.cs +++ b/src/Core/AdminConsole/Services/IProviderService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -7,7 +8,8 @@ namespace Bit.Core.AdminConsole.Services; public interface IProviderService { - Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null); + Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource = null); Task UpdateAsync(Provider provider, bool updateBilling = false); Task> InviteUserAsync(ProviderUserInvite invite); diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 4cd71ae77e..2ab10418a3 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -20,7 +20,7 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService string subscriptionName) : base(handler) { _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.TopicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); _logger = logger; } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs index fc865b327c..224f86a802 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs @@ -14,7 +14,7 @@ public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDispos public AzureServiceBusEventWriteService(GlobalSettings globalSettings) { _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.TopicName); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); } public async Task CreateAsync(IEvent e) diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs new file mode 100644 index 0000000000..8244f39c09 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -0,0 +1,101 @@ +#nullable enable + +using Azure.Messaging.ServiceBus; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class AzureServiceBusIntegrationListenerService : BackgroundService +{ + private readonly int _maxRetries; + private readonly string _subscriptionName; + private readonly string _topicName; + private readonly IIntegrationHandler _handler; + private readonly ServiceBusClient _client; + private readonly ServiceBusProcessor _processor; + private readonly ServiceBusSender _sender; + private readonly ILogger _logger; + + public AzureServiceBusIntegrationListenerService( + IIntegrationHandler handler, + string subscriptionName, + GlobalSettings globalSettings, + ILogger logger) + { + _handler = handler; + _logger = logger; + _maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; + _topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; + _subscriptionName = subscriptionName; + + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions()); + _sender = _client.CreateSender(_topicName); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _processor.ProcessMessageAsync += HandleMessageAsync; + _processor.ProcessErrorAsync += args => + { + _logger.LogError(args.Exception, "Azure Service Bus error"); + return Task.CompletedTask; + }; + + await _processor.StartProcessingAsync(cancellationToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _processor.StopProcessingAsync(cancellationToken); + await _processor.DisposeAsync(); + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + await base.StopAsync(cancellationToken); + } + + private async Task HandleMessageAsync(ProcessMessageEventArgs args) + { + var json = args.Message.Body.ToString(); + + try + { + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + await args.CompleteMessageAsync(args.Message); + return; + } + + message.ApplyRetry(result.DelayUntilDate); + + if (result.Retryable && message.RetryCount < _maxRetries) + { + var scheduledTime = (DateTime)message.DelayUntilDate!; + var retryMsg = new ServiceBusMessage(message.ToJson()) + { + Subject = args.Message.Subject, + ScheduledEnqueueTime = scheduledTime + }; + + await _sender.SendMessageAsync(retryMsg); + } + else + { + await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); + return; + } + + await args.CompleteMessageAsync(args.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error processing ASB message"); + await args.CompleteMessageAsync(args.Message); + } + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs new file mode 100644 index 0000000000..4a906e719f --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs @@ -0,0 +1,36 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.Services; + +public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _sender; + + public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + }; + + await _sender.SendMessageAsync(serviceBusMessage); + } + + public async ValueTask DisposeAsync() + { + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs new file mode 100644 index 0000000000..9a80ed67b2 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Utilities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; + +namespace Bit.Core.Services; + +#nullable enable + +public class EventIntegrationHandler( + IntegrationType integrationType, + IIntegrationPublisher integrationPublisher, + IOrganizationIntegrationConfigurationRepository configurationRepository, + IUserRepository userRepository, + IOrganizationRepository organizationRepository) + : IEventMessageHandler +{ + public async Task HandleEventAsync(EventMessage eventMessage) + { + if (eventMessage.OrganizationId is not Guid organizationId) + { + return; + } + + var configurations = await configurationRepository.GetConfigurationDetailsAsync( + organizationId, + integrationType, + eventMessage.Type); + + foreach (var configuration in configurations) + { + var template = configuration.Template ?? string.Empty; + var context = await BuildContextAsync(eventMessage, template); + var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); + + var config = configuration.MergedConfiguration.Deserialize() + ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); + + var message = new IntegrationMessage + { + IntegrationType = integrationType, + Configuration = config, + RenderedTemplate = renderedTemplate, + RetryCount = 0, + DelayUntilDate = null + }; + + await integrationPublisher.PublishAsync(message); + } + } + + public async Task HandleManyEventsAsync(IEnumerable eventMessages) + { + foreach (var eventMessage in eventMessages) + { + await HandleEventAsync(eventMessage); + } + } + + private async Task BuildContextAsync(EventMessage eventMessage, string template) + { + var context = new IntegrationTemplateContext(eventMessage); + + if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) + { + context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); + } + + if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) + { + context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); + } + + if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) + { + context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); + } + + return context; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs new file mode 100644 index 0000000000..a542e75a7b --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs @@ -0,0 +1,34 @@ +using Bit.Core.Models.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Services; + +public class EventRouteService( + [FromKeyedServices("broadcast")] IEventWriteService broadcastEventWriteService, + [FromKeyedServices("storage")] IEventWriteService storageEventWriteService, + IFeatureService _featureService) : IEventWriteService +{ + public async Task CreateAsync(IEvent e) + { + if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)) + { + await broadcastEventWriteService.CreateAsync(e); + } + else + { + await storageEventWriteService.CreateAsync(e); + } + } + + public async Task CreateManyAsync(IEnumerable e) + { + if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)) + { + await broadcastEventWriteService.CreateManyAsync(e); + } + else + { + await storageEventWriteService.CreateManyAsync(e); + } + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs index 9b99cf71f0..854e486b42 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs @@ -93,16 +93,8 @@ public class OrganizationDomainService : IOrganizationDomainService //Send email to administrators if (adminEmails.Count > 0) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails, - domain.OrganizationId.ToString(), domain.DomainName); - } - else - { - await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails, - domain.OrganizationId.ToString(), domain.DomainName); - } + await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails, + domain.OrganizationId.ToString(), domain.DomainName); } _logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index b31b43406e..7640a82fcb 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -13,9 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; @@ -31,12 +29,10 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; using Bit.Core.Utilities; -using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; using Stripe; using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite; @@ -48,7 +44,6 @@ public class OrganizationService : IOrganizationService private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; - private readonly IUserRepository _userRepository; private readonly IGroupRepository _groupRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushNotificationService; @@ -72,19 +67,15 @@ public class OrganizationService : IOrganizationService private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IProviderRepository _providerRepository; private readonly IFeatureService _featureService; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IPricingClient _pricingClient; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; - private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; - private readonly IDataProtector _dataProtector; public OrganizationService( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICollectionRepository collectionRepository, - IUserRepository userRepository, IGroupRepository groupRepository, IMailService mailService, IPushNotificationService pushNotificationService, @@ -108,19 +99,15 @@ public class OrganizationService : IOrganizationService IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IProviderRepository providerRepository, IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IPricingClient pricingClient, IPolicyRequirementQuery policyRequirementQuery, - ISendOrganizationInvitesCommand sendOrganizationInvitesCommand, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory, - IDataProtectionProvider dataProtectionProvider + ISendOrganizationInvitesCommand sendOrganizationInvitesCommand ) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _collectionRepository = collectionRepository; - _userRepository = userRepository; _groupRepository = groupRepository; _mailService = mailService; _pushNotificationService = pushNotificationService; @@ -144,34 +131,10 @@ public class OrganizationService : IOrganizationService _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _providerRepository = providerRepository; _featureService = featureService; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _pricingClient = pricingClient; _policyRequirementQuery = policyRequirementQuery; _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; - _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; - _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); - } - - public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, - PaymentMethodType paymentMethodType, TaxInfo taxInfo) - { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - await _paymentService.SaveTaxInfoAsync(organization, taxInfo); - var updated = await _paymentService.UpdatePaymentMethodAsync( - organization, - paymentMethodType, - paymentToken, - taxInfo); - if (updated) - { - await ReplaceAndUpdateCacheAsync(organization); - } } public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null) @@ -440,65 +403,6 @@ public class OrganizationService : IOrganizationService } } - public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup) - { - var plan = await _pricingClient.GetPlanOrThrow(signup.Plan); - - ValidatePlan(plan, signup.AdditionalSeats, "Password Manager"); - - var organization = new Organization - { - // Pre-generate the org id so that we can save it with the Stripe subscription. - Id = CoreHelpers.GenerateComb(), - Name = signup.Name, - BillingEmail = signup.BillingEmail, - PlanType = plan!.Type, - Seats = signup.AdditionalSeats, - MaxCollections = plan.PasswordManager.MaxCollections, - MaxStorageGb = 1, - UsePolicies = plan.HasPolicies, - UseSso = plan.HasSso, - UseGroups = plan.HasGroups, - UseEvents = plan.HasEvents, - UseDirectory = plan.HasDirectory, - UseTotp = plan.HasTotp, - Use2fa = plan.Has2fa, - UseApi = plan.HasApi, - UseResetPassword = plan.HasResetPassword, - SelfHost = plan.HasSelfHost, - UsersGetPremium = plan.UsersGetPremium, - UseCustomPermissions = plan.HasCustomPermissions, - UseScim = plan.HasScim, - Plan = plan.Name, - Gateway = GatewayType.Stripe, - ReferenceData = signup.Owner.ReferenceData, - Enabled = true, - LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, - CreationDate = DateTime.UtcNow, - RevisionDate = DateTime.UtcNow, - Status = OrganizationStatusType.Created, - UsePasswordManager = true, - // Secrets Manager not available for purchase with Consolidated Billing. - UseSecretsManager = false, - }; - - var returnValue = await SignUpAsync(organization, default, signup.OwnerKey, signup.CollectionName, false); - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = returnValue.Item1.Seats, - SignupInitiationPath = signup.InitiationPath, - Storage = returnValue.Item1.MaxStorageGb, - }); - - return returnValue; - } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) { var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); @@ -579,6 +483,8 @@ public class OrganizationService : IOrganizationService SmSeats = license.SmSeats, SmServiceAccounts = license.SmServiceAccounts, UseRiskInsights = license.UseRiskInsights, + UseOrganizationDomains = license.UseOrganizationDomains, + UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies, }; var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); @@ -1067,7 +973,7 @@ public class OrganizationService : IOrganizationService organization: organization, initOrganization: initOrganization)); - internal async Task<(bool canScale, string failureReason)> CanScaleAsync( + public async Task<(bool canScale, string failureReason)> CanScaleAsync( Organization organization, int seatsToAdd) { @@ -1809,72 +1715,6 @@ public class OrganizationService : IOrganizationService return result; } - private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled) - { - // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant - // The user will be subject to the same checks when they try to accept the invite - if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited) - { - return; - } - - var userId = orgUser.UserId.Value; - - // Enforce Single Organization Policy of organization user is being restored to - var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId); - var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); - var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg, OrganizationUserStatusType.Revoked); - var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId); - - var singleOrgCompliant = true; - var belongsToOtherOrgCompliant = true; - var twoFactorCompliant = true; - - if (hasOtherOrgs && singleOrgPolicyApplies) - { - singleOrgCompliant = false; - } - - // Enforce Single Organization Policy of other organizations user is a member of - var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg); - if (anySingleOrgPolicies) - { - belongsToOtherOrgCompliant = false; - } - - // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!userHasTwoFactorEnabled) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - twoFactorCompliant = false; - } - } - - var user = await _userRepository.GetByIdAsync(userId); - - if (!singleOrgCompliant && !twoFactorCompliant) - { - throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy"); - } - else if (!singleOrgCompliant) - { - throw new BadRequestException(user.Email + " is not compliant with the single organization policy"); - } - else if (!belongsToOtherOrgCompliant) - { - throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations"); - } - else if (!twoFactorCompliant) - { - throw new BadRequestException(user.Email + " is not compliant with the two-step login policy"); - } - } - public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser) { // Determine status to revert back to @@ -1921,71 +1761,4 @@ public class OrganizationService : IOrganizationService SalesAssistedTrialStarted = salesAssistedTrialStarted, }); } - - public async Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken) - { - await ValidateSignUpPoliciesAsync(user.Id); - - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null) - { - throw new BadRequestException("User invalid."); - } - - // TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete - var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( - _orgUserInviteTokenDataFactory, emailToken, orgUser); - - var tokenValid = newTokenValid || - CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id, - _globalSettings); - - if (!tokenValid) - { - throw new BadRequestException("Invalid token."); - } - - var org = await GetOrgById(organizationId); - - if (org.Enabled) - { - throw new BadRequestException("Organization is already enabled."); - } - - if (org.Status != OrganizationStatusType.Pending) - { - throw new BadRequestException("Organization is not on a Pending status."); - } - - if (!string.IsNullOrEmpty(org.PublicKey)) - { - throw new BadRequestException("Organization already has a Public Key."); - } - - if (!string.IsNullOrEmpty(org.PrivateKey)) - { - throw new BadRequestException("Organization already has a Private Key."); - } - - org.Enabled = true; - org.Status = OrganizationStatusType.Created; - org.PublicKey = publicKey; - org.PrivateKey = privateKey; - - await UpdateAsync(org); - - if (!string.IsNullOrWhiteSpace(collectionName)) - { - // give the owner Can Manage access over the default collection - List defaultOwnerAccess = - [new CollectionAccessSelection { Id = organizationUserId, HidePasswords = false, ReadOnly = false, Manage = true }]; - - var defaultCollection = new Collection - { - Name = collectionName, - OrganizationId = org.Id - }; - await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); - } - } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index 1ee3fa5ea7..74833f38a0 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -29,7 +29,7 @@ public class RabbitMqEventListenerService : EventLoggingListenerService UserName = globalSettings.EventLogging.RabbitMq.Username, Password = globalSettings.EventLogging.RabbitMq.Password }; - _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _logger = logger; _queueName = queueName; } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs index 86abddec58..05fcf71a92 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs @@ -18,7 +18,7 @@ public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable UserName = globalSettings.EventLogging.RabbitMq.Username, Password = globalSettings.EventLogging.RabbitMq.Password }; - _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _lazyConnection = new Lazy>(CreateConnectionAsync); } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs new file mode 100644 index 0000000000..1d6910db95 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -0,0 +1,191 @@ +using System.Text; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public class RabbitMqIntegrationListenerService : BackgroundService +{ + private const string _deadLetterRoutingKey = "dead-letter"; + private IChannel _channel; + private IConnection _connection; + private readonly string _exchangeName; + private readonly string _queueName; + private readonly string _retryQueueName; + private readonly string _deadLetterQueueName; + private readonly string _routingKey; + private readonly string _retryRoutingKey; + private readonly int _maxRetries; + private readonly IIntegrationHandler _handler; + private readonly ConnectionFactory _factory; + private readonly ILogger _logger; + private readonly int _retryTiming; + + public RabbitMqIntegrationListenerService(IIntegrationHandler handler, + string routingKey, + string queueName, + string retryQueueName, + string deadLetterQueueName, + GlobalSettings globalSettings, + ILogger logger) + { + _handler = handler; + _routingKey = routingKey; + _retryRoutingKey = $"{_routingKey}-retry"; + _queueName = queueName; + _retryQueueName = retryQueueName; + _deadLetterQueueName = deadLetterQueueName; + _logger = logger; + _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + _maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries; + _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; + + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + _connection = await _factory.CreateConnectionAsync(cancellationToken); + _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + + await _channel.ExchangeDeclareAsync(exchange: _exchangeName, + type: ExchangeType.Direct, + durable: true, + cancellationToken: cancellationToken); + + // Declare main queue + await _channel.QueueDeclareAsync(queue: _queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: _queueName, + exchange: _exchangeName, + routingKey: _routingKey, + cancellationToken: cancellationToken); + + // Declare retry queue (Configurable TTL, dead-letters back to main queue) + await _channel.QueueDeclareAsync(queue: _retryQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: new Dictionary + { + { "x-dead-letter-exchange", _exchangeName }, + { "x-dead-letter-routing-key", _routingKey }, + { "x-message-ttl", _retryTiming } + }, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: _retryQueueName, + exchange: _exchangeName, + routingKey: _retryRoutingKey, + cancellationToken: cancellationToken); + + // Declare dead letter queue + await _channel.QueueDeclareAsync(queue: _deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: _deadLetterQueueName, + exchange: _exchangeName, + routingKey: _deadLetterRoutingKey, + cancellationToken: cancellationToken); + + await base.StartAsync(cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += async (_, ea) => + { + var json = Encoding.UTF8.GetString(ea.Body.Span); + + try + { + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + // Successful integration send. Acknowledge message delivery and return + await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } + + if (result.Retryable) + { + // Integration failed, but is retryable - apply delay and check max retries + message.ApplyRetry(result.DelayUntilDate); + + if (message.RetryCount < _maxRetries) + { + // Publish message to the retry queue. It will be re-published for retry after a delay + await _channel.BasicPublishAsync( + exchange: _exchangeName, + routingKey: _retryRoutingKey, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + else + { + // Exceeded the max number of retries; fail and send to dead letter queue + await PublishToDeadLetterAsync(message.ToJson()); + _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); + } + } + else + { + // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries + await PublishToDeadLetterAsync(message.ToJson()); + _logger.LogWarning("Non-retryable failure. Sent to DLQ."); + } + + // Message has been sent to retry or dead letter queues. + // Acknowledge receipt so Rabbit knows it's been processed + await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + catch (Exception ex) + { + // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error + _logger.LogError(ex, "Unhandled error processing integration message."); + await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + }; + + await _channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); + } + + private async Task PublishToDeadLetterAsync(string json) + { + await _channel.BasicPublishAsync( + exchange: _exchangeName, + routingKey: _deadLetterRoutingKey, + body: Encoding.UTF8.GetBytes(json)); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _channel.CloseAsync(cancellationToken); + await _connection.CloseAsync(cancellationToken); + await base.StopAsync(cancellationToken); + } + + public override void Dispose() + { + _channel.Dispose(); + _connection.Dispose(); + base.Dispose(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs new file mode 100644 index 0000000000..12801e3216 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs @@ -0,0 +1,54 @@ +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; +using RabbitMQ.Client; + +namespace Bit.Core.Services; + +public class RabbitMqIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable +{ + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _exchangeName; + + public RabbitMqIntegrationPublisher(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + var connection = await _lazyConnection.Value; + await using var channel = await connection.CreateChannelAsync(); + + await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Direct, durable: true); + + var body = Encoding.UTF8.GetBytes(message.ToJson()); + + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: routingKey, body: body); + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + return await _factory.CreateConnectionAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs deleted file mode 100644 index c81914b708..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text.Json; -using Bit.Core.AdminConsole.Utilities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Integrations; -using Bit.Core.Repositories; - -namespace Bit.Core.Services; - -public class SlackEventHandler( - IOrganizationIntegrationConfigurationRepository configurationRepository, - ISlackService slackService) - : IEventMessageHandler -{ - public async Task HandleEventAsync(EventMessage eventMessage) - { - var organizationId = eventMessage.OrganizationId ?? Guid.Empty; - var configurations = await configurationRepository.GetConfigurationDetailsAsync( - organizationId, - IntegrationType.Slack, - eventMessage.Type); - - foreach (var configuration in configurations) - { - var config = configuration.MergedConfiguration.Deserialize(); - if (config is null) - { - continue; - } - - await slackService.SendSlackMessageByChannelIdAsync( - config.token, - IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), - config.channelId - ); - } - } - - public async Task HandleManyEventsAsync(IEnumerable eventMessages) - { - foreach (var eventMessage in eventMessages) - { - await HandleEventAsync(eventMessage); - } - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs new file mode 100644 index 0000000000..134e93e838 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public class SlackIntegrationHandler( + ISlackService slackService) + : IntegrationHandlerBase +{ + public override async Task HandleAsync(IntegrationMessage message) + { + await slackService.SendSlackMessageByChannelIdAsync( + message.Configuration.token, + message.RenderedTemplate, + message.Configuration.channelId + ); + + return new IntegrationHandlerResult(success: true, message: message); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs deleted file mode 100644 index 1c3b279ee5..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Text; -using System.Text.Json; -using Bit.Core.AdminConsole.Utilities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Integrations; -using Bit.Core.Repositories; - -#nullable enable - -namespace Bit.Core.Services; - -public class WebhookEventHandler( - IHttpClientFactory httpClientFactory, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : IEventMessageHandler -{ - private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); - - public const string HttpClientName = "WebhookEventHandlerHttpClient"; - - public async Task HandleEventAsync(EventMessage eventMessage) - { - var organizationId = eventMessage.OrganizationId ?? Guid.Empty; - var configurations = await configurationRepository.GetConfigurationDetailsAsync( - organizationId, - IntegrationType.Webhook, - eventMessage.Type); - - foreach (var configuration in configurations) - { - var config = configuration.MergedConfiguration.Deserialize(); - if (config is null || string.IsNullOrEmpty(config.url)) - { - continue; - } - - var content = new StringContent( - IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), - Encoding.UTF8, - "application/json" - ); - var response = await _httpClient.PostAsync( - config.url, - content); - response.EnsureSuccessStatusCode(); - } - } - - public async Task HandleManyEventsAsync(IEnumerable eventMessages) - { - foreach (var eventMessage in eventMessages) - { - await HandleEventAsync(eventMessage); - } - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs new file mode 100644 index 0000000000..5f9898afe8 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs @@ -0,0 +1,61 @@ +using System.Globalization; +using System.Net; +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; + +#nullable enable + +namespace Bit.Core.Services; + +public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) + : IntegrationHandlerBase +{ + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + + public const string HttpClientName = "WebhookIntegrationHandlerHttpClient"; + + public override async Task HandleAsync(IntegrationMessage message) + { + var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync(message.Configuration.url, content); + var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); + + switch (response.StatusCode) + { + case HttpStatusCode.TooManyRequests: + case HttpStatusCode.RequestTimeout: + case HttpStatusCode.InternalServerError: + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + result.Retryable = true; + result.FailureReason = response.ReasonPhrase; + + if (response.Headers.TryGetValues("Retry-After", out var values)) + { + var value = values.FirstOrDefault(); + if (int.TryParse(value, out var seconds)) + { + // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. + result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds); + } + else if (DateTimeOffset.TryParseExact(value, + "r", // "r" is the round-trip format: RFC1123 + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var retryDate)) + { + // Retry-after was specified as a date. Adjust DelayUntilDate to the specified date. + result.DelayUntilDate = retryDate.UtcDateTime; + } + } + break; + default: + result.Retryable = false; + result.FailureReason = response.ReasonPhrase; + break; + } + + return result; + } +} diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs index bd3a757663..94c1096b58 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -7,7 +8,7 @@ namespace Bit.Core.AdminConsole.Services.NoopImplementations; public class NoopProviderService : IProviderService { - public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) => throw new NotImplementedException(); + public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) => throw new NotImplementedException(); public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException(); diff --git a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs deleted file mode 100644 index ba78601637..0000000000 --- a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Bit.Core.AdminConsole.Errors; - -namespace Bit.Core.AdminConsole.Shared.Validation; - -public abstract record ValidationResult; - -public record Valid : ValidationResult -{ - public Valid() { } - - public Valid(T Value) - { - this.Value = Value; - } - - public T Value { get; init; } -} - -public record Invalid : ValidationResult -{ - public IEnumerable> Errors { get; init; } = []; - - public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message)); - - public Invalid() { } - - public Invalid(Error error) : this([error]) { } - - public Invalid(IEnumerable> errors) - { - Errors = errors; - } -} - -public static class ValidationResultMappers -{ - public static ValidationResult Map(this ValidationResult validationResult, B invalidValue) => - validationResult switch - { - Valid => new Valid(invalidValue), - Invalid invalid => new Invalid(invalid.Errors.Select(x => x.ToError(invalidValue))), - _ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type") - }; -} diff --git a/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs b/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs new file mode 100644 index 0000000000..274b1a8ba5 --- /dev/null +++ b/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs @@ -0,0 +1,51 @@ +#nullable enable + +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; + +namespace Bit.Core.AdminConsole.Utilities.Commands; + +public abstract class CommandResult; + +public class Success(T value) : CommandResult +{ + public T Value { get; } = value; +} + +public class Failure(Error error) : CommandResult +{ + public Error Error { get; } = error; +} + +public class Partial(IEnumerable successfulItems, IEnumerable> failedItems) + : CommandResult +{ + public IEnumerable Successes { get; } = successfulItems; + public IEnumerable> Failures { get; } = failedItems; +} + +public static class CommandResultExtensions +{ + /// + /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. + /// + /// + /// This is the invalid type from validating the object. + /// This function will map between the two types for the inner ErrorT + /// Invalid object's type + /// Failure object's type + /// + public static CommandResult MapToFailure(this Invalid invalidResult, Func mappingFunction) => + new Failure(invalidResult.Error.ToError(mappingFunction(invalidResult.Error.ErroredValue))); +} + +[Obsolete("Use CommandResult instead. This will be removed once old code is updated.")] +public class CommandResult(IEnumerable errors) +{ + public CommandResult(string error) : this([error]) { } + + public bool Success => ErrorMessages.Count == 0; + public bool HasErrors => ErrorMessages.Count > 0; + public List ErrorMessages { get; } = errors.ToList(); + public CommandResult() : this(Array.Empty()) { } +} diff --git a/src/Core/AdminConsole/Errors/Error.cs b/src/Core/AdminConsole/Utilities/Errors/Error.cs similarity index 80% rename from src/Core/AdminConsole/Errors/Error.cs rename to src/Core/AdminConsole/Utilities/Errors/Error.cs index 7ad057d6ed..949c6903a0 100644 --- a/src/Core/AdminConsole/Errors/Error.cs +++ b/src/Core/AdminConsole/Utilities/Errors/Error.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record Error(string Message, T ErroredValue); diff --git a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs b/src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs similarity index 83% rename from src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs rename to src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs index d04ceba7c9..c1a524fa0b 100644 --- a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record InsufficientPermissionsError(string Message, T ErroredValue) : Error(Message, ErroredValue) { diff --git a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs b/src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs similarity index 71% rename from src/Core/AdminConsole/Errors/InvalidResultTypeError.cs rename to src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs index 67b5b634fb..f39aea68ce 100644 --- a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record InvalidResultTypeError(T Value) : Error(Code, Value) { diff --git a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs b/src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs similarity index 82% rename from src/Core/AdminConsole/Errors/RecordNotFoundError.cs rename to src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs index 25a169efe1..748bb57b5f 100644 --- a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record RecordNotFoundError(string Message, T ErroredValue) : Error(Message, ErroredValue) { diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index 178c0348d9..aab4e448e5 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -1,4 +1,6 @@ -using System.Text.RegularExpressions; +#nullable enable + +using System.Text.RegularExpressions; namespace Bit.Core.AdminConsole.Utilities; @@ -9,9 +11,10 @@ public static partial class IntegrationTemplateProcessor public static string ReplaceTokens(string template, object values) { - if (string.IsNullOrEmpty(template) || values == null) + if (string.IsNullOrEmpty(template)) + { return template; - + } var type = values.GetType(); return TokenRegex().Replace(template, match => { @@ -20,4 +23,36 @@ public static partial class IntegrationTemplateProcessor return property?.GetValue(values)?.ToString() ?? match.Value; }); } + + public static bool TemplateRequiresUser(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#UserName#", StringComparison.Ordinal) + || template.Contains("#UserEmail#", StringComparison.Ordinal); + } + + public static bool TemplateRequiresActingUser(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#ActingUserName#", StringComparison.Ordinal) + || template.Contains("#ActingUserEmail#", StringComparison.Ordinal); + } + + public static bool TemplateRequiresOrganization(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#OrganizationName#", StringComparison.Ordinal); + } } diff --git a/src/Core/AdminConsole/Shared/Validation/IValidator.cs b/src/Core/AdminConsole/Utilities/Validation/IValidator.cs similarity index 62% rename from src/Core/AdminConsole/Shared/Validation/IValidator.cs rename to src/Core/AdminConsole/Utilities/Validation/IValidator.cs index d90386f00e..1598e4472f 100644 --- a/src/Core/AdminConsole/Shared/Validation/IValidator.cs +++ b/src/Core/AdminConsole/Utilities/Validation/IValidator.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Shared.Validation; +namespace Bit.Core.AdminConsole.Utilities.Validation; public interface IValidator { diff --git a/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs b/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs new file mode 100644 index 0000000000..c62aa880ec --- /dev/null +++ b/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs @@ -0,0 +1,20 @@ +using Bit.Core.AdminConsole.Utilities.Errors; + +namespace Bit.Core.AdminConsole.Utilities.Validation; + +public abstract record ValidationResult; + +public record Valid(T Value) : ValidationResult; + +public record Invalid(Error Error) : ValidationResult; + +public static class ValidationResultMappers +{ + public static ValidationResult Map(this ValidationResult validationResult, B invalidValue) => + validationResult switch + { + Valid => new Valid(invalidValue), + Invalid invalid => new Invalid(invalid.Error.ToError(invalidValue)), + _ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type") + }; +} diff --git a/src/Core/Auth/Entities/SsoUser.cs b/src/Core/Auth/Entities/SsoUser.cs index 3199f00221..2e457afbc6 100644 --- a/src/Core/Auth/Entities/SsoUser.cs +++ b/src/Core/Auth/Entities/SsoUser.cs @@ -8,7 +8,7 @@ public class SsoUser : ITableObject public long Id { get; set; } public Guid UserId { get; set; } public Guid? OrganizationId { get; set; } - [MaxLength(50)] + [MaxLength(300)] public string ExternalId { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Auth/Enums/EmergencyAccessStatusType.cs b/src/Core/Auth/Enums/EmergencyAccessStatusType.cs index 7faaa11752..d817d6a950 100644 --- a/src/Core/Auth/Enums/EmergencyAccessStatusType.cs +++ b/src/Core/Auth/Enums/EmergencyAccessStatusType.cs @@ -2,9 +2,24 @@ public enum EmergencyAccessStatusType : byte { + /// + /// The user has been invited to be an emergency contact. + /// Invited = 0, + /// + /// The invited user, "grantee", has accepted the request to be an emergency contact. + /// Accepted = 1, + /// + /// The inviting user, "grantor", has approved the grantee's acceptance. + /// Confirmed = 2, + /// + /// The grantee has initiated the recovery process. + /// RecoveryInitiated = 3, + /// + /// The grantee has excercised their emergency access. + /// RecoveryApproved = 4, } diff --git a/src/Core/Auth/Enums/EmergencyAccessType.cs b/src/Core/Auth/Enums/EmergencyAccessType.cs index a3497cc287..6e4e6e7f56 100644 --- a/src/Core/Auth/Enums/EmergencyAccessType.cs +++ b/src/Core/Auth/Enums/EmergencyAccessType.cs @@ -2,6 +2,12 @@ public enum EmergencyAccessType : byte { + /// + /// Allows emergency contact to view the Grantor's data. + /// View = 0, + /// + /// Allows emergency contact to take over the Grantor's account by overwriting the Grantor's password. + /// Takeover = 1, } diff --git a/src/Core/Auth/Enums/TwoFactorProviderType.cs b/src/Core/Auth/Enums/TwoFactorProviderType.cs index 07a52dc429..c3613785bc 100644 --- a/src/Core/Auth/Enums/TwoFactorProviderType.cs +++ b/src/Core/Auth/Enums/TwoFactorProviderType.cs @@ -6,7 +6,8 @@ public enum TwoFactorProviderType : byte Email = 1, Duo = 2, YubiKey = 3, - U2f = 4, // Deprecated + [Obsolete("Deprecated in favor of WebAuthn.")] + U2f = 4, Remember = 5, OrganizationDuo = 6, WebAuthn = 7, diff --git a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs index 9468e4d571..5a3d9522f3 100644 --- a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Enums; using Bit.Core.Entities; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -12,16 +11,13 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}"; - private readonly IServiceProvider _serviceProvider; private readonly IDistributedCache _distributedCache; private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; public AuthenticatorTokenProvider( - IServiceProvider serviceProvider, [FromKeyedServices("persistent")] IDistributedCache distributedCache) { - _serviceProvider = serviceProvider; _distributedCache = distributedCache; _distributedCacheEntryOptions = new DistributedCacheEntryOptions { @@ -29,15 +25,14 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider }; } - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); - if (string.IsNullOrWhiteSpace((string)provider?.MetaData["Key"])) + var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); + if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData["Key"])) { - return false; + return Task.FromResult(false); } - return await _serviceProvider.GetRequiredService() - .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Authenticator, user); + return Task.FromResult(authenticatorProvider.Enabled); } public Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs index 21311326c0..3f2a44915c 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -16,10 +16,11 @@ public class DuoUniversalTokenProvider( IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider { /// - /// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance - /// occurring between IUserService, which extends the UserManager, and the usage of the - /// UserManager within this class. Trying to resolve the IUserService using the DI pipeline - /// will not allow the server to start and it will hang and give no helpful indication as to the problem. + /// We need the IServiceProvider to resolve the . There is a complex dependency dance + /// occurring between , which extends the , and the usage + /// of the within this class. Trying to resolve the using + /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the + /// problem. /// private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly IDataProtectorTokenFactory _tokenDataFactory = tokenDataFactory; @@ -28,12 +29,13 @@ public class DuoUniversalTokenProvider( public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - var provider = await GetDuoTwoFactorProvider(user, userService); - if (provider == null) + var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService); + if (duoUniversalTokenProvider == null) { return false; } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); + + return duoUniversalTokenProvider.Enabled; } public async Task GenerateAsync(string purpose, UserManager manager, User user) @@ -57,7 +59,7 @@ public class DuoUniversalTokenProvider( } /// - /// Get the Duo Two Factor Provider for the user if they have access to Duo + /// Get the Duo Two Factor Provider for the user if they have premium access to Duo /// /// Active User /// null or Duo TwoFactorProvider diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index b0ad9bd480..718e44ae5f 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -1,7 +1,6 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -10,31 +9,25 @@ namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTwoFactorTokenProvider : EmailTokenProvider { - private readonly IServiceProvider _serviceProvider; - public EmailTwoFactorTokenProvider( - IServiceProvider serviceProvider, [FromKeyedServices("persistent")] IDistributedCache distributedCache) : base(distributedCache) { - _serviceProvider = serviceProvider; - TokenAlpha = false; TokenNumeric = true; TokenLength = 6; } - public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (!HasProperMetaData(provider)) + var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if (!HasProperMetaData(emailTokenProvider)) { - return false; + return Task.FromResult(false); } - return await _serviceProvider.GetRequiredService(). - TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user); + return Task.FromResult(emailTokenProvider.Enabled); } public override Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index 202ba3a38c..0bf75d0fc3 100644 --- a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -25,17 +25,16 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider _globalSettings = globalSettings; } - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var userService = _serviceProvider.GetRequiredService(); - var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + // null check happens in this method if (!HasProperMetaData(webAuthnProvider)) { - return false; + return Task.FromResult(false); } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user); + return Task.FromResult(webAuthnProvider.Enabled); } public async Task GenerateAsync(string purpose, UserManager manager, User user) @@ -81,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (!provider.MetaData.ContainsKey("login")) + if (!provider.MetaData.TryGetValue("login", out var value)) { return false; } @@ -89,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var clientResponse = JsonSerializer.Deserialize(token, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var jsonOptions = provider.MetaData["login"].ToString(); + var jsonOptions = value.ToString(); var options = AssertionOptions.FromJson(jsonOptions); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); @@ -126,6 +125,12 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider } + /// + /// Checks if the provider has proper metadata. + /// This is used to determine if the provider has been properly configured. + /// + /// + /// true if metadata is present; false if empty or null private bool HasProperMetaData(TwoFactorProvider provider) { return provider?.MetaData?.Any() ?? false; diff --git a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs index 9794a51ae9..b33d2fc0c9 100644 --- a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs @@ -23,19 +23,21 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { + // Ensure the user has access to premium var userService = _serviceProvider.GetRequiredService(); if (!await userService.CanAccessPremium(user)) { return false; } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); - if (!provider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) + // Check if the user has a YubiKey provider configured + var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); + if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) { return false; } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.YubiKey, user); + return yubicoProvider.Enabled; } public Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/UserStore.cs b/src/Core/Auth/Identity/UserStore.cs index 3716d75b6a..41323f05b7 100644 --- a/src/Core/Auth/Identity/UserStore.cs +++ b/src/Core/Auth/Identity/UserStore.cs @@ -1,7 +1,7 @@ -using Bit.Core.Context; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -167,7 +167,7 @@ public class UserStore : public async Task GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) { - return await _serviceProvider.GetRequiredService().TwoFactorIsEnabledAsync(user); + return await _serviceProvider.GetRequiredService().TwoFactorIsEnabledAsync(user); } public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken) diff --git a/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs b/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs deleted file mode 100644 index 6968a904b0..0000000000 --- a/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bit.Core.Auth.Models.Api; - -public interface ICaptchaProtectedModel -{ - string CaptchaResponse { get; set; } -} diff --git a/src/Core/Auth/Models/Business/CaptchaResponse.cs b/src/Core/Auth/Models/Business/CaptchaResponse.cs deleted file mode 100644 index 1a4b039ec0..0000000000 --- a/src/Core/Auth/Models/Business/CaptchaResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bit.Core.Auth.Models.Business; - -public class CaptchaResponse -{ - public bool Success { get; set; } - public bool MaybeBot { get; set; } - public bool IsBot { get; set; } - public double Score { get; set; } -} diff --git a/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs deleted file mode 100644 index 72994563c1..0000000000 --- a/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; -using Bit.Core.Entities; -using Bit.Core.Tokens; - -namespace Bit.Core.Auth.Models.Business.Tokenables; - -public class HCaptchaTokenable : ExpiringTokenable -{ - private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes - public const string ClearTextPrefix = "BWCaptchaBypass_"; - public const string DataProtectorPurpose = "CaptchaServiceDataProtector"; - public const string TokenIdentifier = "CaptchaBypassToken"; - - public string Identifier { get; set; } = TokenIdentifier; - public Guid Id { get; set; } - public string Email { get; set; } - - [JsonConstructor] - public HCaptchaTokenable() - { - ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours); - } - - public HCaptchaTokenable(User user) : this() - { - Id = user?.Id ?? default; - Email = user?.Email; - } - - public bool TokenIsValid(User user) - { - if (Id == default || Email == default || user == null) - { - return false; - } - - return Id == user.Id && - Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); - } - - // Validates deserialized - protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email); -} diff --git a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs index 24a74bde07..30687a6a4a 100644 --- a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs @@ -4,9 +4,10 @@ using Bit.Core.Tokens; namespace Bit.Core.Auth.Models.Business.Tokenables; -// This token just provides a verifiable authN mechanism for the API service -// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be -// used maliciously. +/// +/// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync +/// anonymous endpoint so it cannot used maliciously. +/// public class SsoEmail2faSessionTokenable : ExpiringTokenable { // Just over 2 min expiration (client expires session after 2 min) diff --git a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs index 5d9ae4b362..f953e4570e 100644 --- a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs +++ b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs @@ -1,10 +1,18 @@ using Bit.Core.Auth.Enums; +using Bit.Core.Services; namespace Bit.Core.Auth.Models; public interface ITwoFactorProvidersUser { string TwoFactorProviders { get; } + /// + /// Get the two factor providers for the user. Currently it can be assumed providers are enabled + /// if they exists in the dictionary. When two factor providers are disabled they are removed + /// from the dictionary. + /// + /// + /// Dictionary of providers with the type enum as the key Dictionary GetTwoFactorProviders(); Guid? GetUserId(); bool GetPremium(); diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs similarity index 74% rename from src/Core/Auth/Services/Implementations/EmergencyAccessService.cs rename to src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs index dda16e29fe..6a8fe9dd17 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; @@ -16,7 +15,6 @@ using Bit.Core.Tokens; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Services; -using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.Services; @@ -31,8 +29,6 @@ public class EmergencyAccessService : IEmergencyAccessService private readonly IMailService _mailService; private readonly IUserService _userService; private readonly GlobalSettings _globalSettings; - private readonly IPasswordHasher _passwordHasher; - private readonly IOrganizationService _organizationService; private readonly IDataProtectorTokenFactory _dataProtectorTokenizer; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; @@ -45,9 +41,7 @@ public class EmergencyAccessService : IEmergencyAccessService ICipherService cipherService, IMailService mailService, IUserService userService, - IPasswordHasher passwordHasher, GlobalSettings globalSettings, - IOrganizationService organizationService, IDataProtectorTokenFactory dataProtectorTokenizer, IRemoveOrganizationUserCommand removeOrganizationUserCommand) { @@ -59,45 +53,43 @@ public class EmergencyAccessService : IEmergencyAccessService _cipherService = cipherService; _mailService = mailService; _userService = userService; - _passwordHasher = passwordHasher; _globalSettings = globalSettings; - _organizationService = organizationService; _dataProtectorTokenizer = dataProtectorTokenizer; _removeOrganizationUserCommand = removeOrganizationUserCommand; } - public async Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) + public async Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime) { - if (!await _userService.CanAccessPremium(invitingUser)) + if (!await _userService.CanAccessPremium(grantorUser)) { throw new BadRequestException("Not a premium user."); } - if (type == EmergencyAccessType.Takeover && invitingUser.UsesKeyConnector) + if (accessType == EmergencyAccessType.Takeover && grantorUser.UsesKeyConnector) { throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector."); } var emergencyAccess = new EmergencyAccess { - GrantorId = invitingUser.Id, - Email = email.ToLowerInvariant(), + GrantorId = grantorUser.Id, + Email = emergencyContactEmail.ToLowerInvariant(), Status = EmergencyAccessStatusType.Invited, - Type = type, + Type = accessType, WaitTimeDays = waitTime, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, }; await _emergencyAccessRepository.CreateAsync(emergencyAccess); - await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser)); + await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser)); return emergencyAccess; } - public async Task GetAsync(Guid emergencyAccessId, Guid userId) + public async Task GetAsync(Guid emergencyAccessId, Guid grantorId) { - var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, userId); + var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, grantorId); if (emergencyAccess == null) { throw new BadRequestException("Emergency Access not valid."); @@ -106,19 +98,19 @@ public class EmergencyAccessService : IEmergencyAccessService return emergencyAccess; } - public async Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId) + public async Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (emergencyAccess == null || emergencyAccess.GrantorId != invitingUser.Id || + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.Invited) { throw new BadRequestException("Emergency Access not valid."); } - await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser)); + await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser)); } - public async Task AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService) + public async Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); if (emergencyAccess == null) @@ -126,7 +118,12 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Emergency Access not valid."); } - if (!_dataProtectorTokenizer.TryUnprotect(token, out var data) && data.IsValid(emergencyAccessId, user.Email)) + if (!_dataProtectorTokenizer.TryUnprotect(token, out var data)) + { + throw new BadRequestException("Invalid token."); + } + + if (!data.IsValid(emergencyAccessId, granteeUser.Email)) { throw new BadRequestException("Invalid token."); } @@ -140,8 +137,10 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Invitation already accepted."); } + // TODO PM-21687 + // Might not be reachable since the Tokenable.IsValid() does an email comparison if (string.IsNullOrWhiteSpace(emergencyAccess.Email) || - !emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) + !emergencyAccess.Email.Equals(granteeUser.Email, StringComparison.InvariantCultureIgnoreCase)) { throw new BadRequestException("User email does not match invite."); } @@ -149,7 +148,7 @@ public class EmergencyAccessService : IEmergencyAccessService var granteeEmail = emergencyAccess.Email; emergencyAccess.Status = EmergencyAccessStatusType.Accepted; - emergencyAccess.GranteeId = user.Id; + emergencyAccess.GranteeId = granteeUser.Id; emergencyAccess.Email = null; var grantor = await userService.GetUserByIdAsync(emergencyAccess.GrantorId); @@ -163,6 +162,8 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); + // TODO PM-19438/PM-21687 + // Not sure why the GrantorId and the GranteeId are supposed to be the same? if (emergencyAccess == null || (emergencyAccess.GrantorId != grantorId && emergencyAccess.GranteeId != grantorId)) { throw new BadRequestException("Emergency Access not valid."); @@ -171,16 +172,16 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.DeleteAsync(emergencyAccess); } - public async Task ConfirmUserAsync(Guid emergencyAcccessId, string key, Guid confirmingUserId) + public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAcccessId); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted || - emergencyAccess.GrantorId != confirmingUserId) + emergencyAccess.GrantorId != grantorId) { throw new BadRequestException("Emergency Access not valid."); } - var grantor = await _userRepository.GetByIdAsync(confirmingUserId); + var grantor = await _userRepository.GetByIdAsync(grantorId); if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector) { throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector."); @@ -197,14 +198,14 @@ public class EmergencyAccessService : IEmergencyAccessService return emergencyAccess; } - public async Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser) + public async Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser) { - if (!await _userService.CanAccessPremium(savingUser)) + if (!await _userService.CanAccessPremium(grantorUser)) { throw new BadRequestException("Not a premium user."); } - if (emergencyAccess.GrantorId != savingUser.Id) + if (emergencyAccess.GrantorId != grantorUser.Id) { throw new BadRequestException("Emergency Access not valid."); } @@ -221,11 +222,11 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); } - public async Task InitiateAsync(Guid id, User initiatingUser) + // TODO PM-21687: rename this to something like InitiateRecoveryAsync, and something similar for Approve and Reject + public async Task InitiateAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); - - if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id || + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); + if (emergencyAccess == null || emergencyAccess.GranteeId != granteeUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.Confirmed) { throw new BadRequestException("Emergency Access not valid."); @@ -245,14 +246,14 @@ public class EmergencyAccessService : IEmergencyAccessService emergencyAccess.LastNotificationDate = now; await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); - await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(initiatingUser), grantor.Email); + await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(granteeUser), grantor.Email); } - public async Task ApproveAsync(Guid id, User approvingUser) + public async Task ApproveAsync(Guid emergencyAccessId, User grantorUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (emergencyAccess == null || emergencyAccess.GrantorId != approvingUser.Id || + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated) { throw new BadRequestException("Emergency Access not valid."); @@ -262,14 +263,14 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value); - await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(approvingUser), grantee.Email); + await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(grantorUser), grantee.Email); } - public async Task RejectAsync(Guid id, User rejectingUser) + public async Task RejectAsync(Guid emergencyAccessId, User grantorUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (emergencyAccess == null || emergencyAccess.GrantorId != rejectingUser.Id || + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id || (emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated && emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved)) { @@ -280,14 +281,17 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value); - await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(rejectingUser), grantee.Email); + await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(grantorUser), grantee.Email); } - public async Task> GetPoliciesAsync(Guid id, User requestingUser) + public async Task> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + // TODO PM-21687 + // Should we look up policies here or just verify the EmergencyAccess is correct + // and handle policy logic else where? Should this be a query/Command? + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover)) { throw new BadRequestException("Emergency Access not valid."); } @@ -295,23 +299,27 @@ public class EmergencyAccessService : IEmergencyAccessService var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id); - var isOrganizationOwner = grantorOrganizations.Any(organization => organization.Type == OrganizationUserType.Owner); + var isOrganizationOwner = grantorOrganizations + .Any(organization => organization.Type == OrganizationUserType.Owner); + var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null; return policies; } - public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User requestingUser) + // TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync + public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover)) { throw new BadRequestException("Emergency Access not valid."); } var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); - + // TODO PM-21687 + // Redundant check of the EmergencyAccessType -> checked in IsValidRequest() ln 308 if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector) { throw new BadRequestException("You cannot takeover an account that is using Key Connector."); @@ -320,11 +328,12 @@ public class EmergencyAccessService : IEmergencyAccessService return (emergencyAccess, grantor); } - public async Task PasswordAsync(Guid id, User requestingUser, string newMasterPasswordHash, string key) + // TODO PM-21687: rename this to something like FinishRecoveryTakeoverAsync + public async Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover)) { throw new BadRequestException("Emergency Access not valid."); } @@ -336,7 +345,9 @@ public class EmergencyAccessService : IEmergencyAccessService grantor.LastPasswordChangeDate = grantor.RevisionDate; grantor.Key = key; // Disable TwoFactor providers since they will otherwise block logins - grantor.SetTwoFactorProviders(new Dictionary()); + grantor.SetTwoFactorProviders([]); + // Disable New Device Verification since it will otherwise block logins + grantor.VerifyDevices = false; await _userRepository.ReplaceAsync(grantor); // Remove grantor from all organizations unless Owner @@ -384,11 +395,11 @@ public class EmergencyAccessService : IEmergencyAccessService } } - public async Task ViewAsync(Guid id, User requestingUser) + public async Task ViewAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View)) { throw new BadRequestException("Emergency Access not valid."); } @@ -402,11 +413,11 @@ public class EmergencyAccessService : IEmergencyAccessService }; } - public async Task GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User requestingUser) + public async Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View)) { throw new BadRequestException("Emergency Access not valid."); } @@ -421,12 +432,23 @@ public class EmergencyAccessService : IEmergencyAccessService await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token); } - private string NameOrEmail(User user) + // TODO PM-21687: move this to the user entity -> User.GetNameOrEmail()? + private static string NameOrEmail(User user) { return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name; } - private bool IsValidRequest(EmergencyAccess availableAccess, User requestingUser, EmergencyAccessType requestedAccessType) + /* + * Checks if EmergencyAccess Object is null + * Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action) + * Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet) + * request type must equal the type of access requested (View or Takeover) + */ + //TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser + private static bool IsValidRequest( + EmergencyAccess availableAccess, + User requestingUser, + EmergencyAccessType requestedAccessType) { return availableAccess != null && availableAccess.GranteeId == requestingUser.Id && diff --git a/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs new file mode 100644 index 0000000000..de695bbd7d --- /dev/null +++ b/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs @@ -0,0 +1,147 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Services; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Auth.Services; + +public interface IEmergencyAccessService +{ + /// + /// Invites a user via email to become an emergency contact for the Grantor user. The Grantor must have a premium subscription. + /// the grantor user must not be a member of the organization that uses KeyConnector. + /// + /// The user initiating the emergency contact request + /// Emergency contact + /// Type of emergency access allowed to the emergency contact + /// The amount of time to pass before the invite is auto confirmed + /// a new Emergency Access object + Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime); + /// + /// Sends an invite to the emergency contact associated with the emergency access id. + /// + /// The grantor. This must be the owner of the Emergency Access object + /// The Id of the emergency access being requested. + /// void + Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId); + /// + /// A grantee user accepts the emergency contact request. This updates the emergency access status to be + /// "Accepted", this is the middle step before the grantor user confirms the request. + /// + /// Id of the emergency access object being acted on. + /// User being invited to be an emergency contact + /// the tokenable that was sent via email + /// service dependency + /// void + Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService); + /// + /// The creator of the emergency access request can delete the request. + /// + /// Id of the emergency access being acted on + /// Id of the owner trying to delete the emergency access request + /// void + Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); + /// + /// The grantor user confirms the acceptance of the emergency contact request. This stores the encrypted key allowing the grantee + /// access based on the emergency access type. + /// + /// Id of the emergency access being acted on. + /// The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key) + /// Id of grantor user + /// emergency access object associated with the Id passed in + Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); + /// + /// Fetches an emergency access object. The grantor user must own the object being fetched. + /// + /// Id of emergency access object + /// Id of the owner of the emergency access object. + /// Details of the emergency access object + Task GetAsync(Guid emergencyAccessId, Guid grantorId); + /// + /// Updates the emergency access object. + /// + /// emergency access entity being updated + /// grantor user + /// void + Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser); + /// + /// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation. + /// + /// EmergencyAccess Id + /// grantee user + /// void + Task InitiateAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Approves a recovery request. Sets the EmergencyAccess.Status to RecoveryApproved. + /// + /// emergency access id + /// grantor user + /// void + Task ApproveAsync(Guid emergencyAccessId, User grantorUser); + /// + /// Rejects a recovery request. Sets the EmergencyAccess.Status to Confirmed. This does not remove the emergency access entity. The + /// Grantee user can still initiate another recovery request. + /// + /// emergency access id + /// grantor user + /// void + Task RejectAsync(Guid emergencyAccessId, User grantorUser); + /// + /// This request is made by the Grantee user to fetch the policies for the Grantor User. + /// The Grantor User has to be the owner of the organization. + /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user + /// are returned. This is used to ensure the password is of the proper complexity for the organization. + /// + /// EmergencyAccess.Id being acted on + /// User making the request, this is the Grantee + /// null if the GrantorUser is not an organization owner; A list of policies otherwise. + Task> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Fetches the emergency access entity and grantor user. The grantor user is returned so the correct KDF configuration is + /// used for the new password. + /// + /// Id of entity being accessed + /// grantee user of the emergency access entity + /// emergency access entity and the grantorUser + Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Updates the grantor's password hash and updates the key for the EmergencyAccess entity. + /// + /// Emergency Access Id being acted on + /// user making the request + /// new password hash set by grantee user + /// new encrypted user key + /// void + Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key); + /// + /// sends a reminder email that there is a pending request for recovery. + /// + /// void + Task SendNotificationsAsync(); + /// + /// This handles the auto approval of recovery requests started in the method. + /// An email will be sent to the Grantee and the Grantor notifying each the recovery has been approved. + /// + /// void + Task HandleTimedOutRequestsAsync(); + /// + /// Fetched ciphers from the grantors account for the grantee to view. + /// + /// Emergency access entity being acted on + /// user requesting cipher items + /// ciphers associated with the emergency access request + Task ViewAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Returns attachment if the grantee user has access to the cipher through the emergency access entity. + /// + /// EmergencyAccess entity being acted on + /// cipher entity containing the attachment + /// Attachment entity + /// user making the request + /// attachment response + Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser); +} diff --git a/src/Core/Auth/Services/EmergencyAccess/readme.md b/src/Core/Auth/Services/EmergencyAccess/readme.md new file mode 100644 index 0000000000..e2bdec3916 --- /dev/null +++ b/src/Core/Auth/Services/EmergencyAccess/readme.md @@ -0,0 +1,95 @@ +# Emergency Access System +This system allows users to share their `User.Key` with other users using public key exchange. An emergency contact (a grantee user) can view or takeover (reset the password) of the grantor user. + +When an account is taken over all two factor methods are turned off and device verification is disabled. + +This system is affected by the Key Rotation feature. The `EmergencyAccess.KeyEncrypted` is the `Grantor.UserKey` encrypted by the `Grantee.PublicKey`. So if the `User.Key` is rotated then all `EmergencyAccess` entities will need to be updated. + +## Special Cases +Users who use `KeyConnector` are not able to allow `Takeover` of their accounts. However, they can allow `View`. + +When a grantee user _takes over_ a grantor user's account, the grantor user will be removed from all organizations where the grantor user is not the `OrganizationUserType.Owner`. A grantor user will not be removed from organizations if the `EmergencyAccessType` is `View`. The grantee user will only be able to `View` the grantor user's ciphers, and not any of the organization ciphers, if any exist. + +## Step 1. Invitation + +A grantor user invites another user to be their emergency contact, the grantee. This will create a new `EmergencyAccess` entity in the database with the `EmergencyAccessStatusType` set to `Invited`. +The `EmergencyAccess.KeyEncrypted` field is empty, and the `GranteeId` is `null` since the user being invited via email may not have an account yet. + +### code +```csharp +// creates entity. +Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime); +// resend email to the EmergencyAccess.Email. +Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId); +``` + +## Step 2. Acceptance + +The grantee user receives an email they have been invited to be an emergency contact for a grantor user. + +At this point the grantee user can accept the request. This will set the `EmergencyAccess.GranteeId` to the `User.Id` of the grantee user. The `EmergencyAccess.Status` is set to `Accepted`. + +If the grantee user does not have an account then they can create an account and accept the invitation. + +### Code +```csharp +// accepts the request to be an emergency contact. +Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService); +``` + +## Step 3. Confirmation + +Once the grantee user has accepted, the `EmergencyAccess.GranteeId` allows the grantor user the ability to query for the `GranteeUser.PublicKey`. With the `Grantee.PublicKey`, the grantor on the client is able to safely encrypt their `User.Key` and save the encrypted string to the database. + +The `EmergencyAccess.Status` is set to `Confirmed`, and the `EmergencyAccess.KeyEncrypted` is set. + +### Code +```csharp +Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); +``` + +## Step 4. Recovery Approval + +The grantee user can now exercise the ability to view or takeover the account. This is done by initiating the recovery. Initiating recovery has a time delay specified by `EmergencyAccess.WaitTime`. `WaitTime` is set in the initial invite. The grantor user can approve the request before the `WaitTime`, but they _cannot_ reject the request _after_ the `WaitTime` has completed. If the recovery request is not rejected then once the `WaitTime` has passed the grantee user will be able to access the emergency access entity. + +### Code +```csharp +// Initiates the recovery process; Will set EmergencyAccess.Status to RecoveryInitiated. +Task InitiateAsync(Guid id, User granteeUser); +// Approved the recovery request; Will set EmergencyAccess.Status to RecoveryApproved. +Task ApproveAsync(Guid id, User approvingUser); +// Rejects the recovery request; Will set EmergencyAccess.Status to Confirmed. +Task RejectAsync(Guid id, User rejectingUser); +// Automatically set the EmergencyAccess.Status to RecoveryApproved after WaitTime has passed. +Task HandleTimedOutRequestsAsync(); +``` + +## Step 5. Recovering the account + +Once the `EmergencyAccess.Status` is `RecoveryApproved` the grantee user is able to exercise their ability to view or takeover the grantor account. Viewing allows the grantee user to view the vault data of the grantor user. Takeover allows the grantee to change the password of the grantor user. + +### Takeover +`TakeoverAsync(Guid, User)` returns the grantor user object along with the `EmergencyAccess` entity. The grantor user object is required since to update the password the client needs access to the grantor kdf configuration. Once the password has been set in the `PasswordAsync(Guid, User, string, string)` the account has been successfully recovered. + +Taking over the account will change the password of the grantor user, empty the two factor array on the grantor user, and disable device verification. + +```csharp +// Takeover returns the grantor user and the emergency access entity. +Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser); +// Password sets the password for the grantor user. +Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key); +// Returns Ciphers the Grantee is allowed to view based on the EmergencyAccess status. +Task ViewAsync(Guid emergencyAccessId, User granteeUser); +// Returns downloadable cipher attachments based on the EmergencyAccess status. +Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser); +``` + +## Optional steps + +The grantor user is able to delete an emergency contact at anytime, at any point in the recovery process. + +### Code +```csharp +// deletes the associated EmergencyAccess Entity +Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); +``` diff --git a/src/Core/Auth/Services/ICaptchaValidationService.cs b/src/Core/Auth/Services/ICaptchaValidationService.cs deleted file mode 100644 index 8547c68f7a..0000000000 --- a/src/Core/Auth/Services/ICaptchaValidationService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Context; -using Bit.Core.Entities; - -namespace Bit.Core.Auth.Services; - -public interface ICaptchaValidationService -{ - string SiteKey { get; } - string SiteKeyResponseKeyName { get; } - bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null); - Task ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress, - User user = null); - string GenerateCaptchaBypassToken(User user); -} diff --git a/src/Core/Auth/Services/IEmergencyAccessService.cs b/src/Core/Auth/Services/IEmergencyAccessService.cs deleted file mode 100644 index 2c94632510..0000000000 --- a/src/Core/Auth/Services/IEmergencyAccessService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models.Data; -using Bit.Core.Entities; -using Bit.Core.Services; -using Bit.Core.Vault.Models.Data; - -namespace Bit.Core.Auth.Services; - -public interface IEmergencyAccessService -{ - Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime); - Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId); - Task AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService); - Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); - Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); - Task GetAsync(Guid emergencyAccessId, Guid userId); - Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser); - Task InitiateAsync(Guid id, User initiatingUser); - Task ApproveAsync(Guid id, User approvingUser); - Task RejectAsync(Guid id, User rejectingUser); - Task> GetPoliciesAsync(Guid id, User requestingUser); - Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser); - Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key); - Task SendNotificationsAsync(); - Task HandleTimedOutRequestsAsync(); - Task ViewAsync(Guid id, User user); - Task GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User user); -} diff --git a/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs b/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs deleted file mode 100644 index cdd6c2017e..0000000000 --- a/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Net.Http.Json; -using System.Text.Json.Serialization; -using Bit.Core.Auth.Models.Business; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Settings; -using Bit.Core.Tokens; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.Auth.Services; - -public class HCaptchaValidationService : ICaptchaValidationService -{ - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly GlobalSettings _globalSettings; - private readonly IDataProtectorTokenFactory _tokenizer; - - public HCaptchaValidationService( - ILogger logger, - IHttpClientFactory httpClientFactory, - IDataProtectorTokenFactory tokenizer, - GlobalSettings globalSettings) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - _globalSettings = globalSettings; - _tokenizer = tokenizer; - } - - public string SiteKeyResponseKeyName => "HCaptcha_SiteKey"; - public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey; - - public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user)); - - public async Task ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress, - User user = null) - { - var response = new CaptchaResponse { Success = false }; - if (string.IsNullOrWhiteSpace(captchaResponse)) - { - return response; - } - - if (user != null && ValidateCaptchaBypassToken(captchaResponse, user)) - { - response.Success = true; - return response; - } - - var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService"); - - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri("https://hcaptcha.com/siteverify"), - Content = new FormUrlEncodedContent(new Dictionary - { - { "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) }, - { "secret", _globalSettings.Captcha.HCaptchaSecretKey }, - { "sitekey", SiteKey }, - { "remoteip", clientIpAddress } - }) - }; - - HttpResponseMessage responseMessage; - try - { - responseMessage = await httpClient.SendAsync(requestMessage); - } - catch (Exception e) - { - _logger.LogError(11389, e, "Unable to verify with HCaptcha."); - return response; - } - - if (!responseMessage.IsSuccessStatusCode) - { - return response; - } - - using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync(); - response.Success = hcaptchaResponse.Success; - var score = hcaptchaResponse.Score.GetValueOrDefault(); - response.MaybeBot = score >= _globalSettings.Captcha.MaybeBotScoreThreshold; - response.IsBot = score >= _globalSettings.Captcha.IsBotScoreThreshold; - response.Score = score; - return response; - } - - public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) - { - if (user == null) - { - return currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired; - } - - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - var requireOnCloud = !_globalSettings.SelfHosted && !user.EmailVerified && - user.CreationDate < DateTime.UtcNow.AddHours(-24); - return currentContext.IsBot || - _globalSettings.Captcha.ForceCaptchaRequired || - requireOnCloud || - failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling; - } - - private static bool TokenIsValidApiKey(string bypassToken, User user) => - !string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken; - - private bool TokenIsValidCaptchaBypassToken(string encryptedToken, User user) - { - return _tokenizer.TryUnprotect(encryptedToken, out var data) && - data.Valid && data.TokenIsValid(user); - } - - private bool ValidateCaptchaBypassToken(string bypassToken, User user) => - TokenIsValidApiKey(bypassToken, user) || TokenIsValidCaptchaBypassToken(bypassToken, user); - - public class HCaptchaResponse : IDisposable - { - [JsonPropertyName("success")] - public bool Success { get; set; } - [JsonPropertyName("score")] - public double? Score { get; set; } - [JsonPropertyName("score_reason")] - public List ScoreReason { get; set; } - - public void Dispose() { } - } -} diff --git a/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs b/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs deleted file mode 100644 index 47e1a38567..0000000000 --- a/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Context; -using Bit.Core.Entities; - -namespace Bit.Core.Auth.Services; - -public class NoopCaptchaValidationService : ICaptchaValidationService -{ - public string SiteKeyResponseKeyName => null; - public string SiteKey => null; - public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false; - public string GenerateCaptchaBypassToken(User user) => ""; - public Task ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress, - User user = null) - { - return Task.FromResult(new CaptchaResponse { Success = true }); - } -} diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 834d2722cc..e721649dc9 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -108,6 +108,7 @@ public class RegisterUserCommand : IRegisterUserCommand var result = await _userService.CreateUserAsync(user, masterPasswordHash); if (result == IdentityResult.Success) { + var sentWelcomeEmail = false; if (!string.IsNullOrEmpty(user.ReferenceData)) { var referenceData = JsonConvert.DeserializeObject>(user.ReferenceData); @@ -115,6 +116,7 @@ public class RegisterUserCommand : IRegisterUserCommand { var initiationPath = value.ToString(); await SendAppropriateWelcomeEmailAsync(user, initiationPath); + sentWelcomeEmail = true; if (!string.IsNullOrEmpty(initiationPath)) { await _referenceEventService.RaiseEventAsync( @@ -128,6 +130,11 @@ public class RegisterUserCommand : IRegisterUserCommand } } + if (!sentWelcomeEmail) + { + await _mailService.SendWelcomeEmailAsync(user); + } + await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs index 203ef3accb..697c10690c 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs @@ -2,6 +2,7 @@ namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; + public interface ITwoFactorIsEnabledQuery { /// @@ -16,7 +17,8 @@ public interface ITwoFactorIsEnabledQuery /// The type of user in the list. Must implement . Task> TwoFactorIsEnabledAsync(IEnumerable users) where T : ITwoFactorProvidersUser; /// - /// Returns whether two factor is enabled for the user. + /// Returns whether two factor is enabled for the user. A user is able to have a TwoFactorProvider that is enabled but requires Premium. + /// If the user does not have premium then the TwoFactorProvider is considered _not_ enabled. /// /// The user to check. Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs index bda2094f24..8d4bd49e42 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs @@ -1,17 +1,13 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; -public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery +public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery { - private readonly IUserRepository _userRepository; - - public TwoFactorIsEnabledQuery(IUserRepository userRepository) - { - _userRepository = userRepository; - } + private readonly IUserRepository _userRepository = userRepository; public async Task> TwoFactorIsEnabledAsync(IEnumerable userIds) { @@ -21,26 +17,15 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery return result; } - var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList()); - + var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]); foreach (var userDetail in userDetails) { - var hasTwoFactor = false; - var providers = userDetail.GetTwoFactorProviders(); - if (providers != null) - { - // Get all enabled providers - var enabledProviderKeys = from provider in providers - where provider.Value?.Enabled ?? false - select provider.Key; - - // Find the first provider that is enabled and passes the premium check - hasTwoFactor = enabledProviderKeys - .Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - } - - result.Add((userDetail.Id, hasTwoFactor)); + result.Add( + (userDetail.Id, + await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(), + () => Task.FromResult(userDetail.HasPremiumAccess)) + ) + ); } return result; @@ -83,41 +68,56 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery return false; } - var providers = user.GetTwoFactorProviders(); - if (providers == null || !providers.Any()) + return await TwoFactorEnabledAsync( + user.GetTwoFactorProviders(), + async () => + { + var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value); + return calcUser?.HasPremiumAccess ?? false; + }); + } + + /// + /// Checks to see what kind of two-factor is enabled. + /// We use a delegate to check if the user has premium access, since there are multiple ways to + /// determine if a user has premium access. + /// + /// dictionary of two factor providers + /// function to check if the user has premium access + /// true if the user has two factor enabled; false otherwise; + private async static Task TwoFactorEnabledAsync( + Dictionary providers, + Func> hasPremiumAccessDelegate) + { + // If there are no providers, then two factor is not enabled + if (providers == null || providers.Count == 0) { return false; } // Get all enabled providers - var enabledProviderKeys = providers - .Where(provider => provider.Value?.Enabled ?? false) - .Select(provider => provider.Key); + // TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into. + var enabledProviderKeys = from provider in providers + where provider.Value?.Enabled ?? false + select provider.Key; + // If no providers are enabled then two factor is not enabled if (!enabledProviderKeys.Any()) { return false; } - // Determine if any enabled provider passes the premium check - var hasTwoFactor = enabledProviderKeys - .Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - - // If no enabled provider passes the check, check the repository for organization premium access - if (!hasTwoFactor) + // If there are only premium two factor options then standard two factor is not enabled + var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium); + if (onlyHasPremiumTwoFactor) { - var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List { userId.Value }); - var userDetail = userDetails.FirstOrDefault(); - - if (userDetail != null) - { - hasTwoFactor = enabledProviderKeys - .Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - } + // There are no Standard two factor options, check if the user has premium access + // If the user has premium access, then two factor is enabled + var premiumAccess = await hasPremiumAccessDelegate(); + return premiumAccess; } - return hasTwoFactor; + // The user has at least one non-premium two factor option + return true; } } diff --git a/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs b/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs deleted file mode 100644 index 052f178165..0000000000 --- a/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bit.Core.Auth.Models.Api; -using Bit.Core.Auth.Services; -using Bit.Core.Context; -using Bit.Core.Exceptions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Auth.Utilities; - -public class CaptchaProtectedAttribute : ActionFilterAttribute -{ - public string ModelParameterName { get; set; } = "model"; - - public override void OnActionExecuting(ActionExecutingContext context) - { - var currentContext = context.HttpContext.RequestServices.GetRequiredService(); - var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService(); - - if (captchaValidationService.RequireCaptchaValidation(currentContext, null)) - { - var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse; - - if (string.IsNullOrWhiteSpace(captchaResponse)) - { - throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey); - } - - var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse, - currentContext.IpAddress, null).GetAwaiter().GetResult(); - if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot) - { - throw new BadRequestException("Captcha is invalid. Please refresh and try again"); - } - } - } -} diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 8a4303e378..28f4dea4b2 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -42,10 +42,12 @@ public static class StripeConstants { public const string Draft = "draft"; public const string Open = "open"; + public const string Paid = "paid"; } public static class MetadataKeys { + public const string BraintreeCustomerId = "btCustomerId"; public const string InvoiceApproved = "invoice_approved"; public const string OrganizationId = "organizationId"; public const string ProviderId = "providerId"; @@ -63,6 +65,11 @@ public static class StripeConstants public const string USBankAccount = "us_bank_account"; } + public static class Prices + { + public const string StoragePlanPersonal = "personal-storage-gb-annually"; + } + public static class ProrationBehavior { public const string AlwaysInvoice = "always_invoice"; @@ -82,6 +89,13 @@ public static class StripeConstants public const string Paused = "paused"; } + public static class TaxExempt + { + public const string Exempt = "exempt"; + public const string None = "none"; + public const string Reverse = "reverse"; + } + public static class ValidateTaxLocationTiming { public const string Deferred = "deferred"; diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs index 3e0c1ea0fb..aa22331f7c 100644 --- a/src/Core/Billing/Extensions/CustomerExtensions.cs +++ b/src/Core/Billing/Extensions/CustomerExtensions.cs @@ -15,12 +15,7 @@ public static class CustomerExtensions } }; - /// - /// Determines if a Stripe customer supports automatic tax - /// - /// - /// - public static bool HasTaxLocationVerified(this Customer customer) => + public static bool HasRecognizedTaxLocation(this Customer customer) => customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation; public static decimal GetBillingBalance(this Customer customer) diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 17285e0676..5c7a42e9b8 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -4,7 +4,9 @@ using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Commands; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; namespace Bit.Core.Billing.Extensions; @@ -24,5 +26,6 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddLicenseServices(); services.AddPricingClient(); + services.AddTransient(); } } diff --git a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs index d70af78fa8..22a715733b 100644 --- a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs +++ b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs @@ -22,7 +22,7 @@ public static class SubscriptionUpdateOptionsExtensions } // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country)) { return false; } diff --git a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs index 88df5638c9..d00b5b46a4 100644 --- a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs +++ b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs @@ -22,7 +22,7 @@ public static class UpcomingInvoiceOptionsExtensions } // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country)) { return false; } diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index 513578f43e..cdfac76614 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -42,6 +42,7 @@ public static class OrganizationLicenseConstants public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod); public const string Trial = nameof(Trial); public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies); + public const string UseOrganizationDomains = nameof(UseOrganizationDomains); } public static class UserLicenseConstants diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 6819d3cc0b..b3f2ab4ec9 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -54,6 +54,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory Products { get; set; } + public int? TrialLength { get; set; } } diff --git a/src/Core/Billing/Models/BillingCommandResult.cs b/src/Core/Billing/Models/BillingCommandResult.cs new file mode 100644 index 0000000000..1b8eefe8df --- /dev/null +++ b/src/Core/Billing/Models/BillingCommandResult.cs @@ -0,0 +1,36 @@ +using OneOf; + +namespace Bit.Core.Billing.Models; + +public record BadRequest(string TranslationKey) +{ + public static BadRequest TaxIdNumberInvalid => new(BillingErrorTranslationKeys.TaxIdInvalid); + public static BadRequest TaxLocationInvalid => new(BillingErrorTranslationKeys.CustomerTaxLocationInvalid); + public static BadRequest UnknownTaxIdType => new(BillingErrorTranslationKeys.UnknownTaxIdType); +} + +public record Unhandled(string TranslationKey = BillingErrorTranslationKeys.UnhandledError); + +public class BillingCommandResult : OneOfBase +{ + private BillingCommandResult(OneOf input) : base(input) { } + + public static implicit operator BillingCommandResult(T output) => new(output); + public static implicit operator BillingCommandResult(BadRequest badRequest) => new(badRequest); + public static implicit operator BillingCommandResult(Unhandled unhandled) => new(unhandled); +} + +public static class BillingErrorTranslationKeys +{ + // "The tax ID number you provided was invalid. Please try again or contact support." + public const string TaxIdInvalid = "taxIdInvalid"; + + // "Your location wasn't recognized. Please ensure your country and postal code are valid and try again." + public const string CustomerTaxLocationInvalid = "customerTaxLocationInvalid"; + + // "Something went wrong with your request. Please contact support." + public const string UnhandledError = "unhandledBillingError"; + + // "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support." + public const string UnknownTaxIdType = "unknownTaxIdType"; +} diff --git a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs index 33b9578d0e..b97390dcc9 100644 --- a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs +++ b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs @@ -1,5 +1,6 @@ using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; +using Bit.Core.Enums; namespace Bit.Core.Billing.Models.Mail; @@ -16,13 +17,26 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail $"&email={Email}" + $"&fromEmail=true" + $"&productTier={(int)ProductTier}" + - $"&product={string.Join(",", Product.Select(p => (int)p))}"; + $"&product={string.Join(",", Product.Select(p => (int)p))}" + + $"&trialLength={TrialLength}"; } + public string VerifyYourEmailHTMLCopy => + TrialLength == 7 + ? "Verify your email address below to finish signing up for your free trial." + : $"Verify your email address below to finish signing up for your {ProductTier.GetDisplayName()} plan."; + + public string VerifyYourEmailTextCopy => + TrialLength == 7 + ? "Verify your email address using the link below and start your free trial of Bitwarden." + : $"Verify your email address using the link below and start your {ProductTier.GetDisplayName()} Bitwarden plan."; + public ProductTierType ProductTier { get; set; } public IEnumerable Product { get; set; } + public int TrialLength { get; set; } + /// /// 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 diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 41666949bf..0f2bf9a454 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -10,7 +10,8 @@ public record OrganizationMetadata( bool IsSubscriptionCanceled, DateTime? InvoiceDueDate, DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate) + DateTime? SubPeriodEndDate, + int OrganizationOccupiedSeats) { public static OrganizationMetadata Default => new OrganizationMetadata( false, @@ -22,5 +23,6 @@ public record OrganizationMetadata( false, null, null, - null); + null, + 0); } diff --git a/src/Core/Billing/Models/PaymentMethod.cs b/src/Core/Billing/Models/PaymentMethod.cs index b07fe82e46..2b8c59fa05 100644 --- a/src/Core/Billing/Models/PaymentMethod.cs +++ b/src/Core/Billing/Models/PaymentMethod.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; + +namespace Bit.Core.Billing.Models; public record PaymentMethod( long AccountCredit, diff --git a/src/Core/Billing/Models/Sales/CustomerSetup.cs b/src/Core/Billing/Models/Sales/CustomerSetup.cs index bb4f2352e3..aa67c712b5 100644 --- a/src/Core/Billing/Models/Sales/CustomerSetup.cs +++ b/src/Core/Billing/Models/Sales/CustomerSetup.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Tax.Models; + +namespace Bit.Core.Billing.Models.Sales; #nullable enable diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 0602cf1dd9..78ad26871b 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Models.Business; namespace Bit.Core.Billing.Models.Sales; @@ -26,12 +27,21 @@ public class OrganizationSale public static OrganizationSale From( Organization organization, - OrganizationSignup signup) => new() + OrganizationSignup signup) + { + var customerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null; + + var subscriptionSetup = GetSubscriptionSetup(signup); + + subscriptionSetup.SkipTrial = signup.SkipTrial; + + return new OrganizationSale { Organization = organization, - CustomerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null, - SubscriptionSetup = GetSubscriptionSetup(signup) + CustomerSetup = customerSetup, + SubscriptionSetup = subscriptionSetup }; + } public static OrganizationSale From( Organization organization, diff --git a/src/Core/Billing/Models/Sales/PremiumUserSale.cs b/src/Core/Billing/Models/Sales/PremiumUserSale.cs index 6bc054eac5..8c9b696aa3 100644 --- a/src/Core/Billing/Models/Sales/PremiumUserSale.cs +++ b/src/Core/Billing/Models/Sales/PremiumUserSale.cs @@ -1,4 +1,5 @@ -using Bit.Core.Entities; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/Billing/Models/StaticStore/Plan.cs b/src/Core/Billing/Models/StaticStore/Plan.cs index 17aa78aa06..d710594f46 100644 --- a/src/Core/Billing/Models/StaticStore/Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plan.cs @@ -24,6 +24,7 @@ public abstract record Plan public bool Has2fa { get; protected init; } public bool HasApi { get; protected init; } public bool HasSso { get; protected init; } + public bool HasOrganizationDomains { get; protected init; } public bool HasKeyConnector { get; protected init; } public bool HasScim { get; protected init; } public bool HasResetPassword { get; protected init; } diff --git a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs index 72db7897b4..b584647a26 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs @@ -26,6 +26,7 @@ public record Enterprise2019Plan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs index 42b984e7e5..a1a6113cbc 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs @@ -26,6 +26,7 @@ public record Enterprise2020Plan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs index 2d498a7654..8aeca521d1 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs @@ -26,6 +26,7 @@ public record EnterprisePlan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs index 8cd8335425..dce1719a49 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs @@ -26,6 +26,7 @@ public record Enterprise2023Plan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs b/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs index b0ca8feeb0..93ab2c39a1 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs @@ -38,7 +38,7 @@ public record Families2019Plan : Plan HasPremiumAccessOption = true; StripePlanId = "personal-org-annually"; - StripeStoragePlanId = "storage-gb-annually"; + StripeStoragePlanId = "personal-storage-gb-annually"; StripePremiumAccessPlanId = "personal-org-premium-access-annually"; BasePrice = 12; AdditionalStoragePricePerGb = 4; diff --git a/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs b/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs index e2f51ec913..8c71e50fa4 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs @@ -37,7 +37,7 @@ public record FamiliesPlan : Plan HasAdditionalStorageOption = true; StripePlanId = "2020-families-org-annually"; - StripeStoragePlanId = "storage-gb-annually"; + StripeStoragePlanId = "personal-storage-gb-annually"; BasePrice = 40; AdditionalStoragePricePerGb = 4; diff --git a/src/Core/Billing/Pricing/PlanAdapter.cs b/src/Core/Billing/Pricing/PlanAdapter.cs index c38eb0501d..f719fd1e87 100644 --- a/src/Core/Billing/Pricing/PlanAdapter.cs +++ b/src/Core/Billing/Pricing/PlanAdapter.cs @@ -26,6 +26,7 @@ public record PlanAdapter : Plan Has2fa = HasFeature("2fa"); HasApi = HasFeature("api"); HasSso = HasFeature("sso"); + HasOrganizationDomains = HasFeature("organizationDomains"); HasKeyConnector = HasFeature("keyConnector"); HasScim = HasFeature("scim"); HasResetPassword = HasFeature("resetPassword"); diff --git a/src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs b/src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs similarity index 88% rename from src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs rename to src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs index 1e719b3ceb..bbb0a90b04 100644 --- a/src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs +++ b/src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs @@ -1,12 +1,12 @@ -using System.ComponentModel.DataAnnotations; +#nullable enable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Utilities; -#nullable enable - -namespace Bit.Core.Billing.Entities; +namespace Bit.Core.Billing.Providers.Entities; public class ClientOrganizationMigrationRecord : ITableObject { diff --git a/src/Core/Billing/Entities/ProviderInvoiceItem.cs b/src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs similarity index 87% rename from src/Core/Billing/Entities/ProviderInvoiceItem.cs rename to src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs index 566d7514e7..9d9eeda754 100644 --- a/src/Core/Billing/Entities/ProviderInvoiceItem.cs +++ b/src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs @@ -1,10 +1,10 @@ -using System.ComponentModel.DataAnnotations; +#nullable enable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Utilities; -#nullable enable - -namespace Bit.Core.Billing.Entities; +namespace Bit.Core.Billing.Providers.Entities; public class ProviderInvoiceItem : ITableObject { diff --git a/src/Core/Billing/Entities/ProviderPlan.cs b/src/Core/Billing/Providers/Entities/ProviderPlan.cs similarity index 86% rename from src/Core/Billing/Entities/ProviderPlan.cs rename to src/Core/Billing/Providers/Entities/ProviderPlan.cs index fd131f64e6..d06c81e9ce 100644 --- a/src/Core/Billing/Entities/ProviderPlan.cs +++ b/src/Core/Billing/Providers/Entities/ProviderPlan.cs @@ -1,10 +1,10 @@ -using Bit.Core.Billing.Enums; +#nullable enable + +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Utilities; -#nullable enable - -namespace Bit.Core.Billing.Entities; +namespace Bit.Core.Billing.Providers.Entities; public class ProviderPlan : ITableObject { diff --git a/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs similarity index 90% rename from src/Core/Billing/Migration/Models/ClientMigrationTracker.cs rename to src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs index 69398004fd..ae0c28de86 100644 --- a/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs +++ b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Migration.Models; +namespace Bit.Core.Billing.Providers.Migration.Models; public enum ClientMigrationProgress { diff --git a/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs similarity index 93% rename from src/Core/Billing/Migration/Models/ProviderMigrationResult.cs rename to src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs index 137ba8bd0d..6f3c3be11d 100644 --- a/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs +++ b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs @@ -1,6 +1,6 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; -namespace Bit.Core.Billing.Migration.Models; +namespace Bit.Core.Billing.Providers.Migration.Models; public class ProviderMigrationResult { diff --git a/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs similarity index 90% rename from src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs rename to src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs index 7bfef8a931..f4708d4cbd 100644 --- a/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs +++ b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Migration.Models; +namespace Bit.Core.Billing.Providers.Migration.Models; public enum ProviderMigrationProgress { diff --git a/src/Core/Billing/Migration/ServiceCollectionExtensions.cs b/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs similarity index 71% rename from src/Core/Billing/Migration/ServiceCollectionExtensions.cs rename to src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs index 109259d59a..1061c82888 100644 --- a/src/Core/Billing/Migration/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs @@ -1,8 +1,8 @@ -using Bit.Core.Billing.Migration.Services; -using Bit.Core.Billing.Migration.Services.Implementations; +using Bit.Core.Billing.Providers.Migration.Services; +using Bit.Core.Billing.Providers.Migration.Services.Implementations; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Billing.Migration; +namespace Bit.Core.Billing.Providers.Migration; public static class ServiceCollectionExtensions { diff --git a/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs b/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs similarity index 85% rename from src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs rename to src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs index 6734c69566..70649590df 100644 --- a/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs +++ b/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Models; -namespace Bit.Core.Billing.Migration.Services; +namespace Bit.Core.Billing.Providers.Migration.Services; public interface IMigrationTrackerCache { diff --git a/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs similarity index 72% rename from src/Core/Billing/Migration/Services/IOrganizationMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs index 7bc9443717..a0548277b4 100644 --- a/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.Entities; -namespace Bit.Core.Billing.Migration.Services; +namespace Bit.Core.Billing.Providers.Migration.Services; public interface IOrganizationMigrator { diff --git a/src/Core/Billing/Migration/Services/IProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs similarity index 55% rename from src/Core/Billing/Migration/Services/IProviderMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs index 9ca14e7fd9..328c2419f4 100644 --- a/src/Core/Billing/Migration/Services/IProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs @@ -1,6 +1,6 @@ -using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Models; -namespace Bit.Core.Billing.Migration.Services; +namespace Bit.Core.Billing.Providers.Migration.Services; public interface IProviderMigrator { diff --git a/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs similarity index 96% rename from src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs rename to src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs index 920bc55392..ea7d118cfa 100644 --- a/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Models; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Billing.Migration.Services.Implementations; +namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; public class MigrationTrackerDistributedCache( [FromKeyedServices("persistent")] diff --git a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs similarity index 97% rename from src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs index 4d93c0119a..3b874579e5 100644 --- a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs @@ -1,10 +1,10 @@ using Bit.Core.AdminConsole.Entities; 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.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Migration.Models; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using Stripe; using Plan = Bit.Core.Models.StaticStore.Plan; -namespace Bit.Core.Billing.Migration.Services.Implementations; +namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; public class OrganizationMigrator( IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, @@ -309,6 +309,7 @@ public class OrganizationMigrator( organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.UsePolicies = plan.HasPolicies; organization.UseSso = plan.HasSso; + organization.UseOrganizationDomains = plan.HasOrganizationDomains; organization.UseGroups = plan.HasGroups; organization.UseEvents = plan.HasEvents; organization.UseDirectory = plan.HasDirectory; diff --git a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs similarity index 98% rename from src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs index 384cfca1d1..3a0b579dcf 100644 --- a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs @@ -3,18 +3,18 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; 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.Repositories; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Migration.Models; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.Extensions.Logging; using Stripe; -namespace Bit.Core.Billing.Migration.Services.Implementations; +namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; public class ProviderMigrator( IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, diff --git a/src/Core/Billing/Models/AddableOrganization.cs b/src/Core/Billing/Providers/Models/AddableOrganization.cs similarity index 72% rename from src/Core/Billing/Models/AddableOrganization.cs rename to src/Core/Billing/Providers/Models/AddableOrganization.cs index fe6d5458bd..aca7a158b0 100644 --- a/src/Core/Billing/Models/AddableOrganization.cs +++ b/src/Core/Billing/Providers/Models/AddableOrganization.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Providers.Models; public record AddableOrganization( Guid Id, diff --git a/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs b/src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs similarity index 80% rename from src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs rename to src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs index 385782c8ad..053d912291 100644 --- a/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs +++ b/src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Enums; -namespace Bit.Core.Billing.Services.Contracts; +namespace Bit.Core.Billing.Providers.Models; public record ChangeProviderPlanCommand( Provider Provider, diff --git a/src/Core/Billing/Models/ConfiguredProviderPlan.cs b/src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs similarity index 75% rename from src/Core/Billing/Models/ConfiguredProviderPlan.cs rename to src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs index 72c1ec5b07..d875106a9e 100644 --- a/src/Core/Billing/Models/ConfiguredProviderPlan.cs +++ b/src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs @@ -1,11 +1,12 @@ using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Providers.Models; public record ConfiguredProviderPlan( Guid Id, Guid ProviderId, Plan Plan, + decimal Price, int SeatMinimum, int PurchasedSeats, int AssignedSeats); diff --git a/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs b/src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs similarity index 89% rename from src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs rename to src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs index 2d2535b60a..dfd04e6605 100644 --- a/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs +++ b/src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Enums; -namespace Bit.Core.Billing.Services.Contracts; +namespace Bit.Core.Billing.Providers.Models; /// The provider to update the seat minimums for. /// The new seat minimums for the provider. diff --git a/src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs b/src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs similarity index 77% rename from src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs rename to src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs index 2165984383..53eb51403f 100644 --- a/src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs +++ b/src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Providers.Repositories; public interface IClientOrganizationMigrationRecordRepository : IRepository { diff --git a/src/Core/Billing/Repositories/IProviderInvoiceItemRepository.cs b/src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs similarity index 74% rename from src/Core/Billing/Repositories/IProviderInvoiceItemRepository.cs rename to src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs index a722d4cf9d..931d8a9186 100644 --- a/src/Core/Billing/Repositories/IProviderInvoiceItemRepository.cs +++ b/src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Providers.Repositories; public interface IProviderInvoiceItemRepository : IRepository { diff --git a/src/Core/Billing/Repositories/IProviderPlanRepository.cs b/src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs similarity index 64% rename from src/Core/Billing/Repositories/IProviderPlanRepository.cs rename to src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs index eccbad82bb..d1cf91ea56 100644 --- a/src/Core/Billing/Repositories/IProviderPlanRepository.cs +++ b/src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Providers.Repositories; public interface IProviderPlanRepository : IRepository { diff --git a/src/Core/Billing/Services/IBusinessUnitConverter.cs b/src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs similarity index 98% rename from src/Core/Billing/Services/IBusinessUnitConverter.cs rename to src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs index 06ff883eae..99df6b1bef 100644 --- a/src/Core/Billing/Services/IBusinessUnitConverter.cs +++ b/src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using OneOf; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Providers.Services; public interface IBusinessUnitConverter { diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Providers/Services/IProviderBillingService.cs similarity index 93% rename from src/Core/Billing/Services/IProviderBillingService.cs rename to src/Core/Billing/Providers/Services/IProviderBillingService.cs index 64585f3361..b634f1a81c 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Providers/Services/IProviderBillingService.cs @@ -1,13 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Models.Business; using Stripe; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Providers.Services; public interface IProviderBillingService { @@ -59,7 +60,7 @@ public interface IProviderBillingService int seatAdjustment); /// - /// Determines whether the provided will result in a purchase for the 's . + /// Determines whether the provided will result in a purchase for the 's . /// Seat adjustments that result in purchases include: /// /// The going from below the seat minimum to above the seat minimum for the provided @@ -79,10 +80,12 @@ public interface IProviderBillingService /// /// The to create a Stripe customer for. /// The to use for calculating the customer's automatic tax. + /// The (ex. Credit Card) to attach to the customer. /// The newly created for the . Task SetupCustomer( Provider provider, - TaxInfo taxInfo); + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource = null); /// /// For use during the provider setup process, this method starts a Stripe for the given . diff --git a/src/Core/Billing/Services/IOrganizationBillingService.cs b/src/Core/Billing/Services/IOrganizationBillingService.cs index db62d545e3..5f7d33f118 100644 --- a/src/Core/Billing/Services/IOrganizationBillingService.cs +++ b/src/Core/Billing/Services/IOrganizationBillingService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Tax.Models; namespace Bit.Core.Billing.Services; diff --git a/src/Core/Billing/Services/IPremiumUserBillingService.cs b/src/Core/Billing/Services/IPremiumUserBillingService.cs index b3bb580e2d..ed7a003599 100644 --- a/src/Core/Billing/Services/IPremiumUserBillingService.cs +++ b/src/Core/Billing/Services/IPremiumUserBillingService.cs @@ -1,5 +1,6 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; namespace Bit.Core.Billing.Services; diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index bb0a23020c..6910948436 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Stripe; diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 2e902ca028..c647e825b6 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -1,11 +1,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -29,20 +31,20 @@ public class OrganizationBillingService( IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, IPricingClient pricingClient, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - ITaxService taxService, - IAutomaticTaxFactory automaticTaxFactory) : IOrganizationBillingService + ITaxService taxService) : IOrganizationBillingService { public async Task Finalize(OrganizationSale sale) { var (organization, customerSetup, subscriptionSetup) = sale; var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null - ? await CreateCustomerAsync(organization, customerSetup) - : await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); + ? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType) + : await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup); var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup); @@ -106,6 +108,8 @@ public class OrganizationBillingService( ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) : null; + var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + return new OrganizationMetadata( isEligibleForSelfHost, isManaged, @@ -116,10 +120,12 @@ public class OrganizationBillingService( subscription.Status == StripeConstants.SubscriptionStatus.Canceled, invoice?.DueDate, invoice?.Created, - subscription.CurrentPeriodEnd); + subscription.CurrentPeriodEnd, + orgOccupiedSeats); } - public async Task UpdatePaymentMethod( + public async Task + UpdatePaymentMethod( Organization organization, TokenizedPaymentSource tokenizedPaymentSource, TaxInformation taxInformation) @@ -149,8 +155,11 @@ public class OrganizationBillingService( private async Task CreateCustomerAsync( Organization organization, - CustomerSetup customerSetup) + CustomerSetup customerSetup, + PlanType? updatedPlanType = null) { + var planType = updatedPlanType ?? organization.PlanType; + var displayName = organization.DisplayName(); var customerCreateOptions = new CustomerCreateOptions @@ -210,13 +219,24 @@ public class OrganizationBillingService( City = customerSetup.TaxInformation.City, PostalCode = customerSetup.TaxInformation.PostalCode, State = customerSetup.TaxInformation.State, - Country = customerSetup.TaxInformation.Country, + Country = customerSetup.TaxInformation.Country }; + customerCreateOptions.Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && + planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && + customerSetup.TaxInformation.Country != "US") + { + customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; + } + if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) { var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, @@ -397,21 +417,68 @@ public class OrganizationBillingService( TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays }; - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriptionSetup.PlanType); - var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters); - automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else + else if (customer.HasRecognizedTaxLocation()) { - subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions(); - subscriptionCreateOptions.AutomaticTax.Enabled = customer.HasBillingLocation(); + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = + subscriptionSetup.PlanType.GetProductTier() == ProductTierType.Families || + customer.Address.Country == "US" || + customer.TaxIds.Any() + }; } return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); } + private async Task GetCustomerWhileEnsuringCorrectTaxExemptionAsync( + Organization organization, + SubscriptionSetup subscriptionSetup) + { + var customer = await subscriberService.GetCustomerOrThrow(organization, + new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); + + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (!setNonUSBusinessUseToReverseCharge || subscriptionSetup.PlanType.GetProductTier() is + not (ProductTierType.Teams or + ProductTierType.TeamsStarter or + ProductTierType.Enterprise)) + { + return customer; + } + + List expansions = ["tax", "tax_ids"]; + + customer = customer switch + { + { Address.Country: not "US", TaxExempt: not StripeConstants.TaxExempt.Reverse } => await + stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions + { + Expand = expansions, + TaxExempt = StripeConstants.TaxExempt.Reverse + }), + { Address.Country: "US", TaxExempt: StripeConstants.TaxExempt.Reverse } => await + stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions + { + Expand = expansions, + TaxExempt = StripeConstants.TaxExempt.None + }), + _ => customer + }; + + return customer; + } + private async Task IsEligibleForSelfHostAsync( Organization organization) { diff --git a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs index 6e984f946e..5a8cf16f5a 100644 --- a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs +++ b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs @@ -5,14 +5,12 @@ using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; namespace Bit.Core.Billing.Services.Implementations; public class PaymentHistoryService( IStripeAdapter stripeAdapter, - ITransactionRepository transactionRepository, - ILogger logger) : IPaymentHistoryService + ITransactionRepository transactionRepository) : IPaymentHistoryService { public async Task> GetInvoiceHistoryAsync( ISubscriber subscriber, diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 6746a8cc98..7496157aaa 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -10,7 +10,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Braintree; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Stripe; using Customer = Stripe.Customer; @@ -22,20 +21,18 @@ using static Utilities; public class PremiumUserBillingService( IBraintreeGateway braintreeGateway, - IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository, - [FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy automaticTaxStrategy) : IPremiumUserBillingService + IUserRepository userRepository) : IPremiumUserBillingService { public async Task Credit(User user, decimal amount) { var customer = await subscriberService.GetCustomer(user); - // Negative credit represents a balance and all Stripe denomination is in cents. + // Negative credit represents a balance, and all Stripe denomination is in cents. var credit = (long)(amount * -100); if (customer == null) @@ -182,7 +179,7 @@ public class PremiumUserBillingService( City = customerSetup.TaxInformation.City, PostalCode = customerSetup.TaxInformation.PostalCode, State = customerSetup.TaxInformation.State, - Country = customerSetup.TaxInformation.Country, + Country = customerSetup.TaxInformation.Country }, Description = user.Name, Email = user.Email, @@ -313,7 +310,7 @@ public class PremiumUserBillingService( { subscriptionItemOptionsList.Add(new SubscriptionItemOptions { - Price = "storage-gb-annually", + Price = StripeConstants.Prices.StoragePlanPersonal, Quantity = storage }); } @@ -322,6 +319,10 @@ public class PremiumUserBillingService( var subscriptionCreateOptions = new SubscriptionCreateOptions { + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, Items = subscriptionItemOptionsList, @@ -335,18 +336,6 @@ public class PremiumUserBillingService( OffSession = true }; - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); - } - else - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, - }; - } - var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); if (usingPayPal) @@ -378,7 +367,7 @@ public class PremiumUserBillingService( City = taxInformation.City, PostalCode = taxInformation.PostalCode, State = taxInformation.State, - Country = taxInformation.Country, + Country = taxInformation.Country }, Expand = ["tax"], Tax = new CustomerTaxOptions diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 1b0e5b665b..75a1bf76ec 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -1,7 +1,12 @@ -using Bit.Core.Billing.Caches; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -26,8 +31,7 @@ public class SubscriberService( ILogger logger, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ITaxService taxService, - IAutomaticTaxFactory automaticTaxFactory) : ISubscriberService + ITaxService taxService) : ISubscriberService { public async Task CancelSubscription( ISubscriber subscriber, @@ -126,7 +130,7 @@ public class SubscriberService( [subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion }, Email = subscriber.BillingEmailAddress(), - PaymentMethodNonce = paymentMethodNonce, + PaymentMethodNonce = paymentMethodNonce }); if (customerResult.IsSuccess()) @@ -480,7 +484,7 @@ public class SubscriberService( var matchingSetupIntent = setupIntentsForUpdatedPaymentMethod.First(); - // Find the customer's existing setup intents that should be cancelled. + // Find the customer's existing setup intents that should be canceled. var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer) .Where(si => si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action"); @@ -517,7 +521,7 @@ public class SubscriberService( await stripeAdapter.PaymentMethodAttachAsync(token, new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); - // Find the customer's existing setup intents that should be cancelled. + // Find the customer's existing setup intents that should be canceled. var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer) .Where(si => si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action"); @@ -635,7 +639,8 @@ public class SubscriberService( logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", taxInformation.Country, taxInformation.TaxId); - throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); + + throw new BadRequestException("billingTaxIdTypeInferenceError"); } } @@ -652,53 +657,84 @@ public class SubscriberService( logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", taxInformation.TaxId, taxInformation.Country); - throw new Exceptions.BadRequestException("billingInvalidTaxIdError"); + + throw new BadRequestException("billingInvalidTaxIdError"); + default: logger.LogError(e, "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", taxInformation.TaxId, taxInformation.Country, customer.Id); - throw new Exceptions.BadRequestException("billingTaxIdCreationError"); + + throw new BadRequestException("billingTaxIdCreationError"); } } } - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var subscription = + customer.Subscriptions.First(subscription => subscription.Id == subscriber.GatewaySubscriptionId); + + var isBusinessUseSubscriber = subscriber switch { - if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) + Organization organization => organization.PlanType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families, + Provider => true, + _ => false + }; + + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && isBusinessUseSubscriber) + { + switch (customer) { - var subscriptionGetOptions = new SubscriptionGetOptions + case { - Expand = ["customer.tax", "customer.tax_ids"] - }; - var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions); - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id)); - var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters); - var automaticTaxOptions = automaticTaxStrategy.GetUpdateOptions(subscription); - if (automaticTaxOptions?.AutomaticTax?.Enabled != null) + Address.Country: not "US", + TaxExempt: not StripeConstants.TaxExempt.Reverse + }: + await stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + break; + case { - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, automaticTaxOptions); - } + Address.Country: "US", + TaxExempt: StripeConstants.TaxExempt.Reverse + }: + await stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.None }); + break; } - } - else - { - if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) + + if (!subscription.AutomaticTax.Enabled) { - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + await stripeAdapter.SubscriptionUpdateAsync(subscription.Id, new SubscriptionUpdateOptions { AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }); } + } + else + { + var automaticTaxShouldBeEnabled = subscriber switch + { + User => true, + Organization organization => organization.PlanType.GetProductTier() == ProductTierType.Families || + customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false), + Provider => customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false), + _ => false + }; - 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; + if (automaticTaxShouldBeEnabled && !subscription.AutomaticTax.Enabled) + { + await stripeAdapter.SubscriptionUpdateAsync(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } } } diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs new file mode 100644 index 0000000000..304abbaae0 --- /dev/null +++ b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs @@ -0,0 +1,147 @@ +#nullable enable +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Tax.Commands; + +public interface IPreviewTaxAmountCommand +{ + Task> Run(OrganizationTrialParameters parameters); +} + +public class PreviewTaxAmountCommand( + ILogger logger, + IPricingClient pricingClient, + IStripeAdapter stripeAdapter, + ITaxService taxService) : IPreviewTaxAmountCommand +{ + public async Task> Run(OrganizationTrialParameters parameters) + { + var (planType, productType, taxInformation) = parameters; + + var plan = await pricingClient.GetPlanOrThrow(planType); + + var options = new InvoiceCreatePreviewOptions + { + Currency = "usd", + CustomerDetails = new InvoiceCustomerDetailsOptions + { + Address = new AddressOptions + { + Country = taxInformation.Country, + PostalCode = taxInformation.PostalCode + } + }, + SubscriptionDetails = new InvoiceSubscriptionDetailsOptions + { + Items = [ + new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.HasNonSeatBasedPasswordManagerPlan() ? plan.PasswordManager.StripePlanId : plan.PasswordManager.StripeSeatPlanId, + Quantity = 1 + } + ] + } + }; + + if (productType == ProductType.SecretsManager) + { + options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.SecretsManager.StripeSeatPlanId, + Quantity = 1 + }); + + options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone; + } + + if (!string.IsNullOrEmpty(taxInformation.TaxId)) + { + var taxIdType = taxService.GetStripeTaxCode( + taxInformation.Country, + taxInformation.TaxId); + + if (string.IsNullOrEmpty(taxIdType)) + { + return BadRequest.UnknownTaxIdType; + } + + options.CustomerDetails.TaxIds = [ + new InvoiceCustomerDetailsTaxIdOptions + { + Type = taxIdType, + Value = taxInformation.TaxId + } + ]; + } + + if (planType.GetProductTier() == ProductTierType.Families) + { + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + } + else + { + options.AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = options.CustomerDetails.Address.Country == "US" || + options.CustomerDetails.TaxIds is [_, ..] + }; + } + + try + { + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return Convert.ToDecimal(invoice.Tax) / 100; + } + catch (StripeException stripeException) when (stripeException.StripeError.Code == + StripeConstants.ErrorCodes.CustomerTaxLocationInvalid) + { + return BadRequest.TaxLocationInvalid; + } + catch (StripeException stripeException) when (stripeException.StripeError.Code == + StripeConstants.ErrorCodes.TaxIdInvalid) + { + return BadRequest.TaxIdNumberInvalid; + } + catch (StripeException stripeException) + { + logger.LogError(stripeException, "Stripe responded with an error during {Operation}. Code: {Code}", nameof(PreviewTaxAmountCommand), stripeException.StripeError.Code); + return new Unhandled(); + } + } +} + +#region Command Parameters + +public record OrganizationTrialParameters +{ + public required PlanType PlanType { get; set; } + public required ProductType ProductType { get; set; } + public required TaxInformationDTO TaxInformation { get; set; } + + public void Deconstruct( + out PlanType planType, + out ProductType productType, + out TaxInformationDTO taxInformation) + { + planType = PlanType; + productType = ProductType; + taxInformation = TaxInformation; + } + + public record TaxInformationDTO + { + public required string Country { get; set; } + public required string PostalCode { get; set; } + public string? TaxId { get; set; } + } +} + +#endregion diff --git a/src/Core/Billing/Services/Contracts/AutomaticTaxFactoryParameters.cs b/src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs similarity index 93% rename from src/Core/Billing/Services/Contracts/AutomaticTaxFactoryParameters.cs rename to src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs index 19a4f0bdfa..a58daa9c48 100644 --- a/src/Core/Billing/Services/Contracts/AutomaticTaxFactoryParameters.cs +++ b/src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; -namespace Bit.Core.Billing.Services.Contracts; +namespace Bit.Core.Billing.Tax.Models; public class AutomaticTaxFactoryParameters { diff --git a/src/Core/Billing/Models/TaxIdType.cs b/src/Core/Billing/Tax/Models/TaxIdType.cs similarity index 92% rename from src/Core/Billing/Models/TaxIdType.cs rename to src/Core/Billing/Tax/Models/TaxIdType.cs index 3fc246d68b..6f8cfdde99 100644 --- a/src/Core/Billing/Models/TaxIdType.cs +++ b/src/Core/Billing/Tax/Models/TaxIdType.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Tax.Models; public class TaxIdType { diff --git a/src/Core/Billing/Models/TaxInformation.cs b/src/Core/Billing/Tax/Models/TaxInformation.cs similarity index 93% rename from src/Core/Billing/Models/TaxInformation.cs rename to src/Core/Billing/Tax/Models/TaxInformation.cs index 23ed3e5faa..2408ee0ecd 100644 --- a/src/Core/Billing/Models/TaxInformation.cs +++ b/src/Core/Billing/Tax/Models/TaxInformation.cs @@ -1,6 +1,6 @@ using Bit.Core.Models.Business; -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Tax.Models; public record TaxInformation( string Country, diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs b/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs similarity index 87% rename from src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs rename to src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs index 8597cea09b..340f07b56c 100644 --- a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Bit.Core.Billing.Models.Api.Requests.Accounts; +namespace Bit.Core.Billing.Tax.Requests; public class PreviewIndividualInvoiceRequestBody { diff --git a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs b/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs similarity index 93% rename from src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs rename to src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs index 461a6dca65..bfb47e7b2c 100644 --- a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Enums; -namespace Bit.Core.Billing.Models.Api.Requests.Organizations; +namespace Bit.Core.Billing.Tax.Requests; public class PreviewOrganizationInvoiceRequestBody { diff --git a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs b/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs similarity index 84% rename from src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs rename to src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs index 9cb43645c6..13d4870ac5 100644 --- a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Bit.Core.Billing.Models.Api.Requests; +namespace Bit.Core.Billing.Tax.Requests; public class TaxInformationRequestModel { diff --git a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs b/src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs similarity index 74% rename from src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs rename to src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs index fdde7dae1e..2753487e2f 100644 --- a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs +++ b/src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Models.Api.Responses; +namespace Bit.Core.Billing.Tax.Responses; public record PreviewInvoiceResponseModel( decimal EffectiveTaxRate, diff --git a/src/Core/Billing/Services/IAutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs similarity index 76% rename from src/Core/Billing/Services/IAutomaticTaxFactory.cs rename to src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs index c52a8f2671..c0a31efb3c 100644 --- a/src/Core/Billing/Services/IAutomaticTaxFactory.cs +++ b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs @@ -1,6 +1,6 @@ -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services; /// /// Responsible for defining the correct automatic tax strategy for either personal use of business use. diff --git a/src/Core/Billing/Services/IAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs similarity index 96% rename from src/Core/Billing/Services/IAutomaticTaxStrategy.cs rename to src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs index 292f2d0939..557bb1d30c 100644 --- a/src/Core/Billing/Services/IAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs @@ -1,7 +1,7 @@ #nullable enable using Stripe; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services; public interface IAutomaticTaxStrategy { diff --git a/src/Core/Billing/Services/ITaxService.cs b/src/Core/Billing/Tax/Services/ITaxService.cs similarity index 94% rename from src/Core/Billing/Services/ITaxService.cs rename to src/Core/Billing/Tax/Services/ITaxService.cs index beee113d17..00cbf56a9b 100644 --- a/src/Core/Billing/Services/ITaxService.cs +++ b/src/Core/Billing/Tax/Services/ITaxService.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services; public interface ITaxService { diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/AutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs similarity index 93% rename from src/Core/Billing/Services/Implementations/AutomaticTax/AutomaticTaxFactory.cs rename to src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs index 133cd2c7a7..6086a16b79 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/AutomaticTaxFactory.cs +++ b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs @@ -1,11 +1,11 @@ #nullable enable using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Services; -namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class AutomaticTaxFactory( IFeatureService featureService, diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs similarity index 95% rename from src/Core/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategy.cs rename to src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs index 40eb6e4540..6affc57354 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs @@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Services; using Stripe; -namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy { @@ -76,7 +76,7 @@ public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : I private bool ShouldBeEnabled(Customer customer) { - if (!customer.HasTaxLocationVerified()) + if (!customer.HasRecognizedTaxLocation()) { return false; } diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs similarity index 93% rename from src/Core/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategy.cs rename to src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs index 15ee1adf8f..615222259e 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs @@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Services; using Stripe; -namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy { @@ -59,6 +59,6 @@ public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : I private static bool ShouldBeEnabled(Customer customer) { - return customer.HasTaxLocationVerified(); + return customer.HasRecognizedTaxLocation(); } } diff --git a/src/Core/Billing/Services/TaxService.cs b/src/Core/Billing/Tax/Services/Implementations/TaxService.cs similarity index 99% rename from src/Core/Billing/Services/TaxService.cs rename to src/Core/Billing/Tax/Services/Implementations/TaxService.cs index 3066be92d1..204c997335 100644 --- a/src/Core/Billing/Services/TaxService.cs +++ b/src/Core/Billing/Tax/Services/Implementations/TaxService.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class TaxService : ITaxService { diff --git a/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs index 01550228be..6ec31d7b8f 100644 --- a/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs @@ -10,5 +10,6 @@ public interface ISendTrialInitiationEmailForRegistrationCommand string? name, bool receiveMarketingEmails, ProductTierType productTier, - IEnumerable products); + IEnumerable products, + int trialLength); } diff --git a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs index 385d7ebbd6..3e5b056ec6 100644 --- a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs @@ -22,7 +22,8 @@ public class SendTrialInitiationEmailForRegistrationCommand( string? name, bool receiveMarketingEmails, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trialLength) { ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email)); @@ -43,7 +44,12 @@ public class SendTrialInitiationEmailForRegistrationCommand( await PerformConstantTimeOperationsAsync(); - await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products); + if (trialLength != 0 && trialLength != 7) + { + trialLength = 7; + } + + await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products, trialLength); return null; } diff --git a/src/Core/Billing/Utilities.cs b/src/Core/Billing/Utilities.cs index 695a3b1bb4..ebb7b0e525 100644 --- a/src/Core/Billing/Utilities.cs +++ b/src/Core/Billing/Utilities.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Services; using Stripe; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5f0265df41..78a8364695 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -109,15 +109,15 @@ public static class FeatureFlagKeys public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; + public const string OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript"; + public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; - public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; - public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; - public const string NewDeviceVerification = "new-device-verification"; + public const string BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; @@ -145,10 +145,14 @@ public static class FeatureFlagKeys public const string UsePricingService = "use-pricing-service"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; - public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; + public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; + public const string UseOrganizationWarningsService = "use-organization-warnings-service"; + public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; + public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; + public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; @@ -169,8 +173,6 @@ public static class FeatureFlagKeys public const string NativeCreateAccountFlow = "native-create-account-flow"; public const string AndroidImportLoginsFlow = "import-logins-flow"; public const string AppReviewPrompt = "app-review-prompt"; - public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; - public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public const string AndroidMutualTls = "mutual-tls"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; @@ -195,14 +197,13 @@ public static class FeatureFlagKeys /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; 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 RestrictProviderAccess = "restrict-provider-access"; public const string SecurityTasks = "security-tasks"; public const string CipherKeyEncryption = "cipher-key-encryption"; public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; + public const string PhishingDetection = "phishing-detection"; + public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy"; public static List GetAllKeys() { diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index c7e812fd2c..88ecaf8cef 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -3,8 +3,6 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - $(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998 @@ -36,9 +34,9 @@ - + - + @@ -62,10 +60,10 @@ - - - - + + + + @@ -77,4 +75,8 @@ + + + + diff --git a/src/Core/Tools/Entities/PasswordHealthReportApplication.cs b/src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs similarity index 100% rename from src/Core/Tools/Entities/PasswordHealthReportApplication.cs rename to src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs diff --git a/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs similarity index 100% rename from src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs rename to src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs diff --git a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs diff --git a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs diff --git a/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs rename to src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs diff --git a/src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs diff --git a/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs diff --git a/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs diff --git a/src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs b/src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs similarity index 100% rename from src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs rename to src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 9878c96c1c..08981ca2d3 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -36,6 +36,11 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public string? TwoFactorRecoveryCode { get; set; } public string? EquivalentDomains { get; set; } public string? ExcludedGlobalEquivalentDomains { get; set; } + /// + /// The Account Revision Date is used to check if new sync needs to occur. It should be updated + /// whenever a change is made that affects a client's sync data; for example, updating their vault or + /// organization membership. + /// public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow; public string? Key { get; set; } public string? PublicKey { get; set; } @@ -128,6 +133,10 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow; + /// + /// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary. + /// + /// Dictionary of TwoFactor providers public Dictionary? GetTwoFactorProviders() { if (string.IsNullOrWhiteSpace(TwoFactorProviders)) @@ -137,19 +146,17 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac try { - if (_twoFactorProviders == null) - { - _twoFactorProviders = - JsonHelpers.LegacyDeserialize>( - TwoFactorProviders); - } + _twoFactorProviders ??= + JsonHelpers.LegacyDeserialize>( + TwoFactorProviders); - // U2F is no longer supported, and all users keys should have been migrated to WebAuthn. - // To prevent issues with accounts being prompted for unsupported U2F we remove them - if (_twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) - { - _twoFactorProviders.Remove(TwoFactorProviderType.U2f); - } + /* + U2F is no longer supported, and all users keys should have been migrated to WebAuthn. + To prevent issues with accounts being prompted for unsupported U2F we remove them. + This will probably exist in perpetuity since there is no way to know for sure if any + given user does or doesn't have this enabled. It is a non-zero chance. + */ + _twoFactorProviders?.Remove(TwoFactorProviderType.U2f); return _twoFactorProviders; } @@ -169,6 +176,10 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac return Premium; } + /// + /// Serializes the C# object to the User.TwoFactorProviders property in JSON format. + /// + /// Dictionary of Two Factor providers public void SetTwoFactorProviders(Dictionary providers) { // When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider. @@ -176,20 +187,21 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac _twoFactorProviders = providers; } - public void ClearTwoFactorProviders() - { - SetTwoFactorProviders(new Dictionary()); - } - + /// + /// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor + /// configured it will still be found, even if the user's premium subscription has ended. + /// + /// TwoFactor provider being searched for + /// TwoFactorProvider if found; null otherwise. public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) + if (providers == null || !providers.TryGetValue(provider, out var value)) { return null; } - return providers[provider]; + return value; } public long StorageBytesRemaining() diff --git a/src/Core/Enums/EnumExtensions.cs b/src/Core/Enums/EnumExtensions.cs new file mode 100644 index 0000000000..d60b530ffb --- /dev/null +++ b/src/Core/Enums/EnumExtensions.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace Bit.Core.Enums; + +public static class EnumExtensions +{ + public static string GetDisplayName(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field?.GetCustomAttribute() is { } attribute) + { + return attribute.Name ?? value.ToString(); + } + + return value.ToString(); + } +} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs deleted file mode 100644 index 43531ef242..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - - - - - - - - - - -
    - Additional security has been placed on your Bitwarden account. -
    - We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. -
    - Account: {{AffectedEmail}}
    - Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
    - IP Address: {{IpAddress}}
    -
    - If this was you, you can remove the captcha requirement by successfully logging in. -
    - If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection. -
    -{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs deleted file mode 100644 index 3393210e4e..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#>BasicTextLayout}} -Additional security has been placed on your Bitwarden account. - -We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. - -Account: {{AffectedEmail}} -Date: {{TheDate}} at {{TheTime}} {{TimeZone}} -IP Address: {{IpAddress}} - -If this was you, you can remove the captcha requirement by successfully logging in. - -If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection. -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs deleted file mode 100644 index d73775f8e8..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - - - - - - - - - - -
    - Additional security has been placed on your Bitwarden account. -
    - We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. -
    - Account: {{AffectedEmail}}
    - Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
    - IP Address: {{IpAddress}}
    -
    - If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code. -
    - If this was not you, you should change your master password immediately. You can view our tips for selecting a secure master password here. -
    -{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs deleted file mode 100644 index e742d35578..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#>BasicTextLayout}} -Additional security has been placed on your Bitwarden account. - -We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. - -Account: {{AffectedEmail}} -Date: {{TheDate}} at {{TheTime}} {{TimeZone}} -IP Address: {{IpAddress}} - -If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code (https://bitwarden.com/help/two-step-recovery-code/). - -If this was not you, you should change your master password (https://bitwarden.com/help/master-password/#change-master-password) immediately. You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/). -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs index 6c1b9edec0..5d379288ef 100644 --- a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs @@ -2,7 +2,7 @@ diff --git a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs index 690cf77734..4e0d064e36 100644 --- a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs +++ b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs @@ -1,5 +1,5 @@ {{#>BasicTextLayout}} -Verify your email address using the link below and start your free trial of Bitwarden. +{{VerifyYourEmailTextCopy}} If you did not request this email from Bitwarden, you can safely ignore it. diff --git a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs deleted file mode 100644 index 11b482acda..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs +++ /dev/null @@ -1,27 +0,0 @@ -{{#>FullHtmlLayout}} -
    - Verify your email address below to finish signing up for your free trial. + {{VerifyYourEmailHTMLCopy}}
    - - - - - - - - - - - - -
    - The domain {{DomainName}} in your Bitwarden organization could not be verified. -
    - Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization. -
    - The domain will be removed from your organization in 7 days if it is not verified. -
    - - Manage Domains - -
    -
    -{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs deleted file mode 100644 index f056bf26c3..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{#>BasicTextLayout}} -The domain {{DomainName}} in your Bitwarden organization could not be verified. - -Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization. - -The domain will be removed from your organization in 7 days if it is not verified. - -{{Url}} - -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs deleted file mode 100644 index bd2e4eb946..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#>FullHtmlLayout}} - - - - -
    - Your user account has been removed from the {{OrganizationName}} organization because you are a part of another organization. The {{OrganizationName}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. -
    -{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs deleted file mode 100644 index 44ef628a90..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#>BasicTextLayout}} -Your user account has been removed from the {{OrganizationName}} organization because you are a part of another -organization. The {{OrganizationName}} has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations, or join with a -new account. -{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs deleted file mode 100644 index e82dfcef27..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - -
    - Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login configured. Before you can re-join this organization you need to set up two-step login on your user account. -
    - Learn how to enable two-step login on your user account at - https://help.bitwarden.com/article/setup-two-step-login/ -
    -{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs deleted file mode 100644 index a79afb588a..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#>BasicTextLayout}} -Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login -configured. Before you can re-join this organization you need to set up two-step login on your user account. - -Learn how to enable two-step login on your user account at - -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs new file mode 100644 index 0000000000..b75144c81b --- /dev/null +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs @@ -0,0 +1,37 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.Models.Api.Response.OrganizationSponsorships; + +public class OrganizationSponsorshipInvitesResponseModel : ResponseModel +{ + public OrganizationSponsorshipInvitesResponseModel(OrganizationSponsorshipData sponsorshipData, string obj = "organizationSponsorship") : base(obj) + { + if (sponsorshipData == null) + { + throw new ArgumentNullException(nameof(sponsorshipData)); + } + + SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId; + FriendlyName = sponsorshipData.FriendlyName; + OfferedToEmail = sponsorshipData.OfferedToEmail; + PlanSponsorshipType = sponsorshipData.PlanSponsorshipType; + LastSyncDate = sponsorshipData.LastSyncDate; + ValidUntil = sponsorshipData.ValidUntil; + ToDelete = sponsorshipData.ToDelete; + IsAdminInitiated = sponsorshipData.IsAdminInitiated; + Notes = sponsorshipData.Notes; + CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; + } + + public Guid SponsoringOrganizationUserId { get; set; } + public string FriendlyName { get; set; } + public string OfferedToEmail { get; set; } + public PlanSponsorshipType PlanSponsorshipType { get; set; } + public DateTime? LastSyncDate { get; set; } + public DateTime? ValidUntil { get; set; } + public bool ToDelete { get; set; } + public bool IsAdminInitiated { get; set; } + public string Notes { get; set; } + public bool CloudSponsorshipRemoved { get; set; } +} diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs index 58c1b2cffb..e082d98de6 100644 --- a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs @@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel public bool ToDelete { get; set; } public bool CloudSponsorshipRemoved { get; set; } + public bool IsAdminInitiated { get; set; } public OrganizationSponsorshipResponseModel() { } @@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel ValidUntil = sponsorshipData.ValidUntil; ToDelete = sponsorshipData.ToDelete; CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; + IsAdminInitiated = sponsorshipData.IsAdminInitiated; } public OrganizationSponsorshipData ToOrganizationSponsorship() @@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel LastSyncDate = LastSyncDate, ValidUntil = ValidUntil, ToDelete = ToDelete, - CloudSponsorshipRemoved = CloudSponsorshipRemoved + CloudSponsorshipRemoved = CloudSponsorshipRemoved, + IsAdminInitiated = IsAdminInitiated, }; } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 02e1c109a7..e8c04b1277 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -84,6 +84,7 @@ public class OrganizationLicense : ILicense SmSeats = org.SmSeats; SmServiceAccounts = org.SmServiceAccounts; UseRiskInsights = org.UseRiskInsights; + UseOrganizationDomains = org.UseOrganizationDomains; // Deprecated. Left for backwards compatibility with old license versions. LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion; @@ -182,6 +183,7 @@ public class OrganizationLicense : ILicense public bool Trial { get; set; } public LicenseType? LicenseType { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public string Hash { get; set; } public string Signature { get; set; } @@ -194,10 +196,10 @@ public class OrganizationLicense : ILicense /// Intentionally set one version behind to allow self hosted users some time to update before /// getting out of date license errors /// - public const int CurrentLicenseFileVersion = 14; + public const int CurrentLicenseFileVersion = 15; private bool ValidLicenseVersion { - get => Version is >= 1 and <= 15; + get => Version is >= 1 and <= 16; } public byte[] GetDataBytes(bool forHash = false) @@ -243,6 +245,8 @@ public class OrganizationLicense : ILicense (Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) && // AllowAdminAccessToAllCollectionItems was added in Version 15 (Version >= 15 || !p.Name.Equals(nameof(AllowAdminAccessToAllCollectionItems))) && + // UseOrganizationDomains was added in Version 16 + (Version >= 16 || !p.Name.Equals(nameof(UseOrganizationDomains))) && ( !forHash || ( @@ -251,7 +255,10 @@ public class OrganizationLicense : ILicense !p.Name.Equals(nameof(Refresh)) ) ) && - !p.Name.Equals(nameof(UseRiskInsights))) + // any new fields added need to be added here so that they're ignored + !p.Name.Equals(nameof(UseRiskInsights)) && + !p.Name.Equals(nameof(UseAdminSponsoredFamilies)) && + !p.Name.Equals(nameof(UseOrganizationDomains))) .OrderBy(p => p.Name) .Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); @@ -445,6 +452,7 @@ public class OrganizationLicense : ILicense var smSeats = claimsPrincipal.GetValue(nameof(SmSeats)); var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); var useAdminSponsoredFamilies = claimsPrincipal.GetValue(nameof(UseAdminSponsoredFamilies)); + var useOrganizationDomains = claimsPrincipal.GetValue(nameof(UseOrganizationDomains)); return issued <= DateTime.UtcNow && expires >= DateTime.UtcNow && @@ -473,7 +481,8 @@ public class OrganizationLicense : ILicense usePasswordManager == organization.UsePasswordManager && smSeats == organization.SmSeats && smServiceAccounts == organization.SmServiceAccounts && - useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies; + useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies && + useOrganizationDomains == organization.UseOrganizationDomains; } @@ -580,6 +589,11 @@ public class OrganizationLicense : ILicense * validation. */ + if (valid && Version >= 16) + { + valid = organization.UseOrganizationDomains; + } + return valid; } diff --git a/src/Core/Models/Business/OrganizationSignup.cs b/src/Core/Models/Business/OrganizationSignup.cs index b5ac69e73f..b8bd670d21 100644 --- a/src/Core/Models/Business/OrganizationSignup.cs +++ b/src/Core/Models/Business/OrganizationSignup.cs @@ -16,4 +16,5 @@ public class OrganizationSignup : OrganizationUpgrade public string InitiationPath { get; set; } public bool IsFromSecretsManagerTrial { get; set; } public bool IsFromProvider { get; set; } + public bool SkipTrial { get; set; } } diff --git a/src/Core/Models/Commands/BadRequestFailure.cs b/src/Core/Models/Commands/BadRequestFailure.cs deleted file mode 100644 index bd2753d4e4..0000000000 --- a/src/Core/Models/Commands/BadRequestFailure.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 4a9477067e..0000000000 --- a/src/Core/Models/Commands/CommandResult.cs +++ /dev/null @@ -1,88 +0,0 @@ -#nullable enable - -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Shared.Validation; - -namespace Bit.Core.Models.Commands; - -public class CommandResult(IEnumerable errors) -{ - public CommandResult(string error) : this([error]) { } - - public bool Success => ErrorMessages.Count == 0; - public bool HasErrors => ErrorMessages.Count > 0; - public List ErrorMessages { get; } = errors.ToList(); - 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 value) : CommandResult -{ - public T Value { get; } = value; -} - -public class Failure(IEnumerable errorMessages) : CommandResult -{ - public List ErrorMessages { get; } = errorMessages.ToList(); - public Error[] Errors { get; set; } = []; - - public string ErrorMessage => string.Join(" ", ErrorMessages); - - public Failure(string error) : this([error]) - { - } - - public Failure(IEnumerable> errors) : this(errors.Select(e => e.Message)) - { - Errors = errors.ToArray(); - } - - public Failure(Error error) : this([error.Message]) - { - Errors = [error]; - } -} - -public class Partial : CommandResult -{ - public T[] Successes { get; set; } = []; - public Error[] Failures { get; set; } = []; - - public Partial(IEnumerable successfulItems, IEnumerable> failedItems) - { - Successes = successfulItems.ToArray(); - Failures = failedItems.ToArray(); - } -} - -public static class CommandResultExtensions -{ - /// - /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. - /// - /// - /// This is the invalid type from validating the object. - /// This function will map between the two types for the inner ErrorT - /// Invalid object's type - /// Failure object's type - /// - public static CommandResult MapToFailure(this Invalid
    invalidResult, Func mappingFunction) => - new Failure(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue)))); -} diff --git a/src/Core/Models/Commands/NoRecordFoundFailure.cs b/src/Core/Models/Commands/NoRecordFoundFailure.cs deleted file mode 100644 index a8a322b928..0000000000 --- a/src/Core/Models/Commands/NoRecordFoundFailure.cs +++ /dev/null @@ -1,24 +0,0 @@ -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/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 164710d522..2bc05017d5 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -69,8 +69,11 @@ public static class OrganizationServiceCollectionExtensions services.AddBaseOrganizationSubscriptionCommandsQueries(); } - private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) => + private static void AddOrganizationSignUpCommands(this IServiceCollection services) + { services.AddScoped(); + services.AddScoped(); + } private static void AddOrganizationDeleteCommands(this IServiceCollection services) { @@ -193,6 +196,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index f81a1d9e84..b15cbea240 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -15,7 +15,8 @@ public class CreateSponsorshipCommand( ICurrentContext currentContext, IOrganizationSponsorshipRepository organizationSponsorshipRepository, IUserService userService, - IOrganizationService organizationService) : ICreateSponsorshipCommand + IOrganizationService organizationService, + IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand { public async Task CreateSponsorshipAsync( Organization sponsoringOrganization, @@ -47,11 +48,12 @@ public class CreateSponsorshipCommand( throw new BadRequestException("Only confirmed users can sponsor other organizations."); } - var existingOrgSponsorship = await organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id); - if (existingOrgSponsorship?.SponsoredOrganizationId != null) + var sponsorships = + await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id); + var existingSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName == friendlyName); + if (existingSponsorship != null) { - throw new BadRequestException("Can only sponsor one organization per Organization User."); + return existingSponsorship; } if (isAdminInitiated) @@ -70,15 +72,37 @@ public class CreateSponsorshipCommand( Notes = notes }; - if (existingOrgSponsorship != null) + if (!isAdminInitiated) { - // Replace existing invalid offer with our new sponsorship offer - sponsorship.Id = existingOrgSponsorship.Id; + var existingOrgSponsorship = await organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id); + if (existingOrgSponsorship?.SponsoredOrganizationId != null) + { + throw new BadRequestException("Can only sponsor one organization per Organization User."); + } + + if (existingOrgSponsorship != null) + { + sponsorship.Id = existingOrgSponsorship.Id; + } } if (isAdminInitiated && sponsoringOrganization.Seats.HasValue) { - await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1); + var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id); + var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats; + + if (availableSeats <= 0) + { + var newSeatsRequired = 1; + var (canScale, failureReason) = await organizationService.CanScaleAsync(sponsoringOrganization, newSeatsRequired); + if (!canScale) + { + throw new BadRequestException(failureReason); + } + + await organizationService.AutoAddSeatsAsync(sponsoringOrganization, newSeatsRequired); + } } try diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 09b766e885..cb37e478f7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -263,6 +263,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand organization.Use2fa = newPlan.Has2fa; organization.UseApi = newPlan.HasApi; organization.UseSso = newPlan.HasSso; + organization.UseOrganizationDomains = newPlan.HasOrganizationDomains; organization.UseKeyConnector = newPlan.HasKeyConnector; organization.UseScim = newPlan.HasScim; organization.UseResetPassword = newPlan.HasResetPassword; diff --git a/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs new file mode 100644 index 0000000000..0d287a2229 --- /dev/null +++ b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs @@ -0,0 +1,92 @@ +using System.Text; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.PhishingDomainFeatures; + +public class AzurePhishingDomainStorageService +{ + private const string _containerName = "phishingdomains"; + private const string _domainsFileName = "domains.txt"; + private const string _checksumFileName = "checksum.txt"; + + private readonly BlobServiceClient _blobServiceClient; + private readonly ILogger _logger; + private BlobContainerClient _containerClient; + + public AzurePhishingDomainStorageService( + GlobalSettings globalSettings, + ILogger logger) + { + _blobServiceClient = new BlobServiceClient(globalSettings.Storage.ConnectionString); + _logger = logger; + } + + public async Task> GetDomainsAsync() + { + await InitAsync(); + + var blobClient = _containerClient.GetBlobClient(_domainsFileName); + if (!await blobClient.ExistsAsync()) + { + return []; + } + + var response = await blobClient.DownloadAsync(); + using var streamReader = new StreamReader(response.Value.Content); + var content = await streamReader.ReadToEndAsync(); + + return [.. content + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))]; + } + + public async Task GetChecksumAsync() + { + await InitAsync(); + + var blobClient = _containerClient.GetBlobClient(_checksumFileName); + if (!await blobClient.ExistsAsync()) + { + return string.Empty; + } + + var response = await blobClient.DownloadAsync(); + using var streamReader = new StreamReader(response.Value.Content); + return (await streamReader.ReadToEndAsync()).Trim(); + } + + public async Task UpdateDomainsAsync(IEnumerable domains, string checksum) + { + await InitAsync(); + + var domainsContent = string.Join(Environment.NewLine, domains); + var domainsStream = new MemoryStream(Encoding.UTF8.GetBytes(domainsContent)); + var domainsBlobClient = _containerClient.GetBlobClient(_domainsFileName); + + await domainsBlobClient.UploadAsync(domainsStream, new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" } + }, CancellationToken.None); + + var checksumStream = new MemoryStream(Encoding.UTF8.GetBytes(checksum)); + var checksumBlobClient = _containerClient.GetBlobClient(_checksumFileName); + + await checksumBlobClient.UploadAsync(checksumStream, new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" } + }, CancellationToken.None); + } + + private async Task InitAsync() + { + if (_containerClient is null) + { + _containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + await _containerClient.CreateIfNotExistsAsync(); + } + } +} diff --git a/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs b/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs new file mode 100644 index 0000000000..420948e310 --- /dev/null +++ b/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs @@ -0,0 +1,100 @@ +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.PhishingDomainFeatures; + +/// +/// Implementation of ICloudPhishingDomainQuery for cloud environments +/// that directly calls the external phishing domain source +/// +public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery +{ + private readonly IGlobalSettings _globalSettings; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public CloudPhishingDomainDirectQuery( + IGlobalSettings globalSettings, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _globalSettings = globalSettings; + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public async Task> GetPhishingDomainsAsync() + { + if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl)) + { + throw new InvalidOperationException("Phishing domain update URL is not configured."); + } + + var httpClient = _httpClientFactory.CreateClient("PhishingDomains"); + var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.UpdateUrl); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + return ParseDomains(content); + } + + /// + /// Gets the SHA256 checksum of the remote phishing domains list + /// + /// The SHA256 checksum as a lowercase hex string + public async Task GetRemoteChecksumAsync() + { + if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.ChecksumUrl)) + { + _logger.LogWarning("Phishing domain checksum URL is not configured."); + return string.Empty; + } + + try + { + var httpClient = _httpClientFactory.CreateClient("PhishingDomains"); + var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.ChecksumUrl); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + return ParseChecksumResponse(content); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving phishing domain checksum from {Url}", + _globalSettings.PhishingDomain.ChecksumUrl); + return string.Empty; + } + } + + /// + /// Parses a checksum response in the format "hash *filename" + /// + private static string ParseChecksumResponse(string checksumContent) + { + if (string.IsNullOrWhiteSpace(checksumContent)) + { + return string.Empty; + } + + // Format is typically "hash *filename" + var parts = checksumContent.Split(' ', 2); + + return parts.Length > 0 ? parts[0].Trim() : string.Empty; + } + + private static List ParseDomains(string content) + { + if (string.IsNullOrWhiteSpace(content)) + { + return []; + } + + return content + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) + .ToList(); + } +} diff --git a/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs b/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs new file mode 100644 index 0000000000..2685d36a7f --- /dev/null +++ b/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs @@ -0,0 +1,66 @@ +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Services; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.PhishingDomainFeatures; + +/// +/// Implementation of ICloudPhishingDomainQuery for self-hosted environments +/// that relays the request to the Bitwarden cloud API +/// +public class CloudPhishingDomainRelayQuery : BaseIdentityClientService, ICloudPhishingDomainQuery +{ + private readonly IGlobalSettings _globalSettings; + + public CloudPhishingDomainRelayQuery( + IHttpClientFactory httpFactory, + IGlobalSettings globalSettings, + ILogger logger) + : base( + httpFactory, + globalSettings.Installation.ApiUri, + globalSettings.Installation.IdentityUri, + "api.licensing", + $"installation.{globalSettings.Installation.Id}", + globalSettings.Installation.Key, + logger) + { + _globalSettings = globalSettings; + } + + public async Task> GetPhishingDomainsAsync() + { + if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication) + { + throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled."); + } + + var result = await SendAsync(HttpMethod.Get, "phishing-domains", null, true); + return result?.ToList() ?? new List(); + } + + /// + /// Gets the SHA256 checksum of the remote phishing domains list + /// + /// The SHA256 checksum as a lowercase hex string + public async Task GetRemoteChecksumAsync() + { + if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication) + { + throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled."); + } + + try + { + // For self-hosted environments, we get the checksum from the Bitwarden cloud API + var result = await SendAsync(HttpMethod.Get, "phishing-domains/checksum", null, true); + return result ?? string.Empty; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving phishing domain checksum from Bitwarden cloud API"); + return string.Empty; + } + } +} diff --git a/src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs b/src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs new file mode 100644 index 0000000000..dac91747f7 --- /dev/null +++ b/src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.PhishingDomainFeatures.Interfaces; + +public interface ICloudPhishingDomainQuery +{ + Task> GetPhishingDomainsAsync(); + Task GetRemoteChecksumAsync(); +} diff --git a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs index 30e6ee4a33..00cf6c8cce 100644 --- a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs +++ b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs @@ -11,7 +11,7 @@ public interface IOrganizationSponsorshipRepository : IRepository organizationSponsorships); Task DeleteManyAsync(IEnumerable organizationSponsorshipIds); Task> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId); - Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId); + Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false); Task GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId); Task GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId); } diff --git a/src/Core/Repositories/IPhishingDomainRepository.cs b/src/Core/Repositories/IPhishingDomainRepository.cs new file mode 100644 index 0000000000..2d653b0a43 --- /dev/null +++ b/src/Core/Repositories/IPhishingDomainRepository.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Repositories; + +public interface IPhishingDomainRepository +{ + Task> GetActivePhishingDomainsAsync(); + Task UpdatePhishingDomainsAsync(IEnumerable domains, string checksum); + Task GetCurrentChecksumAsync(); +} diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 0e59b9998f..22effb4329 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -25,6 +25,16 @@ public interface IUserRepository : IRepository ///
    Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids); /// + /// Retrieves the data for the requested user ID and includes additional property indicating + /// whether the user has premium access directly or through an organization. + /// + /// Calls the same stored procedure as GetManyWithCalculatedPremiumAsync but handles the query + /// for a single user. + /// + /// The user ID to retrieve data for. + /// User data with calculated premium access; null if nothing is found + Task GetCalculatedPremiumAsync(Guid userId); + /// /// Sets a new user key and updates all encrypted data. /// Warning: Any user key encrypted data not included will be lost. /// diff --git a/src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs b/src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs new file mode 100644 index 0000000000..2d4ea15b7e --- /dev/null +++ b/src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs @@ -0,0 +1,126 @@ +using System.Text.Json; +using Bit.Core.PhishingDomainFeatures; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Repositories.Implementations; + +public class AzurePhishingDomainRepository : IPhishingDomainRepository +{ + private readonly AzurePhishingDomainStorageService _storageService; + private readonly IDistributedCache _cache; + private readonly ILogger _logger; + private const string _domainsCacheKey = "PhishingDomains_v1"; + private const string _checksumCacheKey = "PhishingDomains_Checksum_v1"; + private static readonly DistributedCacheEntryOptions _cacheOptions = new() + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24), + SlidingExpiration = TimeSpan.FromHours(1) + }; + + public AzurePhishingDomainRepository( + AzurePhishingDomainStorageService storageService, + IDistributedCache cache, + ILogger logger) + { + _storageService = storageService; + _cache = cache; + _logger = logger; + } + + public async Task> GetActivePhishingDomainsAsync() + { + try + { + var cachedDomains = await _cache.GetStringAsync(_domainsCacheKey); + if (!string.IsNullOrEmpty(cachedDomains)) + { + _logger.LogDebug("Retrieved phishing domains from cache"); + return JsonSerializer.Deserialize>(cachedDomains) ?? []; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to retrieve phishing domains from cache"); + } + + var domains = await _storageService.GetDomainsAsync(); + + try + { + await _cache.SetStringAsync( + _domainsCacheKey, + JsonSerializer.Serialize(domains), + _cacheOptions); + _logger.LogDebug("Stored {Count} phishing domains in cache", domains.Count); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to store phishing domains in cache"); + } + + return domains; + } + + public async Task GetCurrentChecksumAsync() + { + try + { + var cachedChecksum = await _cache.GetStringAsync(_checksumCacheKey); + if (!string.IsNullOrEmpty(cachedChecksum)) + { + _logger.LogDebug("Retrieved phishing domain checksum from cache"); + return cachedChecksum; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to retrieve phishing domain checksum from cache"); + } + + var checksum = await _storageService.GetChecksumAsync(); + + try + { + if (!string.IsNullOrEmpty(checksum)) + { + await _cache.SetStringAsync( + _checksumCacheKey, + checksum, + _cacheOptions); + _logger.LogDebug("Stored phishing domain checksum in cache"); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to store phishing domain checksum in cache"); + } + + return checksum; + } + + public async Task UpdatePhishingDomainsAsync(IEnumerable domains, string checksum) + { + var domainsList = domains.ToList(); + await _storageService.UpdateDomainsAsync(domainsList, checksum); + + try + { + await _cache.SetStringAsync( + _domainsCacheKey, + JsonSerializer.Serialize(domainsList), + _cacheOptions); + + await _cache.SetStringAsync( + _checksumCacheKey, + checksum, + _cacheOptions); + + _logger.LogDebug("Updated phishing domains cache after update operation"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update phishing domains in cache"); + } + } +} diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index 3ef0b54efe..90a791222f 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -1,17 +1,17 @@ - + - diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 9b05810eaa..aa1c0c8c25 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -21,7 +21,8 @@ public interface IMailService string email, string token, ProductTierType productTier, - IEnumerable products); + IEnumerable products, + int trialLength); Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token); Task SendCannotDeleteClaimedAccountEmailAsync(string email); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); @@ -39,7 +40,6 @@ public interface IMailService Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable ownerEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails, bool hasAccessSecretsManager = false); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false); - Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); @@ -60,7 +60,6 @@ public interface IMailService Task SendLicenseExpiredAsync(IEnumerable emails, string? organizationName = null); Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip); Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip); - Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token); Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email); Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email); @@ -87,9 +86,6 @@ public interface IMailService Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate); Task SendOTPEmailAsync(string email, string token); - Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip); - Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip); - Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index ded9f4cfd3..af96b88ee6 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,11 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Models; -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.Tax.Requests; +using Bit.Core.Billing.Tax.Responses; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; @@ -31,8 +29,6 @@ public interface IPaymentService Task AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false); Task ReinstateSubscriptionAsync(ISubscriber subscriber); - Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, - string paymentToken, TaxInfo taxInfo = null); Task CreditAccountAsync(ISubscriber subscriber, decimal creditAmount); Task GetBillingAsync(ISubscriber subscriber); Task GetBillingHistoryAsync(ISubscriber subscriber); diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index cb95732a6e..1ba93da4fa 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -57,4 +57,5 @@ public interface IStripeAdapter Task SetupIntentGet(string id, SetupIntentGetOptions options = null); Task SetupIntentVerifyMicroDeposit(string id, SetupIntentVerifyMicrodepositsOptions options); Task> TestClockListAsync(); + Task PriceGetAsync(string id, PriceGetOptions options = null); } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 9b12713218..e63b4e3b87 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -71,11 +71,13 @@ public interface IUserService Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, int? version = null); Task CheckPasswordAsync(User user, string password); + /// + /// Checks if the user has access to premium features, either through a personal subscription or through an organization. + /// + /// user being acted on + /// true if they can access premium; false otherwise. Task CanAccessPremium(ITwoFactorProvidersUser user); Task HasPremiumFromOrganization(ITwoFactorProvidersUser user); - [Obsolete("Use ITwoFactorIsEnabledQuery instead.")] - Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); - Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user); Task GenerateSignInTokenAsync(User user, string purpose); Task UpdatePasswordHash(User user, string newPassword, @@ -131,16 +133,11 @@ public interface IUserService /// verified domains of that organization, and the user is a member of it. /// The organization must be enabled and able to have verified domains. /// - /// - /// False if the Account Deprovisioning feature flag is disabled. - /// Task IsClaimedByAnyOrganizationAsync(Guid userId); /// /// Verify whether the new email domain meets the requirements for managed users. /// - /// - /// /// /// IdentityResult /// @@ -149,9 +146,6 @@ public interface IUserService /// /// Gets the organizations that manage the user. /// - /// - /// An empty collection if the Account Deprovisioning feature flag is disabled. - /// /// Task> GetOrganizationsClaimingUserAsync(Guid userId); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 1fca85eff4..20f6e3a0ab 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -84,7 +84,8 @@ public class HandlebarsMailService : IMailService string email, string token, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trialLength) { var message = CreateDefaultMessage("Verify your email", email); var model = new TrialInitiationVerifyEmail @@ -95,7 +96,8 @@ public class HandlebarsMailService : IMailService WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, ProductTier = productTier, - Product = products + Product = products, + TrialLength = trialLength }; await AddMessageContentAsync(message, "Billing.TrialInitiationVerifyEmail", model); message.MetaData.Add("SendGridBypassListManagement", true); @@ -299,20 +301,6 @@ public class HandlebarsMailService : IMailService await EnqueueMailAsync(messageModels); } - public async Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email) - { - var message = CreateDefaultMessage($"You have been removed from {organizationName}", email); - var model = new OrganizationUserRemovedForPolicyTwoStepViewModel - { - OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), - WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, - SiteName = _globalSettings.SiteName - }; - await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicyTwoStep", model); - message.Category = "OrganizationUserRemovedForPolicyTwoStep"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) { var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); @@ -530,20 +518,6 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email) - { - var message = CreateDefaultMessage($"You have been removed from {organizationName}", email); - var model = new OrganizationUserRemovedForPolicySingleOrgViewModel - { - OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), - WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, - SiteName = _globalSettings.SiteName - }; - await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicySingleOrg", model); - message.Category = "OrganizationUserRemovedForPolicySingleOrg"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) { var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); @@ -1135,53 +1109,6 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - var message = CreateDefaultMessage("Failed login attempts detected", email); - var model = new FailedAuthAttemptsModel() - { - TheDate = utcNow.ToLongDateString(), - TheTime = utcNow.ToShortTimeString(), - TimeZone = _utcTimeZoneDisplay, - IpAddress = ip, - AffectedEmail = email - - }; - await AddMessageContentAsync(message, "Auth.FailedLoginAttempts", model); - message.Category = "FailedLoginAttempts"; - await _mailDeliveryService.SendEmailAsync(message); - } - - public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - var message = CreateDefaultMessage("Failed login attempts detected", email); - var model = new FailedAuthAttemptsModel() - { - TheDate = utcNow.ToLongDateString(), - TheTime = utcNow.ToShortTimeString(), - TimeZone = _utcTimeZoneDisplay, - IpAddress = ip, - AffectedEmail = email - - }; - await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempts", model); - message.Category = "FailedTwoFactorAttempts"; - await _mailDeliveryService.SendEmailAsync(message); - } - - public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) - { - var message = CreateDefaultMessage("Domain not verified", adminEmails); - var model = new OrganizationDomainUnverifiedViewModel - { - Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification", - DomainName = domainName - }; - await AddMessageContentAsync(message, "OrganizationDomainUnverified", model); - message.Category = "UnverifiedOrganizationDomain"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { var message = CreateDefaultMessage("Domain not claimed", adminEmails); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index f7f4fea066..fd9f212ee7 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -283,4 +283,7 @@ public class StripeAdapter : IStripeAdapter } return items; } + + public Task PriceGetAsync(string id, PriceGetOptions options = null) + => _priceService.GetAsync(id, options); } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 51be369527..23d06bed2b 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,16 +1,17 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; -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.Billing.Services.Contracts; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Requests; +using Bit.Core.Billing.Tax.Responses; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -37,7 +38,6 @@ public class StripePaymentService : IPaymentService private readonly IGlobalSettings _globalSettings; private readonly IFeatureService _featureService; private readonly ITaxService _taxService; - private readonly ISubscriberService _subscriberService; private readonly IPricingClient _pricingClient; private readonly IAutomaticTaxFactory _automaticTaxFactory; private readonly IAutomaticTaxStrategy _personalUseTaxStrategy; @@ -50,7 +50,6 @@ public class StripePaymentService : IPaymentService IGlobalSettings globalSettings, IFeatureService featureService, ITaxService taxService, - ISubscriberService subscriberService, IPricingClient pricingClient, IAutomaticTaxFactory automaticTaxFactory, [FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy) @@ -62,7 +61,6 @@ public class StripePaymentService : IPaymentService _globalSettings = globalSettings; _featureService = featureService; _taxService = taxService; - _subscriberService = subscriberService; _pricingClient = pricingClient; _automaticTaxFactory = automaticTaxFactory; _personalUseTaxStrategy = personalUseTaxStrategy; @@ -112,6 +110,8 @@ public class StripePaymentService : IPaymentService throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes."); } + var existingCoupon = sub.Customer.Discount?.Coupon?.Id; + var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; @@ -133,15 +133,68 @@ public class StripePaymentService : IPaymentService if (subscriptionUpdate is CompleteSubscriptionUpdate) { - if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var setNonUSBusinessUseToReverseCharge = + _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, updatedItemOptions.Select(x => x.Plan ?? x.Price)); - var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters); - automaticTaxStrategy.SetUpdateOptions(subUpdateOptions, sub); + if (sub.Customer is + { + Address.Country: not "US", + TaxExempt: not StripeConstants.TaxExempt.Reverse + }) + { + await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else + else if (sub.Customer.HasRecognizedTaxLocation()) { - subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); + switch (subscriber) + { + case User: + { + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + break; + } + case Organization: + { + if (sub.Customer.Address.Country == "US") + { + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + } + else + { + var familyPriceIds = (await Task.WhenAll( + _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), + _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) + .Select(plan => plan.PasswordManager.StripePlanId); + + var updateIsForPersonalUse = updatedItemOptions + .Select(option => option.Price) + .Intersect(familyPriceIds) + .Any(); + + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = updateIsForPersonalUse || sub.Customer.TaxIds.Any() + }; + } + + break; + } + case Provider: + { + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = sub.Customer.Address.Country == "US" || + sub.Customer.TaxIds.Any() + }; + break; + } + } } } @@ -199,7 +252,7 @@ public class StripePaymentService : IPaymentService } else if (!invoice.Paid) { - // Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h + // Pay invoice with no charge to the customer this completes the invoice immediately without waiting the scheduled 1h invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId); paymentIntentClientSecret = null; } @@ -216,6 +269,19 @@ public class StripePaymentService : IPaymentService DaysUntilDue = daysUntilDue, }); } + + var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId); + + var newCoupon = customer.Discount?.Coupon?.Id; + + if (!string.IsNullOrEmpty(existingCoupon) && string.IsNullOrEmpty(newCoupon)) + { + // Re-add the lost coupon due to the update. + await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, new CustomerUpdateOptions + { + Coupon = existingCoupon + }); + } } return paymentIntentClientSecret; @@ -569,309 +635,6 @@ public class StripePaymentService : IPaymentService } } - public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, - string paymentToken, TaxInfo taxInfo = null) - { - if (subscriber == null) - { - throw new ArgumentNullException(nameof(subscriber)); - } - - if (subscriber.Gateway.HasValue && subscriber.Gateway.Value != GatewayType.Stripe) - { - throw new GatewayException("Switching from one payment type to another is not supported. " + - "Contact us for assistance."); - } - - var createdCustomer = false; - Braintree.Customer braintreeCustomer = null; - string stipeCustomerSourceToken = null; - string stipeCustomerPaymentMethodId = null; - var stripeCustomerMetadata = new Dictionary - { - { "region", _globalSettings.BaseServiceUri.CloudRegion } - }; - var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount; - - Customer customer = null; - - if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) - { - var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] }; - customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options); - if (customer.Metadata?.Any() ?? false) - { - stripeCustomerMetadata = customer.Metadata; - } - } - - var hadBtCustomer = stripeCustomerMetadata.ContainsKey("btCustomerId"); - if (stripePaymentMethod) - { - if (paymentToken.StartsWith("pm_")) - { - stipeCustomerPaymentMethodId = paymentToken; - } - else - { - stipeCustomerSourceToken = paymentToken; - } - } - else if (paymentMethodType == PaymentMethodType.PayPal) - { - if (hadBtCustomer) - { - var pmResult = await _btGateway.PaymentMethod.CreateAsync(new Braintree.PaymentMethodRequest - { - CustomerId = stripeCustomerMetadata["btCustomerId"], - PaymentMethodNonce = paymentToken - }); - - if (pmResult.IsSuccess()) - { - var customerResult = await _btGateway.Customer.UpdateAsync( - stripeCustomerMetadata["btCustomerId"], new Braintree.CustomerRequest - { - DefaultPaymentMethodToken = pmResult.Target.Token - }); - - if (customerResult.IsSuccess() && customerResult.Target.PaymentMethods.Length > 0) - { - braintreeCustomer = customerResult.Target; - } - else - { - await _btGateway.PaymentMethod.DeleteAsync(pmResult.Target.Token); - hadBtCustomer = false; - } - } - else - { - hadBtCustomer = false; - } - } - - if (!hadBtCustomer) - { - var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest - { - PaymentMethodNonce = paymentToken, - Email = subscriber.BillingEmailAddress(), - Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() + - Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false), - CustomFields = new Dictionary - { - [subscriber.BraintreeIdField()] = subscriber.Id.ToString(), - [subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion - } - }); - - if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) - { - throw new GatewayException("Failed to create PayPal customer record."); - } - - braintreeCustomer = customerResult.Target; - } - } - else - { - throw new GatewayException("Payment method is not supported at this time."); - } - - if (stripeCustomerMetadata.ContainsKey("btCustomerId")) - { - if (braintreeCustomer?.Id != stripeCustomerMetadata["btCustomerId"]) - { - stripeCustomerMetadata["btCustomerId_old"] = stripeCustomerMetadata["btCustomerId"]; - } - - stripeCustomerMetadata["btCustomerId"] = braintreeCustomer?.Id; - } - else if (!string.IsNullOrWhiteSpace(braintreeCustomer?.Id)) - { - stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); - } - - try - { - if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) - { - taxInfo.TaxIdType = taxInfo.TaxIdType ?? - _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); - } - - if (customer == null) - { - customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions - { - Description = subscriber.BillingName(), - Email = subscriber.BillingEmailAddress(), - Metadata = stripeCustomerMetadata, - Source = stipeCustomerSourceToken, - PaymentMethod = stipeCustomerPaymentMethodId, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = stipeCustomerPaymentMethodId, - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions() - { - Name = subscriber.SubscriberType(), - Value = subscriber.GetFormattedInvoiceName() - } - - ] - }, - Address = taxInfo == null ? null : new AddressOptions - { - Country = taxInfo.BillingAddressCountry, - PostalCode = taxInfo.BillingAddressPostalCode, - 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 - } - ], - Expand = ["sources", "tax", "subscriptions"], - }); - - subscriber.Gateway = GatewayType.Stripe; - subscriber.GatewayCustomerId = customer.Id; - createdCustomer = true; - } - - if (!createdCustomer) - { - string defaultSourceId = null; - string defaultPaymentMethodId = null; - if (stripePaymentMethod) - { - if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_")) - { - var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new BankAccountCreateOptions - { - Source = paymentToken - }); - defaultSourceId = bankAccount.Id; - } - else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId)) - { - await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId, - new PaymentMethodAttachOptions { Customer = customer.Id }); - defaultPaymentMethodId = stipeCustomerPaymentMethodId; - } - } - - if (customer.Sources != null) - { - foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId)) - { - if (source is BankAccount) - { - await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id); - } - else if (source is Card) - { - await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id); - } - } - } - - var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions - { - Customer = customer.Id, - Type = "card" - }); - foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId)) - { - await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new PaymentMethodDetachOptions()); - } - - await _subscriberService.UpdateTaxInformation(subscriber, TaxInformation.From(taxInfo)); - - customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Metadata = stripeCustomerMetadata, - DefaultSource = defaultSourceId, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = defaultPaymentMethodId, - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions() - { - Name = subscriber.SubscriberType(), - Value = subscriber.GetFormattedInvoiceName() - } - ] - }, - Expand = ["tax", "subscriptions"] - }); - } - - if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) - { - var subscriptionGetOptions = new SubscriptionGetOptions - { - Expand = ["customer.tax", "customer.tax_ids"] - }; - var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions); - - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id)); - var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters); - var subscriptionUpdateOptions = automaticTaxStrategy.GetUpdateOptions(subscription); - - if (subscriptionUpdateOptions != null) - { - _ = await _stripeAdapter.SubscriptionUpdateAsync( - subscriber.GatewaySubscriptionId, - subscriptionUpdateOptions); - } - } - } - else - { - if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && - customer.Subscriptions.Any(sub => - sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled) && - customer.HasTaxLocationVerified()) - { - var subscriptionUpdateOptions = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - DefaultTaxRates = [] - }; - - _ = await _stripeAdapter.SubscriptionUpdateAsync( - subscriber.GatewaySubscriptionId, - subscriptionUpdateOptions); - } - } - } - catch - { - if (braintreeCustomer != null && !hadBtCustomer) - { - await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); - } - throw; - } - - return createdCustomer; - } - public async Task CreditAccountAsync(ISubscriber subscriber, decimal creditAmount) { Customer customer = null; @@ -1002,7 +765,7 @@ public class StripePaymentService : IPaymentService var address = customer.Address; var taxId = customer.TaxIds?.FirstOrDefault(); - // Line1 is required, so if missing we're using the subscriber name + // Line1 is required, so if missing we're using the subscriber name, // see: https://stripe.com/docs/api/customers/create#create_customer-address-line1 if (address != null && string.IsNullOrWhiteSpace(address.Line1)) { diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index de0fa427ba..f0c97b8589 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -6,14 +6,19 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -45,7 +50,6 @@ namespace Bit.Core.Services; public class UserService : UserManager, IUserService, IDisposable { private const string PremiumPlanId = "premium-annually"; - private const string StoragePlanId = "storage-gb-annually"; private readonly IUserRepository _userRepository; private readonly ICipherRepository _cipherRepository; @@ -77,7 +81,9 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDistributedCache _distributedCache; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public UserService( IUserRepository userRepository, @@ -115,7 +121,9 @@ public class UserService : UserManager, IUserService, IDisposable IPremiumUserBillingService premiumUserBillingService, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, - IDistributedCache distributedCache) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IDistributedCache distributedCache, + IPolicyRequirementQuery policyRequirementQuery) : base( store, optionsAccessor, @@ -158,7 +166,9 @@ public class UserService : UserManager, IUserService, IDisposable _premiumUserBillingService = premiumUserBillingService; _removeOrganizationUserCommand = removeOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _distributedCache = distributedCache; + _policyRequirementQuery = policyRequirementQuery; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -918,7 +928,7 @@ public class UserService : UserManager, IUserService, IDisposable await SaveUserAsync(user); await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - if (!await TwoFactorIsEnabledAsync(user)) + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) { await CheckPoliciesOnTwoFactorRemovalAsync(user); } @@ -1106,12 +1116,12 @@ public class UserService : UserManager, IUserService, IDisposable } var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, - StoragePlanId); + StripeConstants.Prices.StoragePlanPersonal); await _referenceEventService.RaiseEventAsync( new ReferenceEvent(ReferenceEventType.AdjustStorage, user, _currentContext) { Storage = storageAdjustmentGb, - PlanName = StoragePlanId, + PlanName = StripeConstants.Prices.StoragePlanPersonal, }); await SaveUserAsync(user); return secret; @@ -1280,48 +1290,6 @@ public class UserService : UserManager, IUserService, IDisposable orgAbility.UsersGetPremium && orgAbility.Enabled); } - - public async Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null) - { - return false; - } - - foreach (var p in providers) - { - if (p.Value?.Enabled ?? false) - { - if (!TwoFactorProvider.RequiresPremium(p.Key)) - { - return true; - } - if (await CanAccessPremium(user)) - { - return true; - } - } - } - return false; - } - - public async Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider) || !providers[provider].Enabled) - { - return false; - } - - if (!TwoFactorProvider.RequiresPremium(provider)) - { - return true; - } - - return await CanAccessPremium(user); - } - public async Task GenerateSignInTokenAsync(User user, string purpose) { var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, @@ -1374,18 +1342,11 @@ public class UserService : UserManager, IUserService, IDisposable public async Task> GetOrganizationsClaimingUserAsync(Guid userId) { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - return Enumerable.Empty(); - } - // Get all organizations that have verified the user's email domain. var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId); // Organizations must be enabled and able to have verified domains. - // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). - // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true }); + return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseOrganizationDomains: true }); } /// @@ -1438,30 +1399,51 @@ public class UserService : UserManager, IUserService, IDisposable private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var requirement = await _policyRequirementQuery.GetAsync(user.Id); + if (!requirement.OrganizationsRequiringTwoFactor.Any()) + { + Logger.LogInformation("No organizations requiring two factor for user {userId}.", user.Id); + return; + } + + var organizationIds = requirement.OrganizationsRequiringTwoFactor.Select(o => o.OrganizationId).ToList(); + var organizations = await _organizationRepository.GetManyByIdsAsync(organizationIds); + var organizationLookup = organizations.ToDictionary(org => org.Id); + + var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor + .Where(o => organizationLookup.ContainsKey(o.OrganizationId)) + .Select(async o => + { + var organization = organizationLookup[o.OrganizationId]; + await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest( + o.OrganizationId, + [new OrganizationUserUserDetails { Id = o.OrganizationUserId, OrganizationId = o.OrganizationId }], + new SystemUser(EventSystemUser.TwoFactorDisabled))); + await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); + }).ToArray(); + + await Task.WhenAll(revokeOrgUserTasks); + + return; + } + var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication); - var removeOrgUserTasks = twoFactorPolicies.Select(async p => + var legacyRevokeOrgUserTasks = twoFactorPolicies.Select(async p => { var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( - new RevokeOrganizationUsersRequest( - p.OrganizationId, - [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], - new SystemUser(EventSystemUser.TwoFactorDisabled))); - await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); - } - else - { - await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id); - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - organization.DisplayName(), user.Email); - } - + await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest( + p.OrganizationId, + [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], + new SystemUser(EventSystemUser.TwoFactorDisabled))); + await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); }).ToArray(); - await Task.WhenAll(removeOrgUserTasks); + await Task.WhenAll(legacyRevokeOrgUserTasks); } public override async Task ConfirmEmailAsync(User user, string token) diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index cd5c1af8a8..26858911a8 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -33,7 +33,8 @@ public class NoopMailService : IMailService string email, string token, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trailLength) { return Task.FromResult(0); } @@ -79,11 +80,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email) - { - return Task.FromResult(0); - } - public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) => Task.CompletedTask; @@ -154,11 +150,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email) - { - return Task.FromResult(0); - } - public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token) { return Task.FromResult(0); @@ -267,21 +258,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - return Task.FromResult(0); - } - - public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - return Task.FromResult(0); - } - - public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) - { - return Task.FromResult(0); - } - public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { return Task.FromResult(0); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 2b658c65b3..e228218a29 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } - public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings(); public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual IBaseServiceUriSettings BaseServiceUri { get; set; } public virtual string DatabaseProvider { get; set; } @@ -85,6 +84,7 @@ public class GlobalSettings : IGlobalSettings public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings(); public virtual string DevelopmentDirectory { get; set; } public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings(); + public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings(); public virtual bool EnableEmailVerification { get; set; } public virtual string KdfDefaultHashKey { get; set; } @@ -288,11 +288,15 @@ public class GlobalSettings : IGlobalSettings public class AzureServiceBusSettings { private string _connectionString; - private string _topicName; + private string _eventTopicName; + private string _integrationTopicName; + public int MaxRetries { get; set; } = 3; public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription"; - public virtual string SlackSubscriptionName { get; set; } = "events-slack-subscription"; - public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription"; + public virtual string SlackEventSubscriptionName { get; set; } = "events-slack-subscription"; + public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription"; + public virtual string WebhookEventSubscriptionName { get; set; } = "events-webhook-subscription"; + public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription"; public string ConnectionString { @@ -300,10 +304,16 @@ public class GlobalSettings : IGlobalSettings set => _connectionString = value.Trim('"'); } - public string TopicName + public string EventTopicName { - get => _topicName; - set => _topicName = value.Trim('"'); + get => _eventTopicName; + set => _eventTopicName = value.Trim('"'); + } + + public string IntegrationTopicName + { + get => _integrationTopicName; + set => _integrationTopicName = value.Trim('"'); } } @@ -312,11 +322,19 @@ public class GlobalSettings : IGlobalSettings private string _hostName; private string _username; private string _password; - private string _exchangeName; + private string _eventExchangeName; + private string _integrationExchangeName; + public int MaxRetries { get; set; } = 3; + public int RetryTiming { get; set; } = 30000; // 30s public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; - public virtual string WebhookQueueName { get; set; } = "events-webhook-queue"; - public virtual string SlackQueueName { get; set; } = "events-slack-queue"; + public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue"; + public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue"; + public virtual string SlackIntegrationQueueName { get; set; } = "integration-slack-queue"; + public virtual string SlackIntegrationRetryQueueName { get; set; } = "integration-slack-retry-queue"; + public virtual string WebhookEventsQueueName { get; set; } = "events-webhook-queue"; + public virtual string WebhookIntegrationQueueName { get; set; } = "integration-webhook-queue"; + public virtual string WebhookIntegrationRetryQueueName { get; set; } = "integration-webhook-retry-queue"; public string HostName { @@ -333,10 +351,15 @@ public class GlobalSettings : IGlobalSettings get => _password; set => _password = value.Trim('"'); } - public string ExchangeName + public string EventExchangeName { - get => _exchangeName; - set => _exchangeName = value.Trim('"'); + get => _eventExchangeName; + set => _eventExchangeName = value.Trim('"'); + } + public string IntegrationExchangeName + { + get => _integrationExchangeName; + set => _integrationExchangeName = value.Trim('"'); } } } @@ -628,22 +651,18 @@ public class GlobalSettings : IGlobalSettings public bool EnforceSsoPolicyForAllUsers { get; set; } } - public class CaptchaSettings - { - public bool ForceCaptchaRequired { get; set; } = false; - public string HCaptchaSecretKey { get; set; } - public string HCaptchaSiteKey { get; set; } - public int MaximumFailedLoginAttempts { get; set; } - public double MaybeBotScoreThreshold { get; set; } = double.MaxValue; - public double IsBotScoreThreshold { get; set; } = double.MaxValue; - } - public class StripeSettings { public string ApiKey { get; set; } public int MaxNetworkRetries { get; set; } = 2; } + public class PhishingDomainSettings : IPhishingDomainSettings + { + public string UpdateUrl { get; set; } + public string ChecksumUrl { get; set; } + } + public class DistributedIpRateLimitingSettings { public string RedisConnectionString { get; set; } diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index 411014ea32..d77842373e 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -29,4 +29,5 @@ public interface IGlobalSettings string DevelopmentDirectory { get; set; } IWebPushSettings WebPush { get; set; } GlobalSettings.EventLoggingSettings EventLogging { get; set; } + IPhishingDomainSettings PhishingDomain { get; set; } } diff --git a/src/Core/Settings/IPhishingDomainSettings.cs b/src/Core/Settings/IPhishingDomainSettings.cs new file mode 100644 index 0000000000..2e4a901a5a --- /dev/null +++ b/src/Core/Settings/IPhishingDomainSettings.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Settings; + +public interface IPhishingDomainSettings +{ + string UpdateUrl { get; set; } + string ChecksumUrl { get; set; } +} diff --git a/src/Core/Tools/Entities/Send.cs b/src/Core/Tools/Entities/Send.cs index 93fa7a37c0..34b68fd6c5 100644 --- a/src/Core/Tools/Entities/Send.cs +++ b/src/Core/Tools/Entities/Send.cs @@ -60,9 +60,21 @@ public class Send : ITableObject /// /// Password provided by the user. Protected with pbkdf2. /// + /// + /// This field is mutually exclusive with + /// [MaxLength(300)] public string? Password { get; set; } + /// + /// Comma-separated list of emails for OTP authentication. + /// + /// + /// This field is mutually exclusive with + /// + [MaxLength(1024)] + public string? Emails { get; set; } + /// /// The send becomes unavailable to API callers when /// >= . diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 3c58dca183..fd7a82172c 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -115,7 +115,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - await _cipherRepository.CreateAsync(ciphers, newFolders); + await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); // push await _pushService.PushSyncVaultAsync(importingUserId); diff --git a/src/Core/Tools/Models/Data/SendAccessResult.cs b/src/Core/Tools/Models/Data/SendAccessResult.cs new file mode 100644 index 0000000000..4516f0d9a2 --- /dev/null +++ b/src/Core/Tools/Models/Data/SendAccessResult.cs @@ -0,0 +1,19 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Models.Data; + +/// +/// This enum represents the possible results when attempting to access a . +/// +/// name="Granted">Access is granted for the . +/// name="PasswordRequired">Access is denied, but a password is required to access the . +/// +/// name="PasswordInvalid">Access is denied due to an invalid password. +/// name="Denied">Access is denied for the . +public enum SendAccessResult +{ + Granted, + PasswordRequired, + PasswordInvalid, + Denied +} diff --git a/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs new file mode 100644 index 0000000000..9ce477ed0c --- /dev/null +++ b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs @@ -0,0 +1,50 @@ +#nullable enable + +namespace Bit.Core.Tools.Models.Data; + +/// +/// A discriminated union for send authentication. +/// +/// +/// const method : SendAuthenticationMethod; +/// // other variable definitions omitted +/// +/// var token = method switch +/// { +/// NotAuthenticated => issueTokenFor(sendId), +/// ResourcePassword(var expected) => tryIssueTokenFor(sendId, expected, actual), +/// EmailOtp(_) => tryIssueTokenFor(sendId, email, actualOtp), +/// _ => throw new Exception() +/// }; +/// +public abstract record SendAuthenticationMethod; + +/// +/// Never issue a send claim. +/// +/// +/// This claim is issued when a send does not exist or when a send +/// has exceeded its max access attempts. +/// +public record NeverAuthenticate : SendAuthenticationMethod; + +/// +/// Create a send claim automatically. +/// +public record NotAuthenticated : SendAuthenticationMethod; + +/// +/// Create a send claim by requesting a password confirmation hash. +/// +/// +/// A base64 encoded hash that permits access to the send. +/// +public record ResourcePassword(string Hash) : SendAuthenticationMethod; + +/// +/// Create a send claim by requesting a one time password (OTP) confirmation code. +/// +/// +/// The list of email addresses permitted access to the send. +/// +public record EmailOtp(string[] Emails) : SendAuthenticationMethod; diff --git a/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs new file mode 100644 index 0000000000..f41c62f409 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs @@ -0,0 +1,52 @@ +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.Services; + +namespace Bit.Core.Tools.SendFeatures.Commands; + +public class AnonymousSendCommand : IAnonymousSendCommand +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendAuthorizationService _sendAuthorizationService; + + public AnonymousSendCommand( + ISendRepository sendRepository, + ISendFileStorageService sendFileStorageService, + IPushNotificationService pushNotificationService, + ISendAuthorizationService sendAuthorizationService + ) + { + _sendRepository = sendRepository; + _sendFileStorageService = sendFileStorageService; + _pushNotificationService = pushNotificationService; + _sendAuthorizationService = sendAuthorizationService; + } + + // Response: Send, password required, password invalid + public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password) + { + if (send.Type != SendType.File) + { + throw new BadRequestException("Can only get a download URL for a file type of Send"); + } + + var result = _sendAuthorizationService.SendCanBeAccessed(send, password); + + if (!result.Equals(SendAccessResult.Granted)) + { + return (null, result); + } + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), result); + } +} diff --git a/src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs new file mode 100644 index 0000000000..ad23d85170 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs @@ -0,0 +1,21 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; + +namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces; + +/// +/// AnonymousSendCommand interface provides methods for managing anonymous Sends. +/// +public interface IAnonymousSendCommand +{ + /// + /// Gets the Send file download URL for a Send object. + /// + /// used to help get file download url and validate file + /// FileId get file download url + /// A hashed and base64-encoded password. This is compared with the send's password to authorize access. + /// Async Task object with Tuple containing the string of download url and + /// to determine if the user can access send. + /// + Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password); +} diff --git a/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs new file mode 100644 index 0000000000..58693e619c --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs @@ -0,0 +1,53 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; + +namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces; + +/// +/// NonAnonymousSendCommand interface provides methods for managing non-anonymous Sends. +/// +public interface INonAnonymousSendCommand +{ + /// + /// Saves a to the database. + /// + /// that will save to database + /// Task completes as saves to the database + Task SaveSendAsync(Send send); + + /// + /// Saves the and to the database. + /// + /// that will save to the database + /// that will save to file storage + /// Length of file help with saving to file storage + /// Task object for async operations with file upload url + Task SaveFileSendAsync(Send send, SendFileData data, long fileLength); + + /// + /// Upload a file to an existing . + /// + /// of file to be uploaded. The position + /// will be set to 0 before uploading the file. + /// used to help with uploading file + /// Task completes after saving and metadata to the file storage + Task UploadFileToExistingSendAsync(Stream stream, Send send); + + /// + /// Deletes a from the database and file storage. + /// + /// is used to delete from database and file storage + /// Task completes once has been deleted from database and file storage. + Task DeleteSendAsync(Send send); + + /// + /// Stores the confirmed file size of a send; when the file size cannot be confirmed, the send is deleted. + /// + /// The this command acts upon + /// when the file is confirmed, otherwise + /// + /// When a file size cannot be confirmed, we assume we're working with a rogue client. The send is deleted out of + /// an abundance of caution. + /// + Task ConfirmFileSize(Send send); +} diff --git a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs new file mode 100644 index 0000000000..00da0a911f --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs @@ -0,0 +1,180 @@ +using System.Text.Json; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.Tools.SendFeatures.Commands; + +public class NonAnonymousSendCommand : INonAnonymousSendCommand +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendValidationService _sendValidationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly ISendCoreHelperService _sendCoreHelperService; + + public NonAnonymousSendCommand(ISendRepository sendRepository, + ISendFileStorageService sendFileStorageService, + IPushNotificationService pushNotificationService, + ISendAuthorizationService sendAuthorizationService, + ISendValidationService sendValidationService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext, + ISendCoreHelperService sendCoreHelperService) + { + _sendRepository = sendRepository; + _sendFileStorageService = sendFileStorageService; + _pushNotificationService = pushNotificationService; + _sendValidationService = sendValidationService; + _referenceEventService = referenceEventService; + _currentContext = currentContext; + _sendCoreHelperService = sendCoreHelperService; + } + + public async Task SaveSendAsync(Send send) + { + // Make sure user can save Sends + await _sendValidationService.ValidateUserCanSaveAsync(send.UserId, send); + + if (send.Id == default(Guid)) + { + await _sendRepository.CreateAsync(send); + await _pushNotificationService.PushSyncSendCreateAsync(send); + await _referenceEventService.RaiseEventAsync(new ReferenceEvent + { + Id = send.UserId ?? default, + Type = ReferenceEventType.SendCreated, + Source = ReferenceEventSource.User, + SendType = send.Type, + MaxAccessCount = send.MaxAccessCount, + HasPassword = !string.IsNullOrWhiteSpace(send.Password), + SendHasNotes = send.Data?.Contains("Notes"), + ClientId = _currentContext.ClientId, + ClientVersion = _currentContext.ClientVersion + }); + } + else + { + send.RevisionDate = DateTime.UtcNow; + await _sendRepository.UpsertAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + } + } + + public async Task SaveFileSendAsync(Send send, SendFileData data, long fileLength) + { + if (send.Type != SendType.File) + { + throw new BadRequestException("Send is not of type \"file\"."); + } + + if (fileLength < 1) + { + throw new BadRequestException("No file data."); + } + + var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send); + + if (storageBytesRemaining < fileLength) + { + throw new BadRequestException("Not enough storage available."); + } + + var fileId = _sendCoreHelperService.SecureRandomString(32, useUpperCase: false, useSpecial: false); + + try + { + data.Id = fileId; + data.Size = fileLength; + data.Validated = false; + send.Data = JsonSerializer.Serialize(data, + JsonHelpers.IgnoreWritingNull); + await SaveSendAsync(send); + return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId); + } + catch + { + // Clean up since this is not transactional + await _sendFileStorageService.DeleteFileAsync(send, fileId); + throw; + } + } + public async Task UploadFileToExistingSendAsync(Stream stream, Send send) + { + if (stream.Position > 0) + { + stream.Position = 0; + } + + if (send?.Data == null) + { + throw new BadRequestException("Send does not have file data"); + } + + if (send.Type != SendType.File) + { + throw new BadRequestException("Not a File Type Send."); + } + + var data = JsonSerializer.Deserialize(send.Data); + + if (data.Validated) + { + throw new BadRequestException("File has already been uploaded."); + } + + await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id); + + if (!await ConfirmFileSize(send)) + { + throw new BadRequestException("File received does not match expected file length."); + } + } + public async Task DeleteSendAsync(Send send) + { + await _sendRepository.DeleteAsync(send); + if (send.Type == Enums.SendType.File) + { + var data = JsonSerializer.Deserialize(send.Data); + await _sendFileStorageService.DeleteFileAsync(send, data.Id); + } + await _pushNotificationService.PushSyncSendDeleteAsync(send); + } + + public async Task ConfirmFileSize(Send send) + { + var fileData = JsonSerializer.Deserialize(send.Data); + + var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY); + + if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY) + { + // File reported differs in size from that promised. Must be a rogue client. Delete Send + await DeleteSendAsync(send); + return false; + } + + // Update Send data if necessary + if (realSize != fileData.Size) + { + fileData.Size = realSize.Value; + } + fileData.Validated = true; + send.Data = JsonSerializer.Serialize(fileData, + JsonHelpers.IgnoreWritingNull); + await SaveSendAsync(send); + + return valid; + } + +} diff --git a/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs new file mode 100644 index 0000000000..f7e5f7022c --- /dev/null +++ b/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs @@ -0,0 +1,20 @@ +using Bit.Core.Tools.Models.Data; + +#nullable enable + +namespace Bit.Core.Tools.SendFeatures.Queries.Interfaces; + +/// +/// Integration with authentication layer for generating send access claims. +/// +public interface ISendAuthenticationQuery +{ + /// + /// Retrieves the authentication method of a Send. + /// + /// Identifies the send to inspect. + /// + /// The authentication method that should be performed for the send. + /// + Task GetAuthenticationMethod(Guid sendId); +} diff --git a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs new file mode 100644 index 0000000000..fed7c9e8d4 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs @@ -0,0 +1,53 @@ +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; + +#nullable enable + +namespace Bit.Core.Tools.SendFeatures.Queries; + +/// +public class SendAuthenticationQuery : ISendAuthenticationQuery +{ + private static readonly NotAuthenticated NOT_AUTHENTICATED = new NotAuthenticated(); + private static readonly NeverAuthenticate NEVER_AUTHENTICATE = new NeverAuthenticate(); + + private readonly ISendRepository _sendRepository; + + /// + /// Instantiates the command + /// + /// + /// Retrieves send records + /// + /// + /// Thrown when is . + /// + public SendAuthenticationQuery(ISendRepository sendRepository) + { + _sendRepository = sendRepository ?? throw new ArgumentNullException(nameof(sendRepository)); + } + + /// + public async Task GetAuthenticationMethod(Guid sendId) + { + var send = await _sendRepository.GetByIdAsync(sendId); + + SendAuthenticationMethod method = send switch + { + null => NEVER_AUTHENTICATE, + var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE, + var s when s.Emails is not null => emailOtp(s.Emails), + var s when s.Password is not null => new ResourcePassword(s.Password), + _ => NOT_AUTHENTICATED + }; + + return method; + } + + private EmailOtp emailOtp(string emails) + { + var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return new EmailOtp(list); + } +} diff --git a/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs new file mode 100644 index 0000000000..3dca1cb482 --- /dev/null +++ b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs @@ -0,0 +1,21 @@ +using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.SendFeatures.Queries; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Tools.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Tools.SendFeatures; + +public static class SendServiceCollectionExtension +{ + public static void AddSendServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Core/Tools/Services/Implementations/AzureSendFileStorageService.cs b/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs similarity index 100% rename from src/Core/Tools/Services/Implementations/AzureSendFileStorageService.cs rename to src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs new file mode 100644 index 0000000000..9acf987ac5 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs @@ -0,0 +1,28 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; + +namespace Bit.Core.Tools.Services; + +/// +/// Send Authorization service is responsible for checking if a Send can be accessed. +/// +public interface ISendAuthorizationService +{ + /// + /// Checks if a can be accessed while updating the , pushing a notification, and sending a reference event. + /// + /// used to determine access + /// A hashed and base64-encoded password. This is compared with the send's password to authorize access. + /// will be returned to determine if the user can access send. + /// + Task AccessAsync(Send send, string password); + SendAccessResult SendCanBeAccessed(Send send, + string password); + + /// + /// Hashes the password using the password hasher. + /// + /// Password to be hashed + /// Hashed password of the password given + string HashPassword(string password); +} diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs new file mode 100644 index 0000000000..a09d7c3c60 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs @@ -0,0 +1,17 @@ +namespace Bit.Core.Tools.Services; + +/// +/// This interface provides helper methods for generating secure random strings. Making +/// it easier to mock the service in unit tests. +/// +public interface ISendCoreHelperService +{ + /// + /// Securely generates a random string of the specified length. + /// + /// Desired string length to be returned + /// Desired casing for the string + /// Determines if special characters will be used in string + /// A secure random string with the desired parameters + string SecureRandomString(int length, bool useUpperCase, bool useSpecial); +} diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs new file mode 100644 index 0000000000..29bc0c6a6a --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs @@ -0,0 +1,71 @@ +using Bit.Core.Enums; +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Services; + +/// +/// Send File Storage Service is responsible for uploading, deleting, and validating files +/// whether they are in local storage or in cloud storage. +/// +public interface ISendFileStorageService +{ + FileUploadType FileUploadType { get; } + /// + /// Uploads a new file to the storage. + /// + /// of the file + /// for the file + /// File id + /// Task completes once and have been saved to the database + Task UploadNewFileAsync(Stream stream, Send send, string fileId); + /// + /// Deletes a file from the storage. + /// + /// used to delete file + /// File id of file to be deleted + /// Task completes once has been deleted to the database + Task DeleteFileAsync(Send send, string fileId); + /// + /// Deletes all files for a specific organization. + /// + /// used to delete all files pertaining to organization + /// Task completes after running code to delete files by organization id + Task DeleteFilesForOrganizationAsync(Guid organizationId); + /// + /// Deletes all files for a specific user. + /// + /// used to delete all files pertaining to user + /// Task completes after running code to delete files by user id + Task DeleteFilesForUserAsync(Guid userId); + /// + /// Gets the download URL for a file. + /// + /// used to help get download url for file + /// File id to help get download url for file + /// Download url as a string + Task GetSendFileDownloadUrlAsync(Send send, string fileId); + /// + /// Gets the upload URL for a file. + /// + /// used to help get upload url for file + /// File id to help get upload url for file + /// File upload url as string + Task GetSendFileUploadUrlAsync(Send send, string fileId); + /// + /// Validates the file size of a file in the storage. + /// + /// used to help validate file + /// File id to identify which file to validate + /// Expected file size of the file + /// + /// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize` + /// is outside of the leeway, the storage operation fails. + /// + /// + /// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect + /// + /// Task object for async operations with Tuple of boolean that determines if file was valid and long that + /// the actual file size of the file. + /// + Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway); +} diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs new file mode 100644 index 0000000000..24d31c5cfe --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs @@ -0,0 +1,35 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Services; + +public interface ISendValidationService +{ + /// + /// Validates a file can be saved by specified user. + /// + /// needed to validate file for specific user + /// needed to help validate file + /// Task completes when a conditional statement has been met it will return out of the method or + /// throw a BadRequestException. + /// + Task ValidateUserCanSaveAsync(Guid? userId, Send send); + + /// + /// Validates a file can be saved by specified user with different policy based on feature flag + /// + /// needed to validate file for specific user + /// needed to help validate file + /// Task completes when a conditional statement has been met it will return out of the method or + /// throw a BadRequestException. + /// + Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send); + + /// + /// Calculates the remaining storage for a Send. + /// + /// needed to help calculate remaining storage + /// Long with the remaining bytes for storage or will throw a BadRequestException if user cannot access + /// file or email is not verified. + /// + Task StorageRemainingForSendAsync(Send send); +} diff --git a/src/Core/Tools/Services/Implementations/LocalSendStorageService.cs b/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs similarity index 100% rename from src/Core/Tools/Services/Implementations/LocalSendStorageService.cs rename to src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs diff --git a/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs b/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs new file mode 100644 index 0000000000..101a33754e --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs @@ -0,0 +1,101 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.Tools.Services; + +public class SendAuthorizationService : ISendAuthorizationService +{ + private readonly ISendRepository _sendRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IPushNotificationService _pushNotificationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + + public SendAuthorizationService( + ISendRepository sendRepository, + IPasswordHasher passwordHasher, + IPushNotificationService pushNotificationService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext) + { + _sendRepository = sendRepository; + _passwordHasher = passwordHasher; + _pushNotificationService = pushNotificationService; + _referenceEventService = referenceEventService; + _currentContext = currentContext; + } + + public SendAccessResult SendCanBeAccessed(Send send, + string password) + { + var now = DateTime.UtcNow; + if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled || + send.DeletionDate < now) + { + return SendAccessResult.Denied; + } + if (!string.IsNullOrWhiteSpace(send.Password)) + { + if (string.IsNullOrWhiteSpace(password)) + { + return SendAccessResult.PasswordRequired; + } + var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password); + if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded) + { + send.Password = HashPassword(password); + } + if (passwordResult == PasswordVerificationResult.Failed) + { + return SendAccessResult.PasswordInvalid; + } + } + + return SendAccessResult.Granted; + } + + public async Task AccessAsync(Send sendToBeAccessed, string password) + { + var accessResult = SendCanBeAccessed(sendToBeAccessed, password); + + if (!accessResult.Equals(SendAccessResult.Granted)) + { + return accessResult; + } + + if (sendToBeAccessed.Type != SendType.File) + { + // File sends are incremented during file download + sendToBeAccessed.AccessCount++; + } + + await _sendRepository.ReplaceAsync(sendToBeAccessed); + await _pushNotificationService.PushSyncSendUpdateAsync(sendToBeAccessed); + await _referenceEventService.RaiseEventAsync(new ReferenceEvent + { + Id = sendToBeAccessed.UserId ?? default, + Type = ReferenceEventType.SendAccessed, + Source = ReferenceEventSource.User, + SendType = sendToBeAccessed.Type, + MaxAccessCount = sendToBeAccessed.MaxAccessCount, + HasPassword = !string.IsNullOrWhiteSpace(sendToBeAccessed.Password), + SendHasNotes = sendToBeAccessed.Data?.Contains("Notes"), + ClientId = _currentContext.ClientId, + ClientVersion = _currentContext.ClientVersion + }); + return accessResult; + } + + public string HashPassword(string password) + { + return _passwordHasher.HashPassword(new User(), password); + } +} diff --git a/src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs b/src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs new file mode 100644 index 0000000000..122759f8f0 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs @@ -0,0 +1,12 @@ +using Bit.Core.Utilities; + +namespace Bit.Core.Tools.Services; + +public class SendCoreHelperService : ISendCoreHelperService +{ + public string SecureRandomString(int length, bool useUpperCase, bool useSpecial) + { + return CoreHelpers.SecureRandomString(length, upper: useUpperCase, special: useSpecial); + } + +} diff --git a/src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs b/src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs new file mode 100644 index 0000000000..ef3f210ff8 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs @@ -0,0 +1,26 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.SendFeatures; + +/// +/// SendFileSettingHelper is a static class that provides constants and helper methods (if needed) for managing file +/// settings. +/// +public static class SendFileSettingHelper +{ + /// + /// The leeway for the file size. This is the calculated 1 megabyte of cushion when doing comparisons of file sizes + /// within the system. + /// + public const long FILE_SIZE_LEEWAY = 1024L * 1024L; // 1MB + /// + /// The maximum file size for a file uploaded in a . Units are calculated in bytes but + /// represent 501 megabytes. 1 megabyte is added for cushion to account for file size. + /// + public const long MAX_FILE_SIZE = Constants.FileSize501mb; + + /// + /// String of the expected file size and to be used when needing to communicate the file size to the client/user. + /// + public const string MAX_FILE_SIZE_READABLE = "500 MB"; +} diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs new file mode 100644 index 0000000000..f1e8855def --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -0,0 +1,142 @@ +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.Context; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tools.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Tools.Services; + +public class SendValidationService : ISendValidationService +{ + + private readonly IUserRepository _userRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IPolicyService _policyService; + private readonly IFeatureService _featureService; + private readonly IUserService _userService; + private readonly GlobalSettings _globalSettings; + private readonly ICurrentContext _currentContext; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + + + + public SendValidationService( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IPolicyService policyService, + IFeatureService featureService, + IUserService userService, + IPolicyRequirementQuery policyRequirementQuery, + GlobalSettings globalSettings, + + ICurrentContext currentContext) + { + _userRepository = userRepository; + _organizationRepository = organizationRepository; + _policyService = policyService; + _featureService = featureService; + _userService = userService; + _policyRequirementQuery = policyRequirementQuery; + _globalSettings = globalSettings; + _currentContext = currentContext; + } + + public 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; + } + + var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, + PolicyType.DisableSend); + if (anyDisableSendPolicies) + { + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + if (send.HideEmail.GetValueOrDefault()) + { + var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); + if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) + { + 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."); + } + } + } + + public async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) + { + if (!userId.HasValue) + { + return; + } + + 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."); + } + + 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."); + } + } + + public async Task StorageRemainingForSendAsync(Send send) + { + var storageBytesRemaining = 0L; + if (send.UserId.HasValue) + { + var user = await _userRepository.GetByIdAsync(send.UserId.Value); + if (!await _userService.CanAccessPremium(user)) + { + throw new BadRequestException("You must have premium status to use file Sends."); + } + + if (!user.EmailVerified) + { + throw new BadRequestException("You must confirm your email to use file Sends."); + } + + if (user.Premium) + { + storageBytesRemaining = user.StorageBytesRemaining(); + } + else + { + // Users that get access to file storage/premium from their organization get the default + // 1 GB max storage. + short limit = _globalSettings.SelfHosted ? (short)10240 : (short)1; + storageBytesRemaining = user.StorageBytesRemaining(limit); + } + } + else if (send.OrganizationId.HasValue) + { + var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value); + if (!org.MaxStorageGb.HasValue) + { + throw new BadRequestException("This organization cannot use file sends."); + } + + storageBytesRemaining = org.StorageBytesRemaining(); + } + + return storageBytesRemaining; + } +} diff --git a/src/Core/Tools/Services/ISendService.cs b/src/Core/Tools/Services/ISendService.cs deleted file mode 100644 index 2c20851ce8..0000000000 --- a/src/Core/Tools/Services/ISendService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Models.Data; - -namespace Bit.Core.Tools.Services; - -public interface ISendService -{ - Task DeleteSendAsync(Send send); - Task SaveSendAsync(Send send); - Task SaveFileSendAsync(Send send, SendFileData data, long fileLength); - Task UploadFileToExistingSendAsync(Stream stream, Send send); - Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password); - string HashPassword(string password); - Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password); - Task ValidateSendFile(Send send); -} diff --git a/src/Core/Tools/Services/ISendStorageService.cs b/src/Core/Tools/Services/ISendStorageService.cs deleted file mode 100644 index 4bf2aa3892..0000000000 --- a/src/Core/Tools/Services/ISendStorageService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Enums; -using Bit.Core.Tools.Entities; - -namespace Bit.Core.Tools.Services; - -public interface ISendFileStorageService -{ - FileUploadType FileUploadType { get; } - Task UploadNewFileAsync(Stream stream, Send send, string fileId); - Task DeleteFileAsync(Send send, string fileId); - Task DeleteFilesForOrganizationAsync(Guid organizationId); - Task DeleteFilesForUserAsync(Guid userId); - Task GetSendFileDownloadUrlAsync(Send send, string fileId); - Task GetSendFileUploadUrlAsync(Send send, string fileId); - Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway); -} diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs deleted file mode 100644 index e09787d7eb..0000000000 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System.Text.Json; -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.Context; -using Bit.Core.Entities; -using Bit.Core.Exceptions; -using Bit.Core.Platform.Push; -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.Models.Data; -using Bit.Core.Tools.Repositories; -using Bit.Core.Utilities; -using Microsoft.AspNetCore.Identity; - -namespace Bit.Core.Tools.Services; - -public class SendService : ISendService -{ - public const long MAX_FILE_SIZE = Constants.FileSize501mb; - public const string MAX_FILE_SIZE_READABLE = "500 MB"; - private readonly ISendRepository _sendRepository; - private readonly IUserRepository _userRepository; - private readonly IPolicyService _policyService; - private readonly IUserService _userService; - private readonly IOrganizationRepository _organizationRepository; - private readonly ISendFileStorageService _sendFileStorageService; - private readonly IPasswordHasher _passwordHasher; - private readonly IPushNotificationService _pushService; - 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( - ISendRepository sendRepository, - IUserRepository userRepository, - IUserService userService, - IOrganizationRepository organizationRepository, - ISendFileStorageService sendFileStorageService, - IPasswordHasher passwordHasher, - IPushNotificationService pushService, - IReferenceEventService referenceEventService, - GlobalSettings globalSettings, - IPolicyService policyService, - ICurrentContext currentContext, - IPolicyRequirementQuery policyRequirementQuery, - IFeatureService featureService) - { - _sendRepository = sendRepository; - _userRepository = userRepository; - _userService = userService; - _policyService = policyService; - _organizationRepository = organizationRepository; - _sendFileStorageService = sendFileStorageService; - _passwordHasher = passwordHasher; - _pushService = pushService; - _referenceEventService = referenceEventService; - _globalSettings = globalSettings; - _currentContext = currentContext; - _policyRequirementQuery = policyRequirementQuery; - _featureService = featureService; - } - - public async Task SaveSendAsync(Send send) - { - // Make sure user can save Sends - await ValidateUserCanSaveAsync(send.UserId, send); - - if (send.Id == default(Guid)) - { - await _sendRepository.CreateAsync(send); - await _pushService.PushSyncSendCreateAsync(send); - await RaiseReferenceEventAsync(send, ReferenceEventType.SendCreated); - } - else - { - send.RevisionDate = DateTime.UtcNow; - await _sendRepository.UpsertAsync(send); - await _pushService.PushSyncSendUpdateAsync(send); - } - } - - public async Task SaveFileSendAsync(Send send, SendFileData data, long fileLength) - { - if (send.Type != SendType.File) - { - throw new BadRequestException("Send is not of type \"file\"."); - } - - if (fileLength < 1) - { - throw new BadRequestException("No file data."); - } - - var storageBytesRemaining = await StorageRemainingForSendAsync(send); - - if (storageBytesRemaining < fileLength) - { - throw new BadRequestException("Not enough storage available."); - } - - var fileId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); - - try - { - data.Id = fileId; - data.Size = fileLength; - data.Validated = false; - send.Data = JsonSerializer.Serialize(data, - JsonHelpers.IgnoreWritingNull); - await SaveSendAsync(send); - return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId); - } - catch - { - // Clean up since this is not transactional - await _sendFileStorageService.DeleteFileAsync(send, fileId); - throw; - } - } - - public async Task UploadFileToExistingSendAsync(Stream stream, Send send) - { - if (send?.Data == null) - { - throw new BadRequestException("Send does not have file data"); - } - - if (send.Type != SendType.File) - { - throw new BadRequestException("Not a File Type Send."); - } - - var data = JsonSerializer.Deserialize(send.Data); - - if (data.Validated) - { - throw new BadRequestException("File has already been uploaded."); - } - - await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id); - - if (!await ValidateSendFile(send)) - { - throw new BadRequestException("File received does not match expected file length."); - } - } - - public async Task ValidateSendFile(Send send) - { - var fileData = JsonSerializer.Deserialize(send.Data); - - var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, _fileSizeLeeway); - - if (!valid || realSize > MAX_FILE_SIZE) - { - // File reported differs in size from that promised. Must be a rogue client. Delete Send - await DeleteSendAsync(send); - return false; - } - - // Update Send data if necessary - if (realSize != fileData.Size) - { - fileData.Size = realSize.Value; - } - fileData.Validated = true; - send.Data = JsonSerializer.Serialize(fileData, - JsonHelpers.IgnoreWritingNull); - await SaveSendAsync(send); - - return valid; - } - - public async Task DeleteSendAsync(Send send) - { - await _sendRepository.DeleteAsync(send); - if (send.Type == Enums.SendType.File) - { - var data = JsonSerializer.Deserialize(send.Data); - await _sendFileStorageService.DeleteFileAsync(send, data.Id); - } - await _pushService.PushSyncSendDeleteAsync(send); - } - - public (bool grant, bool passwordRequiredError, bool passwordInvalidError) SendCanBeAccessed(Send send, - string password) - { - var now = DateTime.UtcNow; - if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || - send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled || - send.DeletionDate < now) - { - return (false, false, false); - } - if (!string.IsNullOrWhiteSpace(send.Password)) - { - if (string.IsNullOrWhiteSpace(password)) - { - return (false, true, false); - } - var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password); - if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded) - { - send.Password = HashPassword(password); - } - if (passwordResult == PasswordVerificationResult.Failed) - { - return (false, false, true); - } - } - - return (true, false, false); - } - - // Response: Send, password required, password invalid - public async Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password) - { - if (send.Type != SendType.File) - { - throw new BadRequestException("Can only get a download URL for a file type of Send"); - } - - var (grantAccess, passwordRequired, passwordInvalid) = SendCanBeAccessed(send, password); - - if (!grantAccess) - { - return (null, passwordRequired, passwordInvalid); - } - - send.AccessCount++; - await _sendRepository.ReplaceAsync(send); - await _pushService.PushSyncSendUpdateAsync(send); - return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), false, false); - } - - // Response: Send, password required, password invalid - public async Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password) - { - var send = await _sendRepository.GetByIdAsync(sendId); - var (grantAccess, passwordRequired, passwordInvalid) = SendCanBeAccessed(send, password); - - if (!grantAccess) - { - return (null, passwordRequired, passwordInvalid); - } - - // TODO: maybe move this to a simple ++ sproc? - if (send.Type != SendType.File) - { - // File sends are incremented during file download - send.AccessCount++; - } - - await _sendRepository.ReplaceAsync(send); - await _pushService.PushSyncSendUpdateAsync(send); - await RaiseReferenceEventAsync(send, ReferenceEventType.SendAccessed); - return (send, false, false); - } - - private async Task RaiseReferenceEventAsync(Send send, ReferenceEventType eventType) - { - await _referenceEventService.RaiseEventAsync(new ReferenceEvent - { - Id = send.UserId ?? default, - Type = eventType, - Source = ReferenceEventSource.User, - SendType = send.Type, - MaxAccessCount = send.MaxAccessCount, - HasPassword = !string.IsNullOrWhiteSpace(send.Password), - SendHasNotes = send.Data?.Contains("Notes"), - ClientId = _currentContext.ClientId, - ClientVersion = _currentContext.ClientVersion - }); - } - - public string HashPassword(string password) - { - return _passwordHasher.HashPassword(new User(), password); - } - - 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; - } - - var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, - PolicyType.DisableSend); - if (anyDisableSendPolicies) - { - throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); - } - - if (send.HideEmail.GetValueOrDefault()) - { - var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); - if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) - { - 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 ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) - { - if (!userId.HasValue) - { - return; - } - - 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."); - } - - 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."); - } - } - - private async Task StorageRemainingForSendAsync(Send send) - { - var storageBytesRemaining = 0L; - if (send.UserId.HasValue) - { - var user = await _userRepository.GetByIdAsync(send.UserId.Value); - if (!await _userService.CanAccessPremium(user)) - { - throw new BadRequestException("You must have premium status to use file Sends."); - } - - if (!user.EmailVerified) - { - throw new BadRequestException("You must confirm your email to use file Sends."); - } - - if (user.Premium) - { - storageBytesRemaining = user.StorageBytesRemaining(); - } - else - { - // Users that get access to file storage/premium from their organization get the default - // 1 GB max storage. - storageBytesRemaining = user.StorageBytesRemaining( - _globalSettings.SelfHosted ? (short)10240 : (short)1); - } - } - else if (send.OrganizationId.HasValue) - { - var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value); - if (!org.MaxStorageGb.HasValue) - { - throw new BadRequestException("This organization cannot use file sends."); - } - - storageBytesRemaining = org.StorageBytesRemaining(); - } - - return storageBytesRemaining; - } -} diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index eebcb00738..ab1537afd5 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -712,6 +712,7 @@ public static class CoreHelpers new(Claims.Premium, isPremium ? "true" : "false"), new(JwtClaimTypes.Email, user.Email), new(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"), + // TODO: [https://bitwarden.atlassian.net/browse/PM-22171] Remove this since it is already added from the persisted grant new(Claims.SecurityStamp, user.SecurityStamp), }; diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs index 5cce87e958..07e9d07299 100644 --- a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -45,7 +45,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer cipher.Value.ViewPassword = true; } } - else if (await CanAccessUnassignedCiphersAsync(org)) + else if (CanAccessUnassignedCiphers(org)) { var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId); foreach (var unassignedCipher in unassignedCiphers) @@ -83,7 +83,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer return false; } - private async Task CanAccessUnassignedCiphersAsync(CurrentContextOrganization org) + private bool CanAccessUnassignedCiphers(CurrentContextOrganization org) { if (org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index b094b42044..46742c6aa3 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -3,6 +3,7 @@ using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; namespace Bit.Core.Vault.Repositories; @@ -31,7 +32,10 @@ public interface ICipherRepository : IRepository Task DeleteByUserIdAsync(Guid userId); Task DeleteByOrganizationIdAsync(Guid organizationId); Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers); - Task CreateAsync(IEnumerable ciphers, IEnumerable folders); + /// + /// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items. + /// + Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, IEnumerable collectionUsers); Task SoftDeleteAsync(IEnumerable ids, Guid userId); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 745d90b741..f81e404db8 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -379,7 +379,7 @@ public class CipherService : ICipherService if (!valid || realSize > MAX_FILE_SIZE) { // File reported differs in size from that promised. Must be a rogue client. Delete Send - await DeleteAttachmentAsync(cipher, attachmentData); + await DeleteAttachmentAsync(cipher, attachmentData, false); return false; } // Update Send data if necessary @@ -483,7 +483,7 @@ public class CipherService : ICipherService throw new NotFoundException(); } - return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]); + return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId], orgAdmin); } public async Task PurgeAsync(Guid organizationId) @@ -877,7 +877,7 @@ public class CipherService : ICipherService } } - private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) + private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, bool orgAdmin) { if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId)) { @@ -891,7 +891,14 @@ public class CipherService : ICipherService // Update the revision date when an attachment is deleted cipher.RevisionDate = DateTime.UtcNow; - await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + if (orgAdmin) + { + await _cipherRepository.ReplaceAsync(cipher); + } + else + { + await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + } // push await _pushService.PushSyncCipherUpdateAsync(cipher, null); diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index bb37e240c8..5fc12854b6 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -1,7 +1,4 @@ using System.Globalization; -using Bit.Core; -using Bit.Core.AdminConsole.Services.Implementations; -using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Context; using Bit.Core.IdentityServer; using Bit.Core.Services; @@ -64,43 +61,7 @@ public class Startup services.AddSingleton(); } - if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - else - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - services.AddScoped(sp => - { - var featureService = sp.GetRequiredService(); - var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations) - ? "broadcast" : "storage"; - return sp.GetRequiredKeyedService(key); - }); + services.AddEventWriteServices(globalSettings); services.AddScoped(); services.AddOptionality(); @@ -116,49 +77,7 @@ public class Startup services.AddHostedService(); } - // Optional RabbitMQ Listeners - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) - { - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); - - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } - services.AddSingleton(); - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.SlackQueueName)); - - services.AddHttpClient(WebhookEventHandler.HttpClientName); - services.AddSingleton(); - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.WebhookQueueName)); - } + services.AddRabbitMqListeners(globalSettings); } public void Configure( diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs index e397bd326b..67676a8afc 100644 --- a/src/EventsProcessor/Startup.cs +++ b/src/EventsProcessor/Startup.cs @@ -1,12 +1,8 @@ using System.Globalization; -using Bit.Core.AdminConsole.Services.NoopImplementations; -using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.IdentityModel.Logging; -using TableStorageRepos = Bit.Core.Repositories.TableStorage; namespace Bit.EventsProcessor; @@ -37,50 +33,7 @@ public class Startup services.AddDatabaseRepositories(globalSettings); // Hosted Services - - // Optional Azure Service Bus Listeners - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); - - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } - services.AddSingleton(); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName)); - - services.AddSingleton(); - services.AddHttpClient(WebhookEventHandler.HttpClientName); - - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName)); - } + services.AddAzureServiceBusListeners(globalSettings); services.AddHostedService(); } diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index 96ec1280cd..b83940d3aa 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -1,6 +1,8 @@ -using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core; +using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.TrialInitiation.Registration; using Bit.Core.Context; +using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; @@ -15,18 +17,24 @@ namespace Bit.Identity.Billing.Controller; public class AccountsController( ICurrentContext currentContext, ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand, - IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller + IReferenceEventService referenceEventService, + IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { + var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0); + + var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7; + var token = await sendTrialInitiationEmailForRegistrationCommand.Handle( model.Email, model.Name, model.ReceiveMarketingEmails, model.ProductTier, - model.Products); + model.Products, + trialLength); var refEvent = new ReferenceEvent { diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index fd42074359..80e9536ea3 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Response.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; @@ -37,7 +36,6 @@ public class AccountsController : Controller private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IRegisterUserCommand _registerUserCommand; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; @@ -85,7 +83,6 @@ public class AccountsController : Controller ILogger logger, IUserRepository userRepository, IRegisterUserCommand registerUserCommand, - ICaptchaValidationService captchaValidationService, IDataProtectorTokenFactory assertionOptionsDataProtector, IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand, ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand, @@ -99,7 +96,6 @@ public class AccountsController : Controller _logger = logger; _userRepository = userRepository; _registerUserCommand = registerUserCommand; - _captchaValidationService = captchaValidationService; _assertionOptionsDataProtector = assertionOptionsDataProtector; _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand; _sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand; @@ -167,7 +163,7 @@ public class AccountsController : Controller } [HttpPost("register/finish")] - public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) + public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) { var user = model.ToUser(); @@ -208,12 +204,11 @@ public class AccountsController : Controller } } - private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user) + private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user) { if (result.Succeeded) { - var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); - return new RegisterResponseModel(captchaBypassToken); + return new RegisterFinishResponseModel(); } foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName")) diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index de9f7428fa..bf5ab82166 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -3,8 +3,6 @@ bitwarden-Identity false - - $(WarningsNotAsErrors);CS0162 diff --git a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs index bce460c5c4..eb441e7941 100644 --- a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs +++ b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs @@ -1,5 +1,4 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Entities; +using Bit.Core.Entities; using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer; @@ -9,7 +8,7 @@ public class CustomValidatorRequestContext public User User { get; set; } /// /// This is the device that the user is using to authenticate. It can be either known or unknown. - /// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required. + /// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation. /// The option to set it here saves a trip to the database. /// public Device Device { get; set; } @@ -39,5 +38,4 @@ public class CustomValidatorRequestContext /// This will be null if the authentication request is successful. /// public Dictionary CustomResponse { get; set; } - public CaptchaResponse CaptchaResponse { get; set; } } diff --git a/src/Identity/IdentityServer/ProfileService.cs b/src/Identity/IdentityServer/ProfileService.cs index b6bfb9056e..6d843aed76 100644 --- a/src/Identity/IdentityServer/ProfileService.cs +++ b/src/Identity/IdentityServer/ProfileService.cs @@ -75,6 +75,10 @@ public class ProfileService : IProfileService public async Task IsActiveAsync(IsActiveContext context) { + // We add the security stamp claim to the persisted grant when we issue the refresh token. + // IdentityServer will add this claim to the subject, and here we evaluate whether the security stamp that + // was persisted matches the current security stamp of the user. If it does not match, then the user has performed + // an operation that we want to invalidate the refresh token. var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == Claims.SecurityStamp); var user = await _userService.GetUserByPrincipalAsync(context.Subject); diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 8b7034c9d7..45c0c26b17 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -29,7 +29,6 @@ public abstract class BaseRequestValidator where T : class private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; @@ -49,7 +48,6 @@ public abstract class BaseRequestValidator where T : class IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -66,7 +64,6 @@ public abstract class BaseRequestValidator where T : class _deviceValidator = deviceValidator; _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _organizationUserRepository = organizationUserRepository; - _mailService = mailService; _logger = logger; CurrentContext = currentContext; _globalSettings = globalSettings; @@ -81,23 +78,12 @@ public abstract class BaseRequestValidator where T : class protected async Task ValidateAsync(T context, ValidatedTokenRequest request, CustomValidatorRequestContext validatorContext) { - // 1. We need to check if the user is a bot and if their master password hash is correct. - var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; + // 1. We need to check if the user's master password hash is correct. var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; - if (!valid || isBot) + if (!valid) { - if (isBot) - { - _logger.LogInformation(Constants.BypassFiltersEventId, - "Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.", - request.UserName, validatorContext.CaptchaResponse.Score); - } - - if (!valid) - { - await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); - } + await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); return; @@ -167,7 +153,7 @@ public abstract class BaseRequestValidator where T : class } else { - await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); + await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); } return; @@ -213,46 +199,26 @@ public abstract class BaseRequestValidator where T : class protected abstract Task ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext); + + /// + /// Responsible for building the response to the client when the user has successfully authenticated. + /// + /// The authenticated user. + /// The current request context. + /// The device used for authentication. + /// Whether to send a 2FA remember token. protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken) { await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn); - var claims = new List(); + var claims = this.BuildSubjectClaims(user, context, device); - if (device != null) - { - claims.Add(new Claim(Claims.Device, device.Identifier)); - claims.Add(new Claim(Claims.DeviceType, device.Type.ToString())); - } - - var customResponse = new Dictionary(); - if (!string.IsNullOrWhiteSpace(user.PrivateKey)) - { - customResponse.Add("PrivateKey", user.PrivateKey); - } - - if (!string.IsNullOrWhiteSpace(user.Key)) - { - customResponse.Add("Key", user.Key); - } - - customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); - customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); - customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); - customResponse.Add("Kdf", (byte)user.Kdf); - customResponse.Add("KdfIterations", user.KdfIterations); - customResponse.Add("KdfMemory", user.KdfMemory); - customResponse.Add("KdfParallelism", user.KdfParallelism); - customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context))); - - if (sendRememberToken) - { - var token = await _userManager.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); - customResponse.Add("TwoFactorToken", token); - } + var customResponse = await BuildCustomResponse(user, context, device, sendRememberToken); await ResetFailedAuthDetailsAsync(user); + + // Once we've built the claims and custom response, we can set the success result. + // We delegate this to the derived classes, as the implementation varies based on the grant type. await SetSuccessResult(context, user, claims, customResponse); } @@ -379,7 +345,7 @@ public abstract class BaseRequestValidator where T : class await _userRepository.ReplaceAsync(user); } - private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice) + private async Task UpdateFailedAuthDetailsAsync(User user) { if (user == null) { @@ -390,32 +356,6 @@ public abstract class BaseRequestValidator where T : class user.FailedLoginCount = ++user.FailedLoginCount; user.LastFailedLoginDate = user.RevisionDate = utcNow; await _userRepository.ReplaceAsync(user); - - if (ValidateFailedAuthEmailConditions(unknownDevice, user)) - { - if (twoFactorInvalid) - { - await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - else - { - await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - } - } - - /// - /// checks to see if a user is trying to log into a new device - /// and has reached the maximum number of failed login attempts. - /// - /// boolean - /// current user - /// - private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user) - { - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; } private async Task GetMasterPasswordPolicyAsync(User user) @@ -432,6 +372,71 @@ public abstract class BaseRequestValidator where T : class return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); } + /// + /// Builds the claims that will be stored on the persisted grant. + /// These claims are supplemented by the claims in the ProfileService when the access token is returned to the client. + /// + /// The authenticated user. + /// The current request context. + /// The device used for authentication. + private List BuildSubjectClaims(User user, T context, Device device) + { + // We are adding the security stamp claim to the list of claims that will be stored in the persisted grant. + // We need this because we check for changes in the stamp to determine if we need to invalidate token refresh requests, + // in the `ProfileService.IsActiveAsync` method. + // If we don't store the security stamp in the persisted grant, we won't have the previous value to compare against. + var claims = new List + { + new Claim(Claims.SecurityStamp, user.SecurityStamp) + }; + + if (device != null) + { + claims.Add(new Claim(Claims.Device, device.Identifier)); + claims.Add(new Claim(Claims.DeviceType, device.Type.ToString())); + } + return claims; + } + + /// + /// Builds the custom response that will be sent to the client upon successful authentication, which + /// includes the information needed for the client to initialize the user's account in state. + /// + /// The authenticated user. + /// The current request context. + /// The device used for authentication. + /// Whether to send a 2FA remember token. + private async Task> BuildCustomResponse(User user, T context, Device device, bool sendRememberToken) + { + var customResponse = new Dictionary(); + if (!string.IsNullOrWhiteSpace(user.PrivateKey)) + { + customResponse.Add("PrivateKey", user.PrivateKey); + } + + if (!string.IsNullOrWhiteSpace(user.Key)) + { + customResponse.Add("Key", user.Key); + } + + customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); + customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); + customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); + customResponse.Add("Kdf", (byte)user.Kdf); + customResponse.Add("KdfIterations", user.KdfIterations); + customResponse.Add("KdfMemory", user.KdfMemory); + customResponse.Add("KdfParallelism", user.KdfParallelism); + customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context))); + + if (sendRememberToken) + { + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); + customResponse.Add("TwoFactorToken", token); + } + return customResponse; + } + #nullable enable /// /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 841cd14137..6f2d81bd1b 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator logger, - IFeatureService featureService) : IDeviceValidator + ILogger logger) : IDeviceValidator { private readonly IDeviceService _deviceService = deviceService; private readonly IDeviceRepository _deviceRepository = deviceRepository; @@ -33,7 +32,6 @@ public class DeviceValidator( private readonly IUserService _userService = userService; private readonly IDistributedCache distributedCache = distributedCache; private readonly ILogger _logger = logger; - private readonly IFeatureService _featureService = featureService; public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) { @@ -64,9 +62,7 @@ public class DeviceValidator( } // We have established that the device is unknown at this point; begin new device verification - // PM-13340: remove feature flag - if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) && - request.GrantType == "password" && + if (request.GrantType == "password" && request.Raw["AuthRequest"] == null && !context.TwoFactorRequired && !context.SsoRequired && diff --git a/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs new file mode 100644 index 0000000000..cc45fcb3eb --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs @@ -0,0 +1,38 @@ + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Entities; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators; + +public interface ITwoFactorAuthenticationValidator +{ + /// + /// Check if the user is required to use two-factor authentication to login. This is based on the user's + /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. + /// Client credentials and webauthn grant types do not require two-factor authentication. + /// + /// the active user for the request + /// the request that contains the grant types + /// boolean + Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); + /// + /// Builds the two-factor authentication result for the user based on the available two-factor providers + /// from either their user account or Organization. + /// + /// user trying to login + /// organization associated with the user; Can be null + /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value + Task> BuildTwoFactorResultAsync(User user, Organization organization); + /// + /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses + /// organization duo, it will use the organization duo token provider to verify the token. + /// + /// the active User + /// organization of user; can be null + /// Two Factor Provider to use to verify the token + /// secret passed from the user and consumed by the two-factor provider's verify method + /// boolean + Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); +} diff --git a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index 7108831e84..c30c94eeee 100644 --- a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs @@ -3,7 +3,6 @@ using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; @@ -21,7 +20,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator _userManager; private readonly ICurrentContext _currentContext; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IAuthRequestRepository _authRequestRepository; private readonly IDeviceValidator _deviceValidator; public ResourceOwnerPasswordValidator( @@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, - ICaptchaValidationService captchaValidationService, IAuthRequestRepository authRequestRepository, IUserRepository userRepository, IPolicyService policyService, @@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator - { - { _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey }, - }); - return; - } - - validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync( - captchaResponse, _currentContext.IpAddress, user); - if (!validatorContext.CaptchaResponse.Success) - { - await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null); - return; - } - bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); - } - await ValidateAsync(context, context.Request, validatorContext); - if (context.Result.CustomResponse != null && bypassToken != null) - { - context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken; - } } protected async override Task ValidateContextAsync(ResourceOwnerPasswordValidationContext context, @@ -204,29 +162,4 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator - /// Check if the user is required to use two-factor authentication to login. This is based on the user's - /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. - /// Client credentials and webauthn grant types do not require two-factor authentication. - /// - /// the active user for the request - /// the request that contains the grant types - /// boolean - Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); - /// - /// Builds the two-factor authentication result for the user based on the available two-factor providers - /// from either their user account or Organization. - /// - /// user trying to login - /// organization associated with the user; Can be null - /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value - Task> BuildTwoFactorResultAsync(User user, Organization organization); - /// - /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses - /// organization duo, it will use the organization duo token provider to verify the token. - /// - /// the active User - /// organization of user; can be null - /// Two Factor Provider to use to verify the token - /// secret passed from the user and consumed by the two-factor provider's verify method - /// boolean - Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); -} - public class TwoFactorAuthenticationValidator( IUserService userService, UserManager userManager, IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, - IFeatureService featureService, IApplicationCacheService applicationCacheService, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, IDataProtectorTokenFactory ssoEmail2faSessionTokeFactory, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ICurrentContext currentContext) : ITwoFactorAuthenticationValidator { private readonly IUserService _userService = userService; private readonly UserManager _userManager = userManager; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider; - private readonly IFeatureService _featureService = featureService; private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; private readonly IOrganizationRepository _organizationRepository = organizationRepository; private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; private readonly ICurrentContext _currentContext = currentContext; public async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) @@ -121,7 +91,10 @@ public class TwoFactorAuthenticationValidator( { "TwoFactorProviders2", providers }, }; - // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token + // If we have an Email 2FA provider we need this session token so SSO users + // can re-request an email TOTP. The TwoFactorController.SendEmailLoginAsync + // endpoint requires a way to authenticate the user before sending another email with + // a TOTP, this token acts as the authentication mechanism. if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) { twoFactorResultDict.Add("SsoEmail2faSessionToken", @@ -130,12 +103,6 @@ public class TwoFactorAuthenticationValidator( twoFactorResultDict.Add("Email", user.Email); } - if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) - { - // Send email now if this is their only 2FA method - await _userService.SendTwoFactorEmailAsync(user); - } - return twoFactorResultDict; } @@ -161,7 +128,7 @@ public class TwoFactorAuthenticationValidator( // These cases we want to always return false, U2f is deprecated and OrganizationDuo // uses a different flow than the other two factor providers, it follows the same - // structure of a UserTokenProvider but has it's logic ran outside the usual token + // structure of a UserTokenProvider but has it's logic runs outside the usual token // provider flow. See IOrganizationDuoUniversalTokenProvider.cs if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo) { @@ -171,12 +138,12 @@ public class TwoFactorAuthenticationValidator( // Now we are concerning the rest of the Two Factor Provider Types // The intent of this check is to make sure that the user is using a 2FA provider that - // is enabled and allowed by their premium status. The exception for Remember - // is because it is a "special" 2FA type that isn't ever explicitly + // is enabled and allowed by their premium status. + // The exception for Remember is because it is a "special" 2FA type that isn't ever explicitly // enabled by a user, so we can't check the user's 2FA providers to see if they're // enabled. We just have to check if the token is valid. if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + user.GetTwoFactorProvider(type) == null) { return false; } diff --git a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index 654edeabe8..76949eb5f7 100644 --- a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs @@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator d.Identifier != _device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type)) - .Any(); + hasLoginApprovingDevice = allDevices.Any(d => d.Identifier != _device.Identifier && _loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); } // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP diff --git a/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs b/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs index 8f3cefcfcd..703cb1f350 100644 --- a/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs +++ b/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs @@ -1,7 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core; -using Bit.Core.Auth.Models.Api; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; @@ -9,7 +8,7 @@ using Bit.Core.Utilities; namespace Bit.Identity.Models.Request.Accounts; -public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel +public class RegisterRequestModel : IValidatableObject { [StringLength(50)] public string Name { get; set; } @@ -22,7 +21,6 @@ public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel public string MasterPasswordHash { get; set; } [StringLength(50)] public string MasterPasswordHint { get; set; } - public string CaptchaResponse { get; set; } public string Key { get; set; } public KeysRequestModel Keys { get; set; } public string Token { get; set; } diff --git a/src/Identity/Models/Response/Accounts/ICaptchaProtectedResponseModel.cs b/src/Identity/Models/Response/Accounts/ICaptchaProtectedResponseModel.cs deleted file mode 100644 index 40ecf849f0..0000000000 --- a/src/Identity/Models/Response/Accounts/ICaptchaProtectedResponseModel.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Bit.Identity.Models.Response.Accounts; -public interface ICaptchaProtectedResponseModel -{ - public string CaptchaBypassToken { get; set; } -} diff --git a/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs b/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs new file mode 100644 index 0000000000..564150ab30 --- /dev/null +++ b/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs @@ -0,0 +1,17 @@ +using Bit.Core.Models.Api; + +namespace Bit.Identity.Models.Response.Accounts; + +public class RegisterFinishResponseModel : ResponseModel +{ + public RegisterFinishResponseModel() + : base("registerFinish") + { + // We are setting this to an empty string so that old mobile clients don't break, as they reqiure a non-null value. + // This will be cleaned up in https://bitwarden.atlassian.net/browse/PM-21720. + CaptchaBypassToken = string.Empty; + } + + public string CaptchaBypassToken { get; set; } + +} diff --git a/src/Identity/Models/Response/Accounts/RegisterResponseModel.cs b/src/Identity/Models/Response/Accounts/RegisterResponseModel.cs deleted file mode 100644 index be5e8ad3b0..0000000000 --- a/src/Identity/Models/Response/Accounts/RegisterResponseModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Bit.Core.Models.Api; - -namespace Bit.Identity.Models.Response.Accounts; - -public class RegisterResponseModel : ResponseModel, ICaptchaProtectedResponseModel -{ - public RegisterResponseModel(string captchaBypassToken) - : base("register") - { - CaptchaBypassToken = captchaBypassToken; - } - - public string CaptchaBypassToken { get; set; } -} diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 320c91b248..2d8ca55def 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -145,6 +145,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddOptionality(); services.AddCoreLocalizationServices(); services.AddBillingOperations(); diff --git a/src/Identity/Utilities/LoginApprovingClientTypes.cs b/src/Identity/Utilities/LoginApprovingClientTypes.cs new file mode 100644 index 0000000000..f0c7b831b7 --- /dev/null +++ b/src/Identity/Utilities/LoginApprovingClientTypes.cs @@ -0,0 +1,39 @@ +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Services; + +namespace Bit.Identity.Utilities; + +public interface ILoginApprovingClientTypes +{ + IReadOnlyCollection TypesThatCanApprove { get; } +} + +public class LoginApprovingClientTypes : ILoginApprovingClientTypes +{ + public LoginApprovingClientTypes( + IFeatureService featureService) + { + if (featureService.IsEnabled(FeatureFlagKeys.BrowserExtensionLoginApproval)) + { + TypesThatCanApprove = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }; + } + else + { + TypesThatCanApprove = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + }; + } + } + + public IReadOnlyCollection TypesThatCanApprove { get; } +} diff --git a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs b/src/Identity/Utilities/LoginApprovingDeviceTypes.cs deleted file mode 100644 index b8b11a4d19..0000000000 --- a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bit.Core.Enums; -using Bit.Core.Utilities; - -namespace Bit.Identity.Utilities; - -public static class LoginApprovingDeviceTypes -{ - private static readonly IReadOnlyCollection _deviceTypes; - - static LoginApprovingDeviceTypes() - { - var deviceTypes = new List(); - deviceTypes.AddRange(DeviceTypes.DesktopTypes); - deviceTypes.AddRange(DeviceTypes.MobileTypes); - deviceTypes.AddRange(DeviceTypes.BrowserTypes); - _deviceTypes = deviceTypes.AsReadOnly(); - } - - public static IReadOnlyCollection Types => _deviceTypes; -} diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 34ad68c676..78f1b9ac74 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/src/Identity/appsettings.Production.json b/src/Identity/appsettings.Production.json index af92ef7048..4897a7d8b1 100644 --- a/src/Identity/appsettings.Production.json +++ b/src/Identity/appsettings.Production.json @@ -17,9 +17,6 @@ }, "braintree": { "production": true - }, - "captcha": { - "maximumFailedLoginAttempts": 5 } }, "Logging": { diff --git a/src/Identity/appsettings.SelfHosted.json b/src/Identity/appsettings.SelfHosted.json index cb31ead155..37faf24b59 100644 --- a/src/Identity/appsettings.SelfHosted.json +++ b/src/Identity/appsettings.SelfHosted.json @@ -14,9 +14,6 @@ "internalVault": null, "internalSso": null, "internalScim": null - }, - "captcha": { - "maximumFailedLoginAttempts": 0 } } } diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs index 155abdb4b4..e43eb9a71f 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs index 69a4be1ef8..cf5ac07ead 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs index f8448f4198..52977c9d3c 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index d48fe95096..ba374ae988 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; diff --git a/src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs b/src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs similarity index 100% rename from src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs rename to src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index b26dc938cf..8feb455feb 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -1,10 +1,5 @@ - - - $(WarningsNotAsErrors);CS8618 - - diff --git a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs index 723200ff1c..33643eba88 100644 --- a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs @@ -17,15 +17,11 @@ public class DeviceRepository : Repository, IDeviceRepository private readonly IGlobalSettings _globalSettings; public DeviceRepository(GlobalSettings globalSettings) - : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) { _globalSettings = globalSettings; } - public DeviceRepository(string connectionString, string readOnlyConnectionString) - : base(connectionString, readOnlyConnectionString) - { } - public async Task GetByIdAsync(Guid id, Guid userId) { var device = await GetByIdAsync(id); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs index cebf4b55c6..7033f2113b 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs @@ -89,7 +89,7 @@ public class OrganizationSponsorshipRepository : Repository GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated) { using (var connection = new SqlConnection(ConnectionString)) { @@ -97,7 +97,8 @@ public class OrganizationSponsorshipRepository : Repository, IUserRepository } } - public async Task UpdateUserKeyAndEncryptedDataV2Async( User user, IEnumerable updateDataActions) @@ -289,7 +288,6 @@ public class UserRepository : Repository, IUserRepository UnprotectData(user); } - public async Task> GetManyAsync(IEnumerable ids) { using (var connection = new SqlConnection(ReadOnlyConnectionString)) @@ -318,6 +316,14 @@ public class UserRepository : Repository, IUserRepository } } + public async Task GetCalculatedPremiumAsync(Guid userId) + { + var result = await GetManyWithCalculatedPremiumAsync([userId]); + + UnprotectData(result); + return result.SingleOrDefault(); + } + private async Task ProtectDataAndSaveAsync(User user, Func saveTask) { if (user == null) diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 3df365330c..e0a89b1685 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -484,7 +484,7 @@ public class CipherRepository : Repository, ICipherRepository } } - public async Task CreateAsync(IEnumerable ciphers, IEnumerable folders) + public async Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders) { if (!ciphers.Any()) { @@ -518,7 +518,7 @@ public class CipherRepository : Repository, ICipherRepository await connection.ExecuteAsync( $"[{Schema}].[User_BumpAccountRevisionDate]", - new { Id = ciphers.First().UserId }, + new { Id = userId }, commandType: CommandType.StoredProcedure, transaction: transaction); transaction.Commit(); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 91e29c1b52..f83f7b70b6 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -107,6 +107,7 @@ public class OrganizationRepository : Repository, IOrganizationUserRepository { @@ -440,15 +441,20 @@ public class OrganizationUserRepository : Repository requestedCollections) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 69f40bebb4..71bf113416 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -7,8 +7,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery Run(DatabaseContext dbContext) { var query = from ou in dbContext.OrganizationUsers - join o in dbContext.Organizations on ou.OrganizationId equals o.Id into outerOrganization - from o in outerOrganization.DefaultIfEmpty() + join o in dbContext.Organizations on ou.OrganizationId equals o.Id join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g from su in su_g.DefaultIfEmpty() join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g @@ -68,10 +67,12 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery DateTime.UtcNow) + ) && + ( + // SENT status: When SponsoredOrganizationId is null + os.SponsoredOrganizationId == null || + // ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (os.SponsoredOrganizationId != null && + (!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow)) + ) select new OrganizationUser { Id = os.Id, diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs index 5fd5868a4b..3689ae4d18 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs @@ -49,8 +49,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs b/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs index 1eea0bf9d2..1bea786f21 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs @@ -4,7 +4,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; namespace Bit.Infrastructure.EntityFramework.Billing.Models; // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global -public class ProviderInvoiceItem : Core.Billing.Entities.ProviderInvoiceItem +public class ProviderInvoiceItem : Core.Billing.Providers.Entities.ProviderInvoiceItem { public virtual Provider Provider { get; set; } } @@ -13,6 +13,6 @@ public class ProviderInvoiceItemMapperProfile : Profile { public ProviderInvoiceItemMapperProfile() { - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs b/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs index 4dbbfe71d7..c9ba4c813e 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs @@ -4,7 +4,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; namespace Bit.Infrastructure.EntityFramework.Billing.Models; // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global -public class ProviderPlan : Core.Billing.Entities.ProviderPlan +public class ProviderPlan : Core.Billing.Providers.Entities.ProviderPlan { public virtual Provider Provider { get; set; } } @@ -13,6 +13,6 @@ public class ProviderPlanMapperProfile : Profile { public ProviderPlanMapperProfile() { - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs index c7c9a6118b..4a9a82c9dc 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -1,6 +1,6 @@ using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs index 87e960e123..ed729070ae 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs @@ -1,6 +1,6 @@ using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using LinqToDB; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs index 386f7115d7..e022527d64 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs @@ -1,6 +1,6 @@ using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index ad6c7cf369..22818517d3 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.KeyManagement.Repositories; @@ -103,6 +104,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index a11a209b39..9814eef2aa 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -1,10 +1,5 @@ - - - $(WarningsNotAsErrors);CS0108;CS8632 - - diff --git a/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs b/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs index 96b60a39ed..601ae993b3 100644 --- a/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs +++ b/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs @@ -3,22 +3,12 @@ using C = Bit.Core.Platform.Installations; namespace Bit.Infrastructure.EntityFramework.Platform; -public class Installation : C.Installation -{ - // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 - // This isn't a value or entity used by self hosted servers, but it's - // being added for synchronicity between database provider options. - public DateTime? LastActivityDate { get; set; } -} +public class Installation : C.Installation; public class InstallationMapperProfile : Profile { public InstallationMapperProfile() { - CreateMap() - // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 - .ForMember(i => i.LastActivityDate, opt => opt.Ignore()) - .ReverseMap(); CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs index 6e954e030c..40f2a79887 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +#nullable enable + +using System.Diagnostics; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Enums; using Bit.Core.Enums; @@ -11,8 +13,18 @@ namespace Bit.Infrastructure.EntityFramework.Repositories; public static class DatabaseContextExtensions { + /// + /// Bump the account revision date for the user. + /// The caller is responsible for providing a valid UserId (not a null or default Guid) for a user that exists + /// in the database. + /// public static async Task UserBumpAccountRevisionDateAsync(this DatabaseContext context, Guid userId) { + if (userId == Guid.Empty) + { + throw new ArgumentException("Invalid UserId."); + } + var user = await context.Users.FindAsync(userId); Debug.Assert(user is not null, "The user id is expected to be validated as a true-in database user before making this call."); user.AccountRevisionDate = DateTime.UtcNow; diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs index 0f76772c57..0481f9e13a 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs @@ -104,12 +104,13 @@ public class OrganizationSponsorshipRepository : Repository GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var orgSponsorship = await GetDbSet(dbContext).Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId) + var orgSponsorship = await GetDbSet(dbContext) + .Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId && e.IsAdminInitiated == isAdminInitiated) .FirstOrDefaultAsync(); return orgSponsorship; } diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 127646ed59..bd70e27e78 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -1,10 +1,10 @@ using AutoMapper; using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using DataModel = Bit.Core.Models.Data; #nullable enable @@ -38,13 +38,13 @@ public class UserRepository : Repository, IUserR } } - public async Task GetKdfInformationByEmailAsync(string email) + public async Task GetKdfInformationByEmailAsync(string email) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); return await GetDbSet(dbContext).Where(e => e.Email == email) - .Select(e => new DataModel.UserKdfInformation + .Select(e => new UserKdfInformation { Kdf = e.Kdf, KdfIterations = e.KdfIterations, @@ -251,13 +251,13 @@ public class UserRepository : Repository, IUserR } } - public async Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids) + public async Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); var users = dbContext.Users.Where(x => ids.Contains(x.Id)); - return await users.Select(e => new DataModel.UserWithCalculatedPremium(e) + return await users.Select(e => new UserWithCalculatedPremium(e) { HasPremiumAccess = e.Premium || dbContext.OrganizationUsers .Any(ou => ou.UserId == e.Id && @@ -269,6 +269,12 @@ public class UserRepository : Repository, IUserR } } + public async Task GetCalculatedPremiumAsync(Guid id) + { + var result = await GetManyWithCalculatedPremiumAsync([id]); + return result.FirstOrDefault(); + } + public override async Task DeleteAsync(Core.Entities.User user) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 090c36ff29..befb835e26 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -142,8 +142,10 @@ public class CipherRepository : Repository ciphers, IEnumerable folders) + public async Task CreateAsync(Guid userId, IEnumerable ciphers, + IEnumerable folders) { + ciphers = ciphers.ToList(); if (!ciphers.Any()) { return; @@ -156,7 +158,8 @@ public class CipherRepository : Repository>(ciphers); await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities); - await dbContext.UserBumpAccountRevisionDateAsync(ciphers.First().UserId.GetValueOrDefault()); + await dbContext.UserBumpAccountRevisionDateAsync(userId); + await dbContext.SaveChangesAsync(); } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index d8f3690679..408113e1c8 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -4,8 +4,8 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; using Azure.Storage.Queues; -using Bit.Core; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; @@ -45,6 +45,7 @@ using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.SendFeatures; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault; @@ -125,7 +126,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddLoginServices(); services.AddScoped(); services.AddVaultServices(); @@ -134,6 +135,7 @@ public static class ServiceCollectionExtensions services.AddNotificationCenterServices(); services.AddPlatformServices(); services.AddImportServices(); + services.AddSendServices(); } public static void AddTokenizers(this IServiceCollection services) @@ -153,14 +155,6 @@ public static class ServiceCollectionExtensions serviceProvider.GetRequiredService>>()) ); - services.AddSingleton>(serviceProvider => - new DataProtectorTokenFactory( - HCaptchaTokenable.ClearTextPrefix, - HCaptchaTokenable.DataProtectorPurpose, - serviceProvider.GetDataProtectionProvider(), - serviceProvider.GetRequiredService>>()) - ); - services.AddSingleton>(serviceProvider => new DataProtectorTokenFactory( SsoTokenable.ClearTextPrefix, @@ -332,48 +326,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); } - if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - else if (globalSettings.SelfHosted) - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - else - { - services.AddKeyedSingleton("storage"); - services.AddKeyedSingleton("broadcast"); - } - services.AddScoped(sp => - { - var featureService = sp.GetRequiredService(); - var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations) - ? "broadcast" : "storage"; - return sp.GetRequiredKeyedService(key); - }); + services.AddEventWriteServices(globalSettings); if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString)) { @@ -409,16 +362,6 @@ public static class ServiceCollectionExtensions { services.AddSingleton(); } - - if (CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) && - CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey)) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } } public static void AddOosServices(this IServiceCollection services) @@ -608,6 +551,237 @@ public static class ServiceCollectionExtensions return globalSettings; } + public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings) + { + if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) + { + services.AddKeyedSingleton("storage"); + + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) + { + services.AddKeyedSingleton("broadcast"); + } + else + { + services.AddKeyedSingleton("broadcast"); + } + } + else if (globalSettings.SelfHosted) + { + services.AddKeyedSingleton("storage"); + + if (IsRabbitMqEnabled(globalSettings)) + { + services.AddKeyedSingleton("broadcast"); + } + else + { + services.AddKeyedSingleton("broadcast"); + } + } + else + { + services.AddKeyedSingleton("storage"); + services.AddKeyedSingleton("broadcast"); + } + + services.AddScoped(); + return services; + } + + private static IServiceCollection AddAzureServiceBusEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + handler: provider.GetRequiredService(), + logger: provider.GetRequiredService>(), + globalSettings: globalSettings, + subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + + return services; + } + + private static IServiceCollection AddAzureServiceBusIntegration( + this IServiceCollection services, + string eventSubscriptionName, + string integrationSubscriptionName, + IntegrationType integrationType, + GlobalSettings globalSettings) + where TConfig : class + where THandler : class, IIntegrationHandler + { + var routingKey = integrationType.ToRoutingKey(); + + services.AddSingleton(); + + services.AddKeyedSingleton(routingKey, (provider, _) => + new EventIntegrationHandler( + integrationType, + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService())); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + handler: provider.GetRequiredKeyedService(routingKey), + logger: provider.GetRequiredService>(), + globalSettings: globalSettings, + subscriptionName: eventSubscriptionName)); + + services.AddSingleton, THandler>(); + + services.AddSingleton(provider => + new AzureServiceBusIntegrationListenerService( + handler: provider.GetRequiredService>(), + subscriptionName: integrationSubscriptionName, + logger: provider.GetRequiredService>(), + globalSettings: globalSettings)); + + return services; + } + + public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings) + { + if (!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) || + !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) + return services; + + services.AddAzureServiceBusEventRepositoryListener(globalSettings); + + services.AddSlackService(globalSettings); + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName, + integrationType: IntegrationType.Slack, + globalSettings: globalSettings); + + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName, + integrationType: IntegrationType.Webhook, + globalSettings: globalSettings); + return services; + } + + private static IServiceCollection AddRabbitMqEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); + + return services; + } + + private static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services, + string eventQueueName, + string integrationQueueName, + string integrationRetryQueueName, + string integrationDeadLetterQueueName, + IntegrationType integrationType, + GlobalSettings globalSettings) + where TConfig : class + where THandler : class, IIntegrationHandler + { + var routingKey = integrationType.ToRoutingKey(); + + services.AddSingleton(); + services.AddKeyedSingleton(routingKey, (provider, _) => + new EventIntegrationHandler( + integrationType, + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService())); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredKeyedService(routingKey), + provider.GetRequiredService>(), + globalSettings, + eventQueueName)); + + services.AddSingleton, THandler>(); + services.AddSingleton(provider => + new RabbitMqIntegrationListenerService( + handler: provider.GetRequiredService>(), + routingKey: routingKey, + queueName: integrationQueueName, + retryQueueName: integrationRetryQueueName, + deadLetterQueueName: integrationDeadLetterQueueName, + globalSettings: globalSettings, + logger: provider.GetRequiredService>())); + + return services; + } + + public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) + { + if (!IsRabbitMqEnabled(globalSettings)) + { + return services; + } + + services.AddRabbitMqEventRepositoryListener(globalSettings); + + services.AddSlackService(globalSettings); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Slack, + globalSettings); + + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Webhook, + globalSettings); + + return services; + } + + private static bool IsRabbitMqEnabled(GlobalSettings settings) + { + return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); + } + + public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings) + { + if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && + CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && + CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) + { + services.AddHttpClient(SlackService.HttpClientName); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + return services; + } + public static void UseDefaultMiddleware(this IApplicationBuilder app, IWebHostEnvironment env, GlobalSettings globalSettings) { diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql index 6979a10ae2..7c1b168f5c 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql @@ -2,7 +2,7 @@ @Id BIGINT OUTPUT, @UserId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER, - @ExternalId NVARCHAR(50), + @ExternalId NVARCHAR(300), @CreationDate DATETIME2(7) AS BEGIN diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql index facbc44572..bbd0720ba2 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql @@ -2,7 +2,7 @@ @Id BIGINT OUTPUT, @UserId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER, - @ExternalId NVARCHAR(50), + @ExternalId NVARCHAR(300), @CreationDate DATETIME2(7) AS BEGIN diff --git a/src/Sql/Auth/dbo/Tables/SsoUser.sql b/src/Sql/Auth/dbo/Tables/SsoUser.sql index f6477f56c2..2f8cd2f190 100644 --- a/src/Sql/Auth/dbo/Tables/SsoUser.sql +++ b/src/Sql/Auth/dbo/Tables/SsoUser.sql @@ -2,7 +2,7 @@ [Id] BIGINT IDENTITY (1, 1) NOT NULL, [UserId] UNIQUEIDENTIFIER NOT NULL, [OrganizationId] UNIQUEIDENTIFIER NULL, - [ExternalId] NVARCHAR(50) NOT NULL, + [ExternalId] NVARCHAR(300) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, CONSTRAINT [PK_SsoUser] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_SsoUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE, diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 65524fca45..849fd3bdfd 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -1,6 +1,6 @@  - + Sql {58554e52-fdec-4832-aff9-302b01e08dca} diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql b/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql index 396c619ba4..58375a0397 100644 --- a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql +++ b/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql @@ -14,7 +14,10 @@ @DeletionDate DATETIME2(7), @Disabled BIT, @HideEmail BIT, - @CipherId UNIQUEIDENTIFIER = NULL + @CipherId UNIQUEIDENTIFIER = NULL, +-- FIXME: remove null default value once this argument has been +-- in 2 server releases + @Emails NVARCHAR(1024) = NULL AS BEGIN SET NOCOUNT ON @@ -36,7 +39,8 @@ BEGIN [DeletionDate], [Disabled], [HideEmail], - [CipherId] + [CipherId], + [Emails] ) VALUES ( @@ -55,7 +59,8 @@ BEGIN @DeletionDate, @Disabled, @HideEmail, - @CipherId + @CipherId, + @Emails ) IF @UserId IS NOT NULL diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql b/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql index e975e18ad9..43873b4b88 100644 --- a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql +++ b/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql @@ -14,7 +14,8 @@ @DeletionDate DATETIME2(7), @Disabled BIT, @HideEmail BIT, - @CipherId UNIQUEIDENTIFIER = NULL + @CipherId UNIQUEIDENTIFIER = NULL, + @Emails NVARCHAR(1024) = NULL AS BEGIN SET NOCOUNT ON @@ -36,7 +37,8 @@ BEGIN [DeletionDate] = @DeletionDate, [Disabled] = @Disabled, [HideEmail] = @HideEmail, - [CipherId] = @CipherId + [CipherId] = @CipherId, + [Emails] = @Emails WHERE [Id] = @Id diff --git a/src/Sql/Tools/dbo/Tables/Send.sql b/src/Sql/Tools/dbo/Tables/Send.sql index 71acf4a9f3..2130dbc07e 100644 --- a/src/Sql/Tools/dbo/Tables/Send.sql +++ b/src/Sql/Tools/dbo/Tables/Send.sql @@ -6,6 +6,7 @@ [Data] VARCHAR(MAX) NOT NULL, [Key] VARCHAR (MAX) NOT NULL, [Password] NVARCHAR (300) NULL, + [Emails] NVARCHAR (1024) NULL, [MaxAccessCount] INT NULL, [AccessCount] INT NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql index 8fc95eb302..214b74d092 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), -- not used @Folders NVARCHAR(MAX), -- not used - @Attachments NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @FolderId UNIQUEIDENTIFIER, @@ -50,7 +50,6 @@ BEGIN ELSE JSON_MODIFY([Favorites], @UserIdPath, NULL) END, - [Attachments] = @Attachments, [Reprompt] = @Reprompt, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql index c03c27ea78..676c013cc8 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), @Folders NVARCHAR(MAX), - @Attachments NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @@ -25,7 +25,6 @@ BEGIN [Data], [Favorites], [Folders], - [Attachments], [CreationDate], [RevisionDate], [DeletedDate], @@ -41,7 +40,6 @@ BEGIN @Data, @Favorites, @Folders, - @Attachments, @CreationDate, @RevisionDate, @DeletedDate, diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql index 5983f557c2..4a2e3187c7 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql @@ -10,20 +10,59 @@ BEGIN DECLARE @UserId UNIQUEIDENTIFIER DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @CurrentAttachments NVARCHAR(MAX) + DECLARE @NewAttachments NVARCHAR(MAX) + -- Get current cipher data SELECT @UserId = [UserId], - @OrganizationId = [OrganizationId] - FROM + @OrganizationId = [OrganizationId], + @CurrentAttachments = [Attachments] + FROM [dbo].[Cipher] WHERE [Id] = @Id - UPDATE - [dbo].[Cipher] - SET - [Attachments] = JSON_MODIFY([Attachments], @AttachmentIdPath, NULL) - WHERE - [Id] = @Id + -- If there are no attachments, nothing to do + IF @CurrentAttachments IS NULL + BEGIN + RETURN; + END + + -- Validate the initial JSON + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current initial attachments data is not valid JSON', 1; + RETURN; + END + + -- Check if the attachment exists before trying to remove it + IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL + BEGIN + -- Attachment doesn't exist, nothing to do + RETURN; + END + + -- Create the new attachments JSON with the specified attachment removed + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL) + + -- Validate the resulting JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when removing attachment', 1; + RETURN; + END + + -- Check if we've removed all attachments and have an empty object + IF @NewAttachments = '{}' + BEGIN + -- If we have an empty JSON object, set to NULL instead + SET @NewAttachments = NULL; + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id IF @OrganizationId IS NOT NULL BEGIN diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql index 7815aa3053..0aa73ae9b6 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), @Folders NVARCHAR(MAX), - @Attachments NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @@ -25,7 +25,6 @@ BEGIN [Data] = @Data, [Favorites] = @Favorites, [Folders] = @Folders, - [Attachments] = @Attachments, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, [DeletedDate] = @DeletedDate, @@ -42,4 +41,4 @@ BEGIN BEGIN EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END -END \ No newline at end of file +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql index 520df72505..4401a4a0c6 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql @@ -8,21 +8,75 @@ AS BEGIN SET NOCOUNT ON + -- Validate that AttachmentData is valid JSON + IF ISJSON(@AttachmentData) = 0 + BEGIN + THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1; + RETURN; + END + + -- Validate that AttachmentData has the expected structure + -- Check for required fields + IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR + JSON_VALUE(@AttachmentData, '$.Size') IS NULL + BEGIN + THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1; + RETURN; + END + + -- Validate data types for critical fields + DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT) + IF @Size IS NULL OR @Size <= 0 + BEGIN + THROW 50000, 'AttachmentData has invalid Size value', 1; + RETURN; + END + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + DECLARE @NewAttachments NVARCHAR(MAX) - UPDATE - [dbo].[Cipher] - SET - [Attachments] = - CASE - WHEN [Attachments] IS NULL THEN - CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') - ELSE - JSON_MODIFY([Attachments], @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) - END - WHERE - [Id] = @Id + -- Get current attachments + DECLARE @CurrentAttachments NVARCHAR(MAX) + SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id + + -- Prepare the new attachments value based on current state + IF @CurrentAttachments IS NULL + BEGIN + -- Create new JSON object with the attachment + SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') + + -- Validate the constructed JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when adding new attachment', 1; + RETURN; + END + END + ELSE + BEGIN + -- Validate existing attachments + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current attachments data is not valid JSON', 1; + RETURN; + END + + -- Modify existing JSON + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) + + -- Validate the modified JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1; + RETURN; + END + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id IF @OrganizationId IS NOT NULL BEGIN @@ -34,4 +88,4 @@ BEGIN EXEC [dbo].[User_UpdateStorage] @UserId EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql index 5cc47213d6..262d4bfd8d 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql @@ -3,9 +3,9 @@ CREATE PROCEDURE [dbo].[OrganizationDomainSsoDetails_ReadByEmail] AS BEGIN SET NOCOUNT ON - + DECLARE @Domain NVARCHAR(256) - + SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) SELECT @@ -19,8 +19,8 @@ BEGIN [dbo].[OrganizationView] O INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId - LEFT JOIN [dbo].[Ssoconfig] S + LEFT JOIN [dbo].[SsoConfig] S ON O.Id = S.OrganizationId WHERE OD.DomainName = @Domain AND O.Enabled = 1 -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql deleted file mode 100644 index 817a95cbce..0000000000 --- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] - @SponsoringOrganizationUserId UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON - - SELECT - * - FROM - [dbo].[OrganizationSponsorshipView] - WHERE - [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId -END -GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql new file mode 100644 index 0000000000..520a902601 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] + @SponsoringOrganizationUserId UNIQUEIDENTIFIER, + @IsAdminInitiated BIT = 0 +AS +BEGIN + SET NOCOUNT ON; + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationUserID] = @SponsoringOrganizationUserId + and [IsAdminInitiated] = @IsAdminInitiated +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql index 933441a210..3d861670a6 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql @@ -19,5 +19,19 @@ BEGIN FROM [dbo].[OrganizationSponsorship] WHERE SponsoringOrganizationId = @OrganizationId AND IsAdminInitiated = 1 + AND ( + -- Not marked for deletion - always count + (ToDelete = 0) + OR + -- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status) + (ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE()) + ) + AND ( + -- SENT status: When SponsoredOrganizationId is null + SponsoredOrganizationId IS NULL + OR + -- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE())) + ) ) END diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index ef434f1078..dc793351f7 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -56,6 +56,7 @@ CREATE PROCEDURE [dbo].[Organization_Create] @AllowAdminAccessToAllCollectionItems BIT = 0, @UseRiskInsights BIT = 0, @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, @UseAdminSponsoredFamilies BIT = 0 AS BEGIN @@ -120,6 +121,7 @@ BEGIN [AllowAdminAccessToAllCollectionItems], [UseRiskInsights], [LimitItemDeletion], + [UseOrganizationDomains], [UseAdminSponsoredFamilies] ) VALUES @@ -181,6 +183,7 @@ BEGIN @AllowAdminAccessToAllCollectionItems, @UseRiskInsights, @LimitItemDeletion, + @UseOrganizationDomains, @UseAdminSponsoredFamilies ) END diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql index a2e274057d..6a8ed9e0d0 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql @@ -26,6 +26,7 @@ BEGIN [AllowAdminAccessToAllCollectionItems], [UseRiskInsights], [LimitItemDeletion], + [UseOrganizationDomains], [UseAdminSponsoredFamilies] FROM [dbo].[Organization] diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index 537433ad51..0043993686 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -56,6 +56,7 @@ CREATE PROCEDURE [dbo].[Organization_Update] @AllowAdminAccessToAllCollectionItems BIT = 0, @UseRiskInsights BIT = 0, @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, @UseAdminSponsoredFamilies BIT = 0 AS BEGIN @@ -120,6 +121,7 @@ BEGIN [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, [UseRiskInsights] = @UseRiskInsights, [LimitItemDeletion] = @LimitItemDeletion, + [UseOrganizationDomains] = @UseOrganizationDomains, [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies WHERE [Id] = @Id diff --git a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql index a32b42f6c1..2b1a594bfc 100644 --- a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql +++ b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql @@ -15,7 +15,7 @@ BEGIN OD.DomainName FROM [dbo].[OrganizationView] O INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId - LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId + LEFT JOIN [dbo].[SsoConfig] S ON O.Id = S.OrganizationId WHERE OD.DomainName = @Domain AND O.Enabled = 1 AND OD.VerifiedDate IS NOT NULL diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index e4c474fdc7..2accd2134b 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -56,6 +56,7 @@ CREATE TABLE [dbo].[Organization] ( [LimitItemDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitItemDeletion] DEFAULT (0), [AllowAdminAccessToAllCollectionItems] BIT NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0), [UseRiskInsights] BIT NOT NULL CONSTRAINT [DF_Organization_UseRiskInsights] DEFAULT (0), + [UseOrganizationDomains] BIT NOT NULL CONSTRAINT [DF_Organization_UseOrganizationDomains] DEFAULT (0), [UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0), CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index 1f749630a6..b032bd5a81 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -50,8 +50,10 @@ SELECT O.[LimitCollectionDeletion], O.[AllowAdminAccessToAllCollectionItems], O.[UseRiskInsights], + O.[LimitItemDeletion], O.[UseAdminSponsoredFamilies], - O.[LimitItemDeletion] + O.[UseOrganizationDomains], + OS.[IsAdminInitiated] FROM [dbo].[OrganizationUser] OU LEFT JOIN diff --git a/src/Sql/dbo/Views/OrganizationView.sql b/src/Sql/dbo/Views/OrganizationView.sql index bdc1c4c2e7..58989273fd 100644 --- a/src/Sql/dbo/Views/OrganizationView.sql +++ b/src/Sql/dbo/Views/OrganizationView.sql @@ -1,6 +1,6 @@ -CREATE VIEW [dbo].[OrganizationView] +CREATE VIEW [dbo].[OrganizationView] AS SELECT * FROM - [dbo].[Organization] \ No newline at end of file + [dbo].[Organization] diff --git a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql index f04ad72d1b..bd2485b411 100644 --- a/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql @@ -38,7 +38,8 @@ SELECT O.[UseRiskInsights], O.[UseAdminSponsoredFamilies], P.[Type] ProviderType, - O.[LimitItemDeletion] + O.[LimitItemDeletion], + O.[UseOrganizationDomains] FROM [dbo].[ProviderUser] PU INNER JOIN diff --git a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 0b5f5c1f01..44ad5088cd 100644 --- a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -5,7 +5,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.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs new file mode 100644 index 0000000000..94432b05a0 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Http.Headers; +using Bit.Api.IntegrationTest.Factories; +using Bit.Seeder.Recipes; +using Xunit; +using Xunit.Abstractions; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) +{ + [Theory(Skip = "Performance test")] + [InlineData(100)] + [InlineData(60000)] + public async Task GetAsync(int seats) + { + await using var factory = new ApiApplicationFactory(); + var client = factory.CreateClient(); + + var db = factory.GetDatabaseContext(); + var seeder = new OrganizationWithUsersRecipe(db); + + var orgId = seeder.Seed("Org", seats, "large.test"); + + var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + var response = await client.GetAsync($"/organizations/{orgId}/users?includeCollections=true"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(result); + + stopwatch.Stop(); + testOutputHelper.WriteLine($"Seed: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms"); + } +} diff --git a/test/Api.IntegrationTest/Api.IntegrationTest.csproj b/test/Api.IntegrationTest/Api.IntegrationTest.csproj index 8fa74f98d4..a9d7fd502e 100644 --- a/test/Api.IntegrationTest/Api.IntegrationTest.csproj +++ b/test/Api.IntegrationTest/Api.IntegrationTest.csproj @@ -18,6 +18,7 @@ + diff --git a/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs b/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs index 1901742777..428726aaac 100644 --- a/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs +++ b/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs @@ -1,5 +1,7 @@ -using Bit.Api.AdminConsole.Authorization; +using AutoFixture.Xunit2; +using Bit.Api.AdminConsole.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using NSubstitute; using Xunit; @@ -25,4 +27,42 @@ public class HttpContextExtensionsTests await callback.ReceivedWithAnyArgs(1).Invoke(); } + [Theory] + [InlineAutoData("orgId")] + [InlineAutoData("organizationId")] + public void GetOrganizationId_GivenValidParameter_ReturnsOrganizationId(string paramName, Guid orgId) + { + var httpContext = new DefaultHttpContext + { + Request = { RouteValues = new RouteValueDictionary + { + { "userId", "someGuid" }, + { paramName, orgId.ToString() } + } + } + }; + + var result = httpContext.GetOrganizationId(); + Assert.Equal(orgId, result); + } + + [Theory] + [InlineAutoData("orgId")] + [InlineAutoData("organizationId")] + [InlineAutoData("missingParameter")] + public void GetOrganizationId_GivenMissingOrInvalidGuid_Throws(string paramName) + { + var httpContext = new DefaultHttpContext + { + Request = { RouteValues = new RouteValueDictionary + { + { "userId", "someGuid" }, + { paramName, "invalidGuid" } + } + } + }; + + var exception = Assert.Throws(() => httpContext.GetOrganizationId()); + Assert.Equal(HttpContextExtensions.NoOrgIdError, exception.Message); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs b/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs index 0e260e73e6..71dc5e5aea 100644 --- a/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs @@ -304,7 +304,7 @@ public class GroupsControllerPutTests // Arrange repositories sutProvider.GetDependency().GetManyUserIdsByIdAsync(group.Id).Returns(currentGroupUsers ?? []); sutProvider.GetDependency().GetByIdWithCollectionsAsync(group.Id) - .Returns(new Tuple>(group, currentCollectionAccess ?? [])); + .Returns(new Tuple>(group, currentCollectionAccess ?? [])); if (savingUser != null) { sutProvider.GetDependency().GetByOrganizationAsync(orgId, savingUser.UserId!.Value) diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index 8a33e17053..f7863401b5 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -3,10 +3,10 @@ using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 107b9cdfb1..de54a44bca 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -238,20 +238,13 @@ public class OrganizationUsersControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.Invite(organizationAbility.Id, model)); } - [Theory] - [BitAutoData(true)] - [BitAutoData(false)] + [Theory, BitAutoData] public async Task Get_ReturnsUser( - bool accountDeprovisioningEnabled, OrganizationUserUserDetails organizationUser, ICollection collections, SutProvider sutProvider) { organizationUser.Permissions = null; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(accountDeprovisioningEnabled); - sutProvider.GetDependency() .ManageUsers(organizationUser.OrganizationId) .Returns(true); @@ -267,8 +260,8 @@ public class OrganizationUsersControllerTests var response = await sutProvider.Sut.Get(organizationUser.Id, false); Assert.Equal(organizationUser.Id, response.Id); - Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization); - Assert.Equal(accountDeprovisioningEnabled, response.ClaimedByOrganization); + Assert.True(response.ManagedByOrganization); + Assert.True(response.ClaimedByOrganization); } [Theory] diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 867f8f8ec6..3484c9a995 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -21,7 +21,7 @@ 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.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -140,7 +140,6 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List { null }); var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); @@ -170,7 +169,6 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List { { foundOrg } }); var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); @@ -205,7 +203,6 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List()); await _sut.Leave(orgId); diff --git a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index 8ddd92a5fa..c7c749effd 100644 --- a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -6,7 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 0076d8bca1..77ce06f4f8 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; using Xunit; namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations; diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index ec22583caf..d6b31ce930 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -2,8 +2,6 @@ false - - $(WarningsNotAsErrors);CS8620;CS0169 diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index bd22fd9346..581a7e8f04 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -7,13 +7,13 @@ using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; -using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; 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.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -40,6 +40,7 @@ public class AccountsControllerTests : IDisposable private readonly IPolicyService _policyService; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; @@ -64,6 +65,7 @@ public class AccountsControllerTests : IDisposable _policyService = Substitute.For(); _setInitialMasterPasswordCommand = Substitute.For(); _rotateUserKeyCommand = Substitute.For(); + _twoFactorIsEnabledQuery = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); _cipherValidator = @@ -87,6 +89,7 @@ public class AccountsControllerTests : IDisposable _setInitialMasterPasswordCommand, _tdeOffboardingPasswordCommand, _rotateUserKeyCommand, + _twoFactorIsEnabledQuery, _featureService, _cipherValidator, _folderValidator, @@ -189,21 +192,6 @@ public class AccountsControllerTests : IDisposable await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); } - [Fact] - public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail() - { - var user = GenerateExampleUser(); - ConfigureUserServiceToReturnValidPrincipalFor(user); - _userService.ChangeEmailAsync(user, default, default, default, default, default) - .Returns(Task.FromResult(IdentityResult.Success)); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false); - - await _sut.PostEmail(new EmailRequestModel()); - - await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); - } - [Fact] public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException() { @@ -533,12 +521,11 @@ public class AccountsControllerTests : IDisposable } [Fact] - public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException() + public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException() { var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToAcceptPasswordFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true); var result = await Assert.ThrowsAsync(() => _sut.Delete(new SecretVerificationRequestModel())); @@ -547,12 +534,11 @@ public class AccountsControllerTests : IDisposable } [Fact] - public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed() + public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed() { var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToAcceptPasswordFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false); _userService.DeleteAsync(user).Returns(IdentityResult.Success); diff --git a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs index 81e100c58c..540d23f98b 100644 --- a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -23,7 +22,6 @@ public class DevicesControllerTest private readonly IUntrustDevicesCommand _untrustDevicesCommand; private readonly IUserRepository _userRepositoryMock; private readonly ICurrentContext _currentContextMock; - private readonly IGlobalSettings _globalSettingsMock; private readonly ILogger _loggerMock; private readonly DevicesController _sut; diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index a8c3cf15a9..aff51b0d1d 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null)); + .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); diff --git a/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs index 377bc9c2c8..2ad7686c30 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -13,6 +14,7 @@ using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Api.Test.Billing.Controllers; @@ -146,4 +148,86 @@ public class OrganizationSponsorshipsControllerTests .DidNotReceiveWithAnyArgs() .RemoveSponsorshipAsync(default); } + + [Theory] + [BitAutoData] + public async Task GetSponsoredOrganizations_OrganizationNotFound_ThrowsNotFound( + Guid sponsoringOrgId, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(sponsoringOrgId).ReturnsNull(); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrgId)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetManyBySponsoringOrganizationAsync(default); + } + + [Theory] + [BitAutoData] + public async Task GetSponsoredOrganizations_NotOrganizationOwner_ThrowsNotFound( + Organization sponsoringOrg, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + sutProvider.GetDependency().OrganizationOwner(sponsoringOrg.Id).Returns(false); + sutProvider.GetDependency().OrganizationAdmin(sponsoringOrg.Id).Returns(false); + + // Create a CurrentContextOrganization with ManageUsers set to false + var currentContextOrg = new CurrentContextOrganization + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = false } + }; + sutProvider.GetDependency().Organizations.Returns(new List { currentContextOrg }); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetManyBySponsoringOrganizationAsync(default); + } + + [Theory] + [BitAutoData] + public async Task GetSponsoredOrganizations_Success_ReturnsSponsorships( + Organization sponsoringOrg, + List sponsorships, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + sutProvider.GetDependency().OrganizationOwner(sponsoringOrg.Id).Returns(true); + sutProvider.GetDependency().OrganizationAdmin(sponsoringOrg.Id).Returns(false); + + // Create a CurrentContextOrganization from the sponsoringOrg + var currentContextOrg = new CurrentContextOrganization + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = true } + }; + sutProvider.GetDependency().Organizations.Returns(new List { currentContextOrg }); + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships); + + // Set IsAdminInitiated to true for all test sponsorships + foreach (var sponsorship in sponsorships) + { + sponsorship.IsAdminInitiated = true; + } + + // Act + var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id); + + // Assert + Assert.Equal(sponsorships.Count, result.Data.Count()); + await sutProvider.GetDependency().Received(1) + .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id); + } } diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index df84f74d11..a082caa469 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -1,16 +1,19 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Commercial.Core.Billing.Providers.Services; +using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; 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.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Models.Api; using Bit.Core.Models.BitStripe; @@ -285,6 +288,19 @@ public class ProviderBillingControllerTests Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } }, TaxIds = new StripeList { Data = [new TaxId { Value = "123456789" }] } }, + Items = new StripeList + { + Data = [ + new SubscriptionItem + { + Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise } + }, + new SubscriptionItem + { + Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } + } + ] + }, Status = "unpaid", }; @@ -330,11 +346,21 @@ public class ProviderBillingControllerTests } }; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe) + .Returns(true); + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); foreach (var providerPlan in providerPlans) { - sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType)); + var plan = StaticStore.GetPlan(providerPlan.PlanType); + sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); + var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); + sutProvider.GetDependency().PriceGetAsync(priceId) + .Returns(new Price + { + UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100 + }); } var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); diff --git a/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs b/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs new file mode 100644 index 0000000000..67979f506e --- /dev/null +++ b/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs @@ -0,0 +1,315 @@ +using Bit.Api.Billing.Queries.Organizations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Services; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Stripe.TestHelpers; +using Xunit; + +namespace Bit.Api.Test.Billing.Queries.Organizations; + +[SutProviderCustomize] +public class OrganizationWarningsQueryTests +{ + private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"]; + + [Theory, BitAutoData] + public async Task Run_NoSubscription_NoWarnings( + Organization organization, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .ReturnsNull(); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + FreeTrial: null, + InactiveSubscription: null, + ResellerRenewal: null + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_FreeTrialWarning( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Trialing, + TrialEnd = now.AddDays(7), + Customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + FreeTrial.RemainingTrialDays: 7 + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Unpaid + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider()); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "contact_provider" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Unpaid + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "add_payment_method" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Canceled + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "resubscribe" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Unpaid + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(false); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "contact_owner" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_ResellerRenewalWarning_Upcoming( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + Status = StripeConstants.SubscriptionStatus.Active, + CurrentPeriodEnd = now.AddDays(10), + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider + { + Type = ProviderType.Reseller + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + ResellerRenewal.Type: "upcoming" + }); + + Assert.Equal(now.AddDays(10), response.ResellerRenewal.Upcoming!.RenewalDate); + } + + [Theory, BitAutoData] + public async Task Run_Has_ResellerRenewalWarning_Issued( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + Status = StripeConstants.SubscriptionStatus.Active, + LatestInvoice = new Invoice + { + Status = StripeConstants.InvoiceStatus.Open, + DueDate = now.AddDays(30), + Created = now + }, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider + { + Type = ProviderType.Reseller + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + ResellerRenewal.Type: "issued" + }); + + Assert.Equal(now, response.ResellerRenewal.Issued!.IssuedDate); + Assert.Equal(now.AddDays(30), response.ResellerRenewal.Issued!.DueDate); + } + + [Theory, BitAutoData] + public async Task Run_Has_ResellerRenewalWarning_PastDue( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + const string subscriptionId = "subscription_id"; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Id = subscriptionId, + CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + Status = StripeConstants.SubscriptionStatus.PastDue, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider + { + Type = ProviderType.Reseller + }); + + var dueDate = now.AddDays(-10); + + sutProvider.GetDependency().InvoiceSearchAsync(Arg.Is(options => + options.Query == $"subscription:'{subscriptionId}' status:'open'")).Returns([ + new Invoice { DueDate = dueDate, Created = dueDate.AddDays(-30) } + ]); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + ResellerRenewal.Type: "past_due" + }); + + Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate); + } +} diff --git a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs similarity index 100% rename from test/Api.Test/Tools/Controllers/ReportsControllerTests.cs rename to test/Api.Test/Dirt/ReportsControllerTests.cs diff --git a/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs index 842343ba33..7bab587cf0 100644 --- a/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs @@ -23,11 +23,11 @@ public class SendRotationValidatorTests public async Task ValidateAsync_Success() { // Arrange - var sendService = Substitute.For(); + var sendAuthorizationService = Substitute.For(); var sendRepository = Substitute.For(); var sut = new SendRotationValidator( - sendService, + sendAuthorizationService, sendRepository ); @@ -52,11 +52,11 @@ public class SendRotationValidatorTests public async Task ValidateAsync_SendNotReturnedFromRepository_NotIncludedInOutput() { // Arrange - var sendService = Substitute.For(); + var sendAuthorizationService = Substitute.For(); var sendRepository = Substitute.For(); var sut = new SendRotationValidator( - sendService, + sendAuthorizationService, sendRepository ); @@ -76,11 +76,11 @@ public class SendRotationValidatorTests public async Task ValidateAsync_InputMissingUserSend_Throws() { // Arrange - var sendService = Substitute.For(); + var sendAuthorizationService = Substitute.For(); var sendRepository = Substitute.For(); var sut = new SendRotationValidator( - sendService, + sendAuthorizationService, sendRepository ); diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index f784448e50..b1fa5c9260 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -10,7 +10,9 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; @@ -26,7 +28,9 @@ public class SendsControllerTests : IDisposable private readonly GlobalSettings _globalSettings; private readonly IUserService _userService; private readonly ISendRepository _sendRepository; - private readonly ISendService _sendService; + private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; + private readonly IAnonymousSendCommand _anonymousSendCommand; + private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; @@ -35,7 +39,9 @@ public class SendsControllerTests : IDisposable { _userService = Substitute.For(); _sendRepository = Substitute.For(); - _sendService = Substitute.For(); + _nonAnonymousSendCommand = Substitute.For(); + _anonymousSendCommand = Substitute.For(); + _sendAuthorizationService = Substitute.For(); _sendFileStorageService = Substitute.For(); _globalSettings = new GlobalSettings(); _logger = Substitute.For>(); @@ -44,7 +50,9 @@ public class SendsControllerTests : IDisposable _sut = new SendsController( _sendRepository, _userService, - _sendService, + _sendAuthorizationService, + _anonymousSendCommand, + _nonAnonymousSendCommand, _sendFileStorageService, _logger, _globalSettings, @@ -68,7 +76,8 @@ public class SendsControllerTests : IDisposable send.Data = JsonSerializer.Serialize(new Dictionary()); send.HideEmail = true; - _sendService.AccessAsync(id, null).Returns((send, false, false)); + _sendRepository.GetByIdAsync(Arg.Any()).Returns(send); + _sendAuthorizationService.AccessAsync(send, null).Returns(SendAccessResult.Granted); _userService.GetUserByIdAsync(Arg.Any()).Returns(user); var request = new SendAccessRequestModel(); diff --git a/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs b/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs index 59fb35d32e..8049667011 100644 --- a/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs +++ b/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs @@ -34,11 +34,11 @@ public class SendRequestModelTests Type = SendType.Text, }; - var sendService = Substitute.For(); - sendService.HashPassword(Arg.Any()) + var sendAuthorizationService = Substitute.For(); + sendAuthorizationService.HashPassword(Arg.Any()) .Returns((info) => $"hashed_{(string)info[0]}"); - var send = sendRequest.ToSend(Guid.NewGuid(), sendService); + var send = sendRequest.ToSend(Guid.NewGuid(), sendAuthorizationService); Assert.Equal(deletionDate, send.DeletionDate); Assert.False(send.Disabled); diff --git a/test/Api.Test/Utilities/CommandResultExtensionTests.cs b/test/Api.Test/Utilities/CommandResultExtensionTests.cs deleted file mode 100644 index dafae10b5b..0000000000 --- a/test/Api.Test/Utilities/CommandResultExtensionTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -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 -{ - -} diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 0bdc6ab545..e4643f3185 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -193,49 +193,6 @@ public class CiphersControllerTests } } - [Theory] - [BitAutoData(false)] - [BitAutoData(false)] - [BitAutoData(true)] - public async Task CanEditCiphersAsAdminAsync_Providers( - bool restrictProviders, CipherDetails cipherDetails, CurrentContextOrganization organization, Guid userId, SutProvider sutProvider - ) - { - cipherDetails.OrganizationId = organization.Id; - - // Simulate that the user is a provider for the organization - sutProvider.GetDependency().EditAnyCollection(organization.Id).Returns(true); - sutProvider.GetDependency().ProviderUserForOrgAsync(organization.Id).Returns(true); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails }); - - sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility - { - Id = organization.Id, - AllowAdminAccessToAllCollectionItems = false - }); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(restrictProviders); - - // Non restricted providers should succeed - if (!restrictProviders) - { - await sutProvider.Sut.DeleteAdmin(cipherDetails.Id); - await sutProvider.GetDependency().ReceivedWithAnyArgs() - .DeleteAsync(default, default); - } - else // Otherwise, they should fail - { - await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id)); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .DeleteAsync(default, default); - } - - await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(organization.Id); - } - [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] @@ -456,24 +413,7 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task DeleteAdmin_WithProviderUser_DeletesCipher( - CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) - { - cipherDetails.OrganizationId = Guid.NewGuid(); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List { cipherDetails }); - - await sutProvider.Sut.DeleteAdmin(cipherDetails.Id); - - await sutProvider.GetDependency().Received(1).DeleteAsync(cipherDetails, userId, true); - } - - [Theory] - [BitAutoData] - public async Task DeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task DeleteAdmin_WithProviderUser_ThrowsNotFoundException( Cipher cipher, Guid userId, SutProvider sutProvider) { cipher.OrganizationId = Guid.NewGuid(); @@ -481,7 +421,6 @@ public class CiphersControllerTests 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)); } @@ -737,43 +676,13 @@ public class CiphersControllerTests [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( + public async Task DeleteManyAdmin_WithProviderUser_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)); } @@ -1000,24 +909,7 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task PutDeleteAdmin_WithProviderUser_SoftDeletesCipher( - CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) - { - cipherDetails.OrganizationId = Guid.NewGuid(); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List { cipherDetails }); - - await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); - - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); - } - - [Theory] - [BitAutoData] - public async Task PutDeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task PutDeleteAdmin_WithProviderUser_ThrowsNotFoundException( Cipher cipher, Guid userId, SutProvider sutProvider) { cipher.OrganizationId = Guid.NewGuid(); @@ -1025,7 +917,6 @@ public class CiphersControllerTests 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)); } @@ -1272,43 +1163,13 @@ public class CiphersControllerTests [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( + public async Task PutDeleteManyAdmin_WithProviderUser_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)); } @@ -1546,27 +1407,7 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task PutRestoreAdmin_WithProviderUser_RestoresCipher( - CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) - { - cipherDetails.OrganizationId = Guid.NewGuid(); - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List { cipherDetails }); - - var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); - - Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); - } - - [Theory] - [BitAutoData] - public async Task PutRestoreAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task PutRestoreAdmin_WithProviderUser_ThrowsNotFoundException( CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) { cipherDetails.OrganizationId = Guid.NewGuid(); @@ -1574,7 +1415,6 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id)); } @@ -1896,49 +1736,12 @@ public class CiphersControllerTests [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( + public async Task PutRestoreManyAdmin_WithProviderUser_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/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 03c05ef0f4..ebbfc2a2ba 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Models; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -64,6 +65,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -119,7 +121,7 @@ public class SyncControllerTests collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections); collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List()); // Back to standard test setup - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -129,7 +131,7 @@ public class SyncControllerTests // Asserts // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -155,6 +157,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -205,7 +208,7 @@ public class SyncControllerTests policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies); - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -216,7 +219,7 @@ public class SyncControllerTests // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -244,6 +247,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -283,7 +287,7 @@ public class SyncControllerTests collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections); collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List()); // Back to standard test setup - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -293,7 +297,7 @@ public class SyncControllerTests // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -315,6 +319,7 @@ public class SyncControllerTests private async Task AssertMethodsCalledAsync(IUserService userService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, IFolderRepository folderRepository, ICipherRepository cipherRepository, ISendRepository sendRepository, @@ -356,7 +361,7 @@ public class SyncControllerTests .GetManyByUserIdAsync(default); } - await userService.ReceivedWithAnyArgs(1) + await twoFactorIsEnabledQuery.ReceivedWithAnyArgs(1) .TwoFactorIsEnabledAsync(default(ITwoFactorProvidersUser)); await userService.ReceivedWithAnyArgs(1) .HasPremiumFromOrganization(default); diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index 26ce310b9c..90f8a09ea0 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSubstitute; +using NSubstitute.ReceivedExtensions; using Xunit; namespace Bit.Billing.Test.Controllers; @@ -71,6 +72,41 @@ public class FreshdeskControllerTests _ = mockHttpMessageHandler.Received(1).Send(Arg.Is(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any()); } + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhook_add_note_when_user_is_invalid( + string freshdeskWebhookKey, FreshdeskWebhookModel model, + SutProvider sutProvider) + { + // Arrange - for an invalid user + model.TicketContactEmail = "invalid@user"; + sutProvider.GetDependency().GetByEmailAsync(model.TicketContactEmail).Returns((User)null); + sutProvider.GetDependency>().Value.FreshDesk.WebhookKey.Returns(WebhookKey); + + var mockHttpMessageHandler = Substitute.ForPartsOf(); + var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockResponse); + var httpClient = new HttpClient(mockHttpMessageHandler); + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); + + // Act + var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model); + + // Assert + var statusCodeResult = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); + + await mockHttpMessageHandler + .Received(1).Send( + Arg.Is( + m => m.Method == HttpMethod.Post + && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes") + && m.Content.ReadAsStringAsync().Result.Contains("No user found")), + Arg.Any()); + } + + [Theory] [BitAutoData((string)null, null)] [BitAutoData((string)null)] diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index e080dd8288..7d95157bd2 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -4,10 +4,10 @@ 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.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Utilities; diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs new file mode 100644 index 0000000000..0946841347 --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Xunit; + +namespace Bit.Core.Test.Models.Data.Integrations; + +public class IntegrationMessageTests +{ + [Fact] + public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate() + { + var message = new IntegrationMessage + { + RetryCount = 2, + DelayUntilDate = null + }; + + var baseline = DateTime.UtcNow; + message.ApplyRetry(baseline); + + Assert.Equal(3, message.RetryCount); + Assert.NotNull(message.DelayUntilDate); + Assert.True(message.DelayUntilDate > baseline); + } + + [Fact] + public void FromToJson_SerializesCorrectly() + { + var message = new IntegrationMessage + { + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + RenderedTemplate = "This is the message", + IntegrationType = IntegrationType.Webhook, + RetryCount = 2, + DelayUntilDate = null + }; + + var json = message.ToJson(); + var result = IntegrationMessage.FromJson(json); + + Assert.Equal(message.Configuration, result.Configuration); + Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); + Assert.Equal(message.IntegrationType, result.IntegrationType); + Assert.Equal(message.RetryCount, result.RetryCount); + } + + [Fact] + public void FromJson_InvalidJson_ThrowsJsonException() + { + var json = "{ Invalid JSON"; + Assert.Throws(() => IntegrationMessage.FromJson(json)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index daa560f3bc..b0774927e3 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -166,7 +166,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( + public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( OrganizationDomain domain, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency() @@ -177,10 +177,6 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(domain.DomainName, domain.Txt) .Returns(true); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .UserId.Returns(userId); @@ -196,33 +192,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( - OrganizationDomain domain, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetClaimedDomainsByDomainNameAsync(domain.DomainName) - .Returns([]); - - sutProvider.GetDependency() - .ResolveAsync(domain.DomainName, domain.Txt) - .Returns(true); - - sutProvider.GetDependency() - .UserId.Returns(Guid.NewGuid()); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - - await sutProvider.GetDependency() - .DidNotReceive() - .SaveAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( + public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( OrganizationDomain domain, SutProvider sutProvider) { sutProvider.GetDependency() @@ -236,10 +206,6 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .UserId.Returns(Guid.NewGuid()); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); await sutProvider.GetDependency() @@ -248,33 +214,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( - OrganizationDomain domain, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetClaimedDomainsByDomainNameAsync(domain.DomainName) - .Returns([]); - - sutProvider.GetDependency() - .ResolveAsync(domain.DomainName, domain.Txt) - .Returns(false); - - sutProvider.GetDependency() - .UserId.Returns(Guid.NewGuid()); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - - await sutProvider.GetDependency() - .DidNotReceive() - .SaveAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain( + public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain( ICollection organizationUsers, OrganizationDomain domain, Organization organization, @@ -306,10 +246,6 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .UserId.Returns(Guid.NewGuid()); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(domain.OrganizationId) .Returns(mockedUsers); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 2dda23481a..540bac4d1c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -1,7 +1,11 @@ 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.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -164,9 +168,6 @@ public class AcceptOrgUserCommandTests // Arrange SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); - // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); - // Organization they are trying to join requires 2FA var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; sutProvider.GetDependency() @@ -183,6 +184,107 @@ public class AcceptOrgUserCommandTests exception.Message); } + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // Organization they are trying to join requires 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = orgUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal("You cannot join this organization until you enable two-step login on your user account.", + exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // User has 2FA enabled + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); + + // Organization they are trying to join requires 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = orgUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + + await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(ou => ou.Status == OrganizationUserStatusType.Accepted)); + } + + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // Organization they are trying to join doesn't require 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = Guid.NewGuid(), + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + + await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(ou => ou.Status == OrganizationUserStatusType.Accepted)); + } + [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] @@ -645,9 +747,6 @@ public class AcceptOrgUserCommandTests .AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg) .Returns(false); - // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); - // Org does not require 2FA sutProvider.GetDependency().GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index 06335f668d..366d8cb2d6 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -1,6 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -321,4 +324,122 @@ public class ConfirmOrganizationUserCommandTests Assert.Contains("User does not have two-step login enabled.", result[1].Item2); Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorRequired_ThrowsBadRequestException( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var featureService = sutProvider.GetDependency(); + var policyRequirementQuery = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + policyRequirementQuery.GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = org.Id, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id)); + Assert.Contains("User does not have two-step login enabled.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorNotRequired_Succeeds( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var featureService = sutProvider.GetDependency(); + var policyRequirementQuery = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + policyRequirementQuery.GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = Guid.NewGuid(), + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager); + await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(users => users.Contains(orgUser) && users.Count == 1)); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorEnabled_Succeeds( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var featureService = sutProvider.GetDependency(); + var policyRequirementQuery = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + policyRequirementQuery.GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = org.Id, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager); + await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(users => users.Contains(orgUser) && users.Count == 1)); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs index fd6d827791..85dc643022 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs @@ -25,13 +25,13 @@ public class GetOrganizationUsersClaimedStatusQueryTests } [Theory, BitAutoData] - public async Task GetUsersOrganizationManagementStatusAsync_WithUseSsoEnabled_Success( + public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsEnabled_Success( Organization organization, ICollection usersWithClaimedDomain, SutProvider sutProvider) { organization.Enabled = true; - organization.UseSso = true; + organization.UseOrganizationDomains = true; var userIdWithoutClaimedDomain = Guid.NewGuid(); var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List { userIdWithoutClaimedDomain }).ToList(); @@ -51,13 +51,13 @@ public class GetOrganizationUsersClaimedStatusQueryTests } [Theory, BitAutoData] - public async Task GetUsersOrganizationManagementStatusAsync_WithUseSsoDisabled_ReturnsAllFalse( + public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsDisabled_ReturnsAllFalse( Organization organization, ICollection usersWithClaimedDomain, SutProvider sutProvider) { organization.Enabled = true; - organization.UseSso = false; + organization.UseOrganizationDomains = false; var userIdWithoutClaimedDomain = Guid.NewGuid(); var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List { userIdWithoutClaimedDomain }).ToList(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 0592b481d3..e54e4aa99b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -11,12 +10,13 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.M using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.StaticStore; @@ -80,7 +80,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(NoUsersToInviteError.Code, (result as Failure).ErrorMessage); + Assert.Equal(NoUsersToInviteError.Code, (result as Failure)!.Error.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -209,7 +209,7 @@ public class InviteOrganizationUserCommandTests Assert.IsType>(result); var failure = result as Failure; - Assert.Equal(errorMessage, failure!.ErrorMessage); + Assert.Equal(errorMessage, failure!.Error.Message); await sutProvider.GetDependency() .DidNotReceive() @@ -571,7 +571,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.ErrorMessage); + Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.Error.Message); // org user revert await orgUserRepository.Received(1).DeleteManyAsync(Arg.Is>(x => x.Count() == 1)); @@ -677,7 +677,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2, Arg.Is>(emails => emails.Any(email => email == "provider@email.com"))); } @@ -768,7 +768,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SendOrganizationAutoscaledEmailAsync(organization, 1, Arg.Is>(emails => emails.Any(email => email == "provider@email.com"))); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index 191ef05603..7c06e04256 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -2,7 +2,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -61,7 +61,7 @@ public class InviteOrganizationUsersValidatorTests _ = await sutProvider.Sut.ValidateAsync(request); - sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) .ValidateUpdateAsync(Arg.Is(x => x.SmSeatsChanged == true && x.SmSeats == 12)); @@ -156,6 +156,6 @@ public class InviteOrganizationUsersValidatorTests var result = await sutProvider.Sut.ValidateAsync(request); Assert.IsType>(result); - Assert.Equal("Some Secrets Manager Failure", (result as Invalid)!.ErrorMessageString); + Assert.Equal("Some Secrets Manager Failure", (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs index 508b9f3cb0..be5586f8a6 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -36,7 +36,7 @@ public class InviteUserOrganizationValidationTests var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); - Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.Error.Message); } [Theory] @@ -53,6 +53,6 @@ public class InviteUserOrganizationValidationTests var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); - Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs index bcca89e1d2..d508f7cc5e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; @@ -39,7 +39,7 @@ public class InviteUserPaymentValidationTests }); Assert.IsType>(result); - Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid)!.Error.Message); } [Fact] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs index c320ada8cb..571832d675 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Test.Common.AutoFixture; @@ -67,7 +67,7 @@ public class InviteUsersPasswordManagerValidatorTests var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); - Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.Error.Message); } [Theory] @@ -88,6 +88,6 @@ public class InviteUsersPasswordManagerValidatorTests var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); - Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 3578706e47..c105c7a9ee 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -40,43 +40,6 @@ public class RemoveOrganizationUserCommandTests // Act await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - // Assert - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteAsync(organizationUser); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); - } - - [Theory, BitAutoData] - public async Task RemoveUser_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( - [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, - SutProvider sutProvider) - { - // Arrange - organizationUser.OrganizationId = deletingUser.OrganizationId; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - sutProvider.GetDependency() - .GetByIdAsync(deletingUser.Id) - .Returns(deletingUser); - sutProvider.GetDependency() - .OrganizationOwner(deletingUser.OrganizationId) - .Returns(true); - - // Act - await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - // Assert await sutProvider.GetDependency() .Received(1) @@ -235,15 +198,12 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUserAsync_WithDeletingUserId_WithAccountDeprovisioningEnabled_WhenUserIsManaged_ThrowsException( + public async Task RemoveUserAsync_WithDeletingUserId_WhenUserIsManaged_ThrowsException( [OrganizationUser(status: OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser, Guid deletingUserId, SutProvider sutProvider) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); sutProvider.GetDependency() .GetByIdAsync(orgUser.Id) .Returns(orgUser); @@ -285,34 +245,6 @@ public class RemoveOrganizationUserCommandTests .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); } - [Theory, BitAutoData] - public async Task RemoveUser_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success( - [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, - EventSystemUser eventSystemUser, SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - // Act - await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); - - // Assert - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteAsync(organizationUser); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); - } - [Theory] [BitAutoData] public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException( @@ -474,7 +406,6 @@ public class RemoveOrganizationUserCommandTests var sutProvider = SutProviderFactory(); var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUserIds = organizationUsers.Select(u => u.Id); @@ -499,60 +430,6 @@ public class RemoveOrganizationUserCommandTests // Act var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - // Assert - Assert.Equal(2, result.Count()); - Assert.All(result, r => Assert.Empty(r.ErrorMessage)); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventsAsync( - Arg.Is>(i => - i.First().OrganizationUser.Id == orgUser1.Id - && i.Last().OrganizationUser.Id == orgUser2.Id - && i.All(u => u.DateTime == eventDate))); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2) - { - // Arrange - var sutProvider = SutProviderFactory(); - var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetManyAsync(default) - .ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .GetByIdAsync(deletingUser.Id) - .Returns(deletingUser); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) - .Returns(true); - sutProvider.GetDependency() - .OrganizationOwner(deletingUser.OrganizationId) - .Returns(true); - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync( - deletingUser.OrganizationId, - Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))) - .Returns(new Dictionary { { orgUser1.Id, false }, { orgUser2.Id, false } }); - - // Act - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - // Assert Assert.Equal(2, result.Count()); Assert.All(result, r => Assert.Empty(r.ErrorMessage)); @@ -638,7 +515,7 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_WithAccountDeprovisioningEnabled_ThrowsException( + public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_ThrowsException( [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser, OrganizationUser deletingUser, SutProvider sutProvider) @@ -646,10 +523,6 @@ public class RemoveOrganizationUserCommandTests // Arrange orgUser.OrganizationId = deletingUser.OrganizationId; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetManyAsync(Arg.Is>(i => i.Contains(orgUser.Id))) .Returns(new[] { orgUser }); @@ -739,51 +612,6 @@ public class RemoveOrganizationUserCommandTests && u.DateTime == eventDate))); } - [Theory, BitAutoData] - public async Task RemoveUsers_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success( - EventSystemUser eventSystemUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, - OrganizationUser orgUser2) - { - // Arrange - var sutProvider = SutProviderFactory(); - var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; - orgUser1.OrganizationId = orgUser2.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetManyAsync(default) - .ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any>()) - .Returns(true); - - // Act - var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser); - - // Assert - Assert.Equal(2, result.Count()); - Assert.All(result, r => Assert.Empty(r.ErrorMessage)); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventsAsync( - Arg.Is>( - i => i.First().OrganizationUser.Id == orgUser1.Id - && i.Last().OrganizationUser.Id == orgUser2.Id - && i.All(u => u.EventSystemUser == eventSystemUser - && u.DateTime == eventDate))); - } - [Theory, BitAutoData] public async Task RemoveUsers_WithEventSystemUser_WithMismatchingOrganizationId_ThrowsException( EventSystemUser eventSystemUser, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs index d6880a3a12..fbd711307c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs @@ -1,6 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -208,6 +211,57 @@ public class RestoreOrganizationUserCommandTests .PushSyncOrgKeysAsync(Arg.Any()); } + [Theory, BitAutoData] + public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + organizationUser.Email = null; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) }); + + RestoreUser_Setup(organization, owner, organizationUser, sutProvider); + + sutProvider.GetDependency() + .GetAsync(organizationUser.UserId.Value) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); + + Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); + } + [Theory, BitAutoData] public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success( Organization organization, @@ -235,6 +289,46 @@ public class RestoreOrganizationUserCommandTests .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } + [Theory, BitAutoData] + public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithUser2FAConfigured_Success( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + RestoreUser_Setup(organization, owner, organizationUser, sutProvider); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); + sutProvider.GetDependency() + .GetAsync(organizationUser.UserId.Value) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); + + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); + } + [Theory, BitAutoData] public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails( Organization organization, @@ -277,45 +371,6 @@ public class RestoreOrganizationUserCommandTests .PushSyncOrgKeysAsync(Arg.Any()); } - [Theory, BitAutoData] - public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails( - Organization organization, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, - SutProvider sutProvider) - { - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke - secondOrganizationUser.UserId = organizationUser.UserId; - RestoreUser_Setup(organization, owner, organizationUser, sutProvider); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) }); - - sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) - .Returns(true); - - var user = new User { Email = "test@bitwarden.com" }; - sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - - Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant()); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .RestoreAsync(Arg.Any(), Arg.Any()); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .PushSyncOrgKeysAsync(Arg.Any()); - } - [Theory, BitAutoData] public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails( Organization organization, @@ -364,20 +419,42 @@ public class RestoreOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails( + public async Task RestoreUser_WithPolicyRequirementsEnabled_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails( Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, SutProvider sutProvider) { - organizationUser.Email = null; - + organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke + secondOrganizationUser.UserId = organizationUser.UserId; RestoreUser_Setup(organization, owner, organizationUser, sutProvider); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByUserAsync(organizationUser.UserId.Value) + .Returns(new[] { organizationUser, secondOrganizationUser }); sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) - .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } - ]); + .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) + .Returns(new[] + { + new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked } + }); + + sutProvider.GetDependency() + .GetAsync(organizationUser.UserId.Value) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); var user = new User { Email = "test@bitwarden.com" }; sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); @@ -385,7 +462,7 @@ public class RestoreOrganizationUserCommandTests var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant()); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -398,35 +475,6 @@ public class RestoreOrganizationUserCommandTests .PushSyncOrgKeysAsync(Arg.Any()); } - [Theory, BitAutoData] - public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success( - Organization organization, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, - SutProvider sutProvider) - { - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke - RestoreUser_Setup(organization, owner, organizationUser, sutProvider); - - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) - .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } - ]); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) }); - - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); - - await sutProvider.GetDependency() - .Received(1) - .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); - } - [Theory, BitAutoData] public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails( Organization organization, @@ -672,6 +720,77 @@ public class RestoreOrganizationUserCommandTests .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited); } + [Theory, BitAutoData] + public async Task RestoreUsers_WithPolicyRequirementsEnabled_With2FAPolicy_BlocksNonCompliantUser(Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3, + SutProvider sutProvider) + { + // Arrange + RestoreUser_Setup(organization, owner, orgUser1, sutProvider); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + var organizationUserRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var policyService = sutProvider.GetDependency(); + var userService = Substitute.For(); + + orgUser1.Email = orgUser2.Email = null; + orgUser3.UserId = null; + orgUser3.Key = null; + orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id; + organizationUserRepository + .GetManyAsync(Arg.Is>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id))) + .Returns(new[] { orgUser1, orgUser2, orgUser3 }); + + userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" }); + + // Setup 2FA policy + sutProvider.GetDependency() + .GetAsync(Arg.Any()) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organization.Id, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + + // User1 has 2FA, User2 doesn't + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> + { + (orgUser1.UserId!.Value, true), + (orgUser2.UserId!.Value, false) + }); + + // Act + var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService); + + // Assert + Assert.Equal(3, result.Count); + Assert.Empty(result[0].Item2); // First user should succeed + Assert.Contains("two-step login", result[1].Item2); // Second user should fail + Assert.Empty(result[2].Item2); // Third user should succeed + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed); + await organizationUserRepository + .DidNotReceive() + .RestoreAsync(orgUser2.Id, Arg.Any()); + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited); + } + [Theory, BitAutoData] public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs new file mode 100644 index 0000000000..83ea4798db --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs @@ -0,0 +1,169 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Fakes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class InitPendingOrganizationCommandTests +{ + + private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For(); + private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); + + [Theory, BitAutoData] + public async Task Init_Organization_Success(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PrivateKey = null; + org.PublicKey = null; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var organizationServcie = sutProvider.GetDependency(); + var collectionRepository = sutProvider.GetDependency(); + + await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token); + + await organizationRepository.Received().GetByIdAsync(orgId); + await organizationServcie.Received().UpdateAsync(org); + await collectionRepository.DidNotReceiveWithAnyArgs().CreateAsync(default); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_With_CollectionName_Success(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, string collectionName, OrganizationUser orgUser) + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PrivateKey = null; + org.PublicKey = null; + org.Id = orgId; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var organizationServcie = sutProvider.GetDependency(); + var collectionRepository = sutProvider.GetDependency(); + + await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, collectionName, token); + + await organizationRepository.Received().GetByIdAsync(orgId); + await organizationServcie.Received().UpdateAsync(org); + + await collectionRepository.Received().CreateAsync( + Arg.Any(), + Arg.Is>(l => l == null), + Arg.Is>(l => l.Any(i => i.Manage == true))); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Is_Enabled(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.Enabled = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization is already enabled.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Is_Not_Pending(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + + + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.Status = Enums.OrganizationStatusType.Created; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization is not on a Pending status.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Has_Public_Key(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PublicKey = publicKey; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization already has a Public Key.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Has_Private_Key(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PublicKey = null; + org.PrivateKey = privateKey; + org.Enabled = false; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization already has a Private Key.", exception.Message); + + } + + public string CreateToken(OrganizationUser orgUser, Guid orgUserId, SutProvider sutProvider) + { + sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); + sutProvider.Create(); + + _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser); + var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable); + sutProvider.GetDependency().GetByIdAsync(orgUserId).Returns(orgUser); + + return protectedToken; + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs new file mode 100644 index 0000000000..b13c7e5b65 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs @@ -0,0 +1,169 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +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 Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp; + +[SutProviderCustomize] +public class ProviderClientOrganizationSignUpCommandTests +{ + [Theory] + [BitAutoData(PlanType.TeamsAnnually)] + [BitAutoData(PlanType.TeamsMonthly)] + [BitAutoData(PlanType.EnterpriseAnnually)] + [BitAutoData(PlanType.EnterpriseMonthly)] + public async Task SignupClientAsync_ValidParameters_CreatesOrganizationSuccessfully( + PlanType planType, + OrganizationSignup signup, + string collectionName, + SutProvider sutProvider) + { + signup.Plan = planType; + signup.AdditionalSeats = 15; + signup.CollectionName = collectionName; + + var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns(plan); + + var result = await sutProvider.Sut.SignUpClientOrganizationAsync(signup); + + Assert.NotNull(result.Organization); + Assert.NotNull(result.DefaultCollection); + Assert.Equal(collectionName, result.DefaultCollection.Name); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(o => + o.Name == signup.Name && + o.BillingEmail == signup.BillingEmail && + o.PlanType == plan.Type && + o.Seats == signup.AdditionalSeats && + o.MaxCollections == plan.PasswordManager.MaxCollections && + o.UsePasswordManager == true && + o.UseSecretsManager == false && + o.Status == OrganizationStatusType.Created + ) + ); + + await sutProvider.GetDependency() + .Received(1) + .RaiseEventAsync(Arg.Is(referenceEvent => + referenceEvent.Type == ReferenceEventType.Signup && + referenceEvent.PlanName == plan.Name && + referenceEvent.PlanType == plan.Type && + referenceEvent.Seats == result.Organization.Seats && + referenceEvent.Storage == result.Organization.MaxStorageGb && + referenceEvent.SignupInitiationPath == signup.InitiationPath + )); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(c => + c.Name == collectionName && + c.OrganizationId == result.Organization.Id + ), + Arg.Any>(), + Arg.Any>() + ); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(k => + k.OrganizationId == result.Organization.Id && + k.Type == OrganizationApiKeyType.Default + ) + ); + + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(Arg.Is(o => o.Id == result.Organization.Id)); + } + + [Theory] + [BitAutoData] + public async Task SignupClientAsync_NullPlan_ThrowsBadRequestException( + OrganizationSignup signup, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns((Plan)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpClientOrganizationAsync(signup)); + + Assert.Contains(ProviderClientOrganizationSignUpCommand.PlanNullErrorMessage, exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignupClientAsync_NegativeAdditionalSeats_ThrowsBadRequestException( + OrganizationSignup signup, + SutProvider sutProvider) + { + signup.Plan = PlanType.TeamsMonthly; + signup.AdditionalSeats = -5; + + var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns(plan); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpClientOrganizationAsync(signup)); + + Assert.Contains(ProviderClientOrganizationSignUpCommand.AdditionalSeatsNegativeErrorMessage, exception.Message); + } + + [Theory] + [BitAutoData(PlanType.TeamsAnnually)] + public async Task SignupClientAsync_WhenExceptionIsThrown_CleanupIsPerformed( + PlanType planType, + OrganizationSignup signup, + SutProvider sutProvider) + { + signup.Plan = planType; + + var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns(plan); + + sutProvider.GetDependency() + .When(x => x.CreateAsync(Arg.Any())) + .Do(_ => throw new Exception()); + + var thrownException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpClientOrganizationAsync(signup)); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Is(o => o.Name == signup.Name)); + + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(Arg.Any()); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..c20ea494ab --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs @@ -0,0 +1,117 @@ +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.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class RequireTwoFactorPolicyRequirementFactoryTests +{ + [Theory] + [BitAutoData] + public void IsTwoFactorRequiredForOrganization_WithNoPolicies_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId)); + } + + [Theory] + [BitAutoData] + public void IsTwoFactorRequiredForOrganization_WithOrganizationPolicy_ReturnsTrue( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create( + [ + new PolicyDetails + { + OrganizationId = organizationId, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ]); + + Assert.True(actual.IsTwoFactorRequiredForOrganization(organizationId)); + } + + [Theory] + [BitAutoData] + public void IsTwoFactorRequiredForOrganization_WithOtherOrganizationPolicy_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create( + [ + new PolicyDetails + { + OrganizationId = Guid.NewGuid(), + PolicyType = PolicyType.TwoFactorAuthentication, + }, + ]); + + Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId)); + } + + [Theory, BitAutoData] + public void OrganizationsRequiringTwoFactor_WithNoPolicies_ReturnsEmptyCollection( + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.Empty(actual.OrganizationsRequiringTwoFactor); + } + + [Theory, BitAutoData] + public void OrganizationsRequiringTwoFactor_WithMultiplePolicies_ReturnsActiveMemberships( + Guid orgId1, Guid orgUserId1, Guid orgId2, Guid orgUserId2, + Guid orgId3, Guid orgUserId3, Guid orgId4, Guid orgUserId4, + SutProvider sutProvider) + { + var policies = new[] + { + new PolicyDetails + { + OrganizationId = orgId1, + OrganizationUserId = orgUserId1, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Accepted + }, + new PolicyDetails + { + OrganizationId = orgId2, + OrganizationUserId = orgUserId2, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed + }, + new PolicyDetails + { + OrganizationId = orgId3, + OrganizationUserId = orgUserId3, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Invited + }, + new PolicyDetails + { + OrganizationId = orgId4, + OrganizationUserId = orgUserId4, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Revoked + } + }; + + var actual = sutProvider.Sut.Create(policies); + + var result = actual.OrganizationsRequiringTwoFactor.ToList(); + Assert.Equal(2, result.Count); + Assert.Contains(result, p => p.OrganizationId == orgId1 && p.OrganizationUserId == orgUserId1); + Assert.Contains(result, p => p.OrganizationId == orgId2 && p.OrganizationUserId == orgUserId2); + Assert.DoesNotContain(result, p => p.OrganizationId == orgId3 && p.OrganizationUserId == orgUserId3); + Assert.DoesNotContain(result, p => p.OrganizationId == orgId4 && p.OrganizationUserId == orgUserId4); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index d2809102aa..e982a67e46 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; @@ -11,7 +12,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -122,9 +122,6 @@ public class SingleOrgPolicyValidatorTests sutProvider.GetDependency().UserId.Returns(savingUserId); sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) .Returns(new CommandResult()); @@ -148,161 +145,4 @@ public class SingleOrgPolicyValidatorTests .Received(1) .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( - [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg, false)] Policy policy, - Guid savingUserId, - Guid nonCompliantUserId, - Organization organization, SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - - var compliantUser1 = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user1@example.com" - }; - - var compliantUser2 = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user2@example.com" - }; - - var nonCompliantUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = nonCompliantUserId, - Email = "user3@example.com" - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([compliantUser1, compliantUser2, nonCompliantUser]); - - var otherOrganizationUser = new OrganizationUser - { - Id = Guid.NewGuid(), - OrganizationId = new Guid(), - UserId = nonCompliantUserId, - Status = OrganizationUserStatusType.Confirmed - }; - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) - .Returns([otherOrganizationUser]); - - sutProvider.GetDependency().UserId.Returns(savingUserId); - sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); - - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - sutProvider.GetDependency() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) - .Returns(new CommandResult()); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - await sutProvider.GetDependency() - .DidNotReceive() - .RemoveUserAsync(policyUpdate.OrganizationId, compliantUser1.Id, savingUserId); - await sutProvider.GetDependency() - .DidNotReceive() - .RemoveUserAsync(policyUpdate.OrganizationId, compliantUser2.Id, savingUserId); - await sutProvider.GetDependency() - .Received(1) - .RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId); - await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email); - await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email); - await sutProvider.GetDependency() - .Received(1) - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_WhenAccountDeprovisioningIsEnabled_ThenUsersAreRevoked( - [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg, false)] Policy policy, - Guid savingUserId, - Guid nonCompliantUserId, - Organization organization, SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - - var compliantUser1 = new OrganizationUserUserDetails - { - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user1@example.com" - }; - - var compliantUser2 = new OrganizationUserUserDetails - { - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user2@example.com" - }; - - var nonCompliantUser = new OrganizationUserUserDetails - { - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = nonCompliantUserId, - Email = "user3@example.com" - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([compliantUser1, compliantUser2, nonCompliantUser]); - - var otherOrganizationUser = new OrganizationUser - { - OrganizationId = new Guid(), - UserId = nonCompliantUserId, - Status = OrganizationUserStatusType.Confirmed - }; - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) - .Returns([otherOrganizationUser]); - - sutProvider.GetDependency().UserId.Returns(savingUserId); - sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId) - .Returns(organization); - - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - - sutProvider.GetDependency() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) - .Returns(new CommandResult()); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - await sutProvider.GetDependency() - .Received() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); - } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index 0edc2b5973..6a97f6bc1e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -4,11 +4,10 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -24,7 +23,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidat public class TwoFactorAuthenticationPolicyValidatorTests { [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( + public async Task OnSaveSideEffectsAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws( Organization organization, [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, @@ -33,249 +32,6 @@ public class TwoFactorAuthenticationPolicyValidatorTests policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var orgUserDetailUserInvited = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Invited, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user1@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user2@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailAdmin = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Admin, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "admin@test.com", - Name = "ADMIN", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns(new List - { - orgUserDetailUserInvited, - orgUserDetailUserAcceptedWith2FA, - orgUserDetailUserAcceptedWithout2FA, - orgUserDetailAdmin - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Any>()) - .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() - { - (orgUserDetailUserInvited, false), - (orgUserDetailUserAcceptedWith2FA, true), - (orgUserDetailUserAcceptedWithout2FA, false), - (orgUserDetailAdmin, false), - }); - - var savingUserId = Guid.NewGuid(); - sutProvider.GetDependency().UserId.Returns(savingUserId); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - var removeOrganizationUserCommand = sutProvider.GetDependency(); - - await removeOrganizationUserCommand.Received() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId); - await sutProvider.GetDependency().Received() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email); - - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email); - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email); - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_UsersToBeRemovedDontHaveMasterPasswords_Throws( - Organization organization, - [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, - [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, - SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - - var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user1@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user2@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailAdmin = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Admin, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "admin@test.com", - Name = "ADMIN", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List - { - orgUserDetailUserWith2FAAndMP, - orgUserDetailUserWith2FANoMP, - orgUserDetailUserWithout2FA, - orgUserDetailAdmin - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(ids => - ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value) - && ids.Contains(orgUserDetailUserWithout2FA.UserId.Value) - && ids.Contains(orgUserDetailAdmin.UserId.Value))) - .Returns(new List<(Guid userId, bool hasTwoFactor)>() - { - (orgUserDetailUserWith2FANoMP.UserId.Value, true), - (orgUserDetailUserWithout2FA.UserId.Value, false), - (orgUserDetailAdmin.UserId.Value, false), - }); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy)); - - Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, badRequestException.Message); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsDisabled_ThenRevokeUserCommandShouldNotBeCalled( - Organization organization, - [PolicyUpdate(PolicyType.TwoFactorAuthentication)] - PolicyUpdate policyUpdate, - [Policy(PolicyType.TwoFactorAuthentication, false)] - Policy policy, - SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - var orgUserDetailUserAcceptedWithout2Fa = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns(new List - { - orgUserDetailUserAcceptedWithout2Fa - }); - - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Any>()) - .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() - { - (orgUserDetailUserAcceptedWithout2Fa, false), - }); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - await sutProvider.GetDependency() - .DidNotReceive() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsEnabledAndUserDoesNotHaveMasterPassword_ThenNonCompliantMembersErrorMessageWillReturn( - Organization organization, - [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, - [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, - SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -304,7 +60,7 @@ public class TwoFactorAuthenticationPolicyValidatorTests } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_WhenAccountProvisioningIsEnabledAndUserHasMasterPassword_ThenUserWillBeRevoked( + public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers( Organization organization, [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, @@ -313,10 +69,6 @@ public class TwoFactorAuthenticationPolicyValidatorTests policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails { Id = Guid.NewGuid(), diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs new file mode 100644 index 0000000000..f0a0d1d724 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventIntegrationHandlerTests +{ + private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; + private const string _templateWithOrganization = "Org: #OrganizationName#"; + private const string _templateWithUser = "#UserName#, #UserEmail#"; + private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; + private const string _url = "https://localhost"; + private const string _url2 = "https://example.com"; + private readonly IIntegrationPublisher _integrationPublisher = Substitute.For(); + + private SutProvider> GetSutProvider( + List configurations) + { + var configurationRepository = Substitute.For(); + configurationRepository.GetConfigurationDetailsAsync(Arg.Any(), + IntegrationType.Webhook, Arg.Any()).Returns(configurations); + + return new SutProvider>() + .SetDependency(configurationRepository) + .SetDependency(_integrationPublisher) + .SetDependency(IntegrationType.Webhook) + .Create(); + } + + private static IntegrationMessage expectedMessage(string template) + { + return new IntegrationMessage() + { + IntegrationType = IntegrationType.Webhook, + Configuration = new WebhookIntegrationConfigurationDetails(_url), + RenderedTemplate = template, + RetryCount = 0, + DelayUntilDate = null + }; + } + + private static List NoConfigurations() + { + return []; + } + + private static List OneConfiguration(string template) + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.Template = template; + + return [config]; + } + + private static List TwoConfigurations(string template) + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.Template = template; + var config2 = Substitute.For(); + config2.Configuration = null; + config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 }); + config2.Template = template; + + return [config, config2]; + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(NoConfigurations()); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + Assert.Empty(_integrationPublisher.ReceivedCalls()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); + var user = Substitute.For(); + user.Email = "test@example.com"; + user.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); + var organization = Substitute.For(); + organization.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var user = Substitute.For(); + user.Email = "test@example.com"; + user.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) + { + var sutProvider = GetSutProvider(NoConfigurations()); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + Assert.Empty(_integrationPublisher.ReceivedCalls()); + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + + foreach (var eventMessage in eventMessages) + { + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + } + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes( + List eventMessages) + { + var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + + foreach (var eventMessage in eventMessages) + { + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + + expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + } + } +} diff --git a/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs new file mode 100644 index 0000000000..1a42d846f2 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs @@ -0,0 +1,65 @@ +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventRouteServiceTests +{ + private readonly IEventWriteService _broadcastEventWriteService = Substitute.For(); + private readonly IEventWriteService _storageEventWriteService = Substitute.For(); + private readonly IFeatureService _featureService = Substitute.For(); + private readonly EventRouteService Subject; + + public EventRouteServiceTests() + { + Subject = new EventRouteService(_broadcastEventWriteService, _storageEventWriteService, _featureService); + } + + [Theory, BitAutoData] + public async Task CreateAsync_FlagDisabled_EventSentToStorageService(EventMessage eventMessage) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false); + + await Subject.CreateAsync(eventMessage); + + await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + await _storageEventWriteService.Received(1).CreateAsync(eventMessage); + } + + [Theory, BitAutoData] + public async Task CreateAsync_FlagEnabled_EventSentToBroadcastService(EventMessage eventMessage) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true); + + await Subject.CreateAsync(eventMessage); + + await _broadcastEventWriteService.Received(1).CreateAsync(eventMessage); + await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CreateManyAsync_FlagDisabled_EventsSentToStorageService(IEnumerable eventMessages) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false); + + await Subject.CreateManyAsync(eventMessages); + + await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + await _storageEventWriteService.Received(1).CreateManyAsync(eventMessages); + } + + [Theory, BitAutoData] + public async Task CreateManyAsync_FlagEnabled_EventsSentToBroadcastService(IEnumerable eventMessages) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true); + + await Subject.CreateManyAsync(eventMessages); + + await _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages); + await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + } +} diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs new file mode 100644 index 0000000000..10f39665d5 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -0,0 +1,41 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Services; +using Xunit; + +namespace Bit.Core.Test.Services; + +public class IntegrationHandlerTests +{ + + [Fact] + public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage() + { + var sut = new TestIntegrationHandler(); + var expected = new IntegrationMessage() + { + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + IntegrationType = IntegrationType.Webhook, + RenderedTemplate = "Template", + DelayUntilDate = null, + RetryCount = 0 + }; + + var result = await sut.HandleAsync(expected.ToJson()); + var typedResult = Assert.IsType>(result.Message); + + Assert.Equal(expected.Configuration, typedResult.Configuration); + Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate); + Assert.Equal(expected.IntegrationType, typedResult.IntegrationType); + } + + private class TestIntegrationHandler : IntegrationHandlerBase + { + public override Task HandleAsync( + IntegrationMessage message) + { + var result = new IntegrationHandlerResult(success: true, message: message); + return Task.FromResult(result); + } + } +} diff --git a/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs new file mode 100644 index 0000000000..98cf974df8 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs @@ -0,0 +1,30 @@ +using Bit.Core.Enums; +using Xunit; + +namespace Bit.Core.Test.Services; + +public class IntegrationTypeTests +{ + [Fact] + public void ToRoutingKey_Slack_Succeeds() + { + Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey()); + } + [Fact] + public void ToRoutingKey_Webhook_Succeeds() + { + Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_CloudBillingSync_ThrowsException() + { + Assert.Throws(() => IntegrationType.CloudBillingSync.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_Scim_ThrowsException() + { + Assert.Throws(() => IntegrationType.Scim.ToRoutingKey()); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index c138cfac2e..18f1f79900 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -9,7 +9,6 @@ using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -177,47 +176,6 @@ public class OrganizationServiceTests referenceEvent.Users == expectedNewUsersCount)); } - [Theory, BitAutoData] - public async Task SignupClientAsync_Succeeds( - OrganizationSignup signup, - SutProvider sutProvider) - { - signup.Plan = PlanType.TeamsMonthly; - - 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 && - org.Plan == plan.Name && - org.PlanType == plan.Type && - org.UsePolicies == plan.HasPolicies && - org.PublicKey == signup.PublicKey && - org.PrivateKey == signup.PrivateKey && - org.UseSecretsManager == false)); - - await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Is(orgApiKey => - orgApiKey.OrganizationId == organization.Id)); - - await sutProvider.GetDependency().Received(1) - .UpsertOrganizationAbilityAsync(organization); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); - - await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Is(c => c.Name == signup.CollectionName && c.OrganizationId == organization.Id), null, null); - - await sutProvider.GetDependency().Received(1).RaiseEventAsync(Arg.Is( - re => - re.Type == ReferenceEventType.Signup && - re.PlanType == plan.Type)); - } - [Theory] [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData] diff --git a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs deleted file mode 100644 index 798ba219eb..0000000000 --- a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.Helpers; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class SlackEventHandlerTests -{ - private readonly IOrganizationIntegrationConfigurationRepository _repository = Substitute.For(); - private readonly ISlackService _slackService = Substitute.For(); - private readonly string _channelId = "C12345"; - private readonly string _channelId2 = "C67890"; - private readonly string _token = "xoxb-test-token"; - private readonly string _token2 = "xoxb-another-test-token"; - - private SutProvider GetSutProvider( - List integrationConfigurations) - { - _repository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Slack, Arg.Any()) - .Returns(integrationConfigurations); - - return new SutProvider() - .SetDependency(_repository) - .SetDependency(_slackService) - .Create(); - } - - private List NoConfigurations() - { - return []; - } - - private List OneConfiguration() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { token = _token }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config]; - } - - private List TwoConfigurations() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { token = _token }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - var config2 = Substitute.For(); - config2.Configuration = JsonSerializer.Serialize(new { token = _token2 }); - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId2 }); - config2.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config, config2]; - } - - private List WrongConfiguration() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OneConfiguration_SendsEventViaSlackService(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) - ); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_TwoConfigurations_SendsMultipleEvents(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) - ); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token2)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId2)) - ); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_WrongConfiguration_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(WrongConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_OneConfiguration_SendsEventsViaSlackService(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - var received = sutProvider.GetDependency().ReceivedCalls(); - using var calls = received.GetEnumerator(); - - Assert.Equal(eventMessages.Count, received.Count()); - - foreach (var eventMessage in eventMessages) - { - Assert.True(calls.MoveNext()); - var arguments = calls.Current.GetArguments(); - Assert.Equal(_token, arguments[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments[1] as string); - Assert.Equal(_channelId, arguments[2] as string); - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_TwoConfigurations_SendsMultipleEvents(List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - var received = sutProvider.GetDependency().ReceivedCalls(); - using var calls = received.GetEnumerator(); - - Assert.Equal(eventMessages.Count * 2, received.Count()); - - foreach (var eventMessage in eventMessages) - { - Assert.True(calls.MoveNext()); - var arguments = calls.Current.GetArguments(); - Assert.Equal(_token, arguments[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments[1] as string); - Assert.Equal(_channelId, arguments[2] as string); - - Assert.True(calls.MoveNext()); - var arguments2 = calls.Current.GetArguments(); - Assert.Equal(_token2, arguments2[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments2[1] as string); - Assert.Equal(_channelId2, arguments2[2] as string); - } - } -} diff --git a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs new file mode 100644 index 0000000000..9f66e2eb2f --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs @@ -0,0 +1,42 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class SlackIntegrationHandlerTests +{ + private readonly ISlackService _slackService = Substitute.For(); + private readonly string _channelId = "C12345"; + private readonly string _token = "xoxb-test-token"; + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_slackService) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_token)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) + ); + } +} diff --git a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs deleted file mode 100644 index c426f8eaad..0000000000 --- a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.Helpers; -using Bit.Test.Common.MockedHttpClient; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class WebhookEventHandlerTests -{ - private readonly MockedHttpMessageHandler _handler; - private readonly HttpClient _httpClient; - - private const string _template = - """ - { - "Date": "#Date#", - "Type": "#Type#", - "UserId": "#UserId#" - } - """; - private const string _webhookUrl = "http://localhost/test/event"; - private const string _webhookUrl2 = "http://localhost/another/event"; - - public WebhookEventHandlerTests() - { - _handler = new MockedHttpMessageHandler(); - _handler.Fallback - .WithStatusCode(HttpStatusCode.OK) - .WithContent(new StringContent("testtest")); - _httpClient = _handler.ToHttpClient(); - } - - private SutProvider GetSutProvider( - List configurations) - { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient); - - var repository = Substitute.For(); - repository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Webhook, Arg.Any()).Returns(configurations); - - return new SutProvider() - .SetDependency(repository) - .SetDependency(clientFactory) - .Create(); - } - - private static List NoConfigurations() - { - return []; - } - - private static List OneConfiguration() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl }); - config.Template = _template; - - return [config]; - } - - private static List TwoConfigurations() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl }); - config.Template = _template; - var config2 = Substitute.For(); - config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl2 }); - config2.Template = _template; - - return [config, config2]; - } - - private static List WrongConfiguration() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { error = string.Empty }); - config.Template = _template; - - return [config]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OneConfiguration_PostsEventToUrl(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Single(_handler.CapturedRequests); - var request = _handler.CapturedRequests[0]; - Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); - var expected = MockEvent.From(eventMessage); - - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(expected, returned); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_WrongConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(WrongConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_NoConfigurations_DoesNothing(List eventMessages) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_OneConfiguration_PostsEventsToUrl(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Equal(eventMessages.Count, _handler.CapturedRequests.Count); - var index = 0; - foreach (var request in _handler.CapturedRequests) - { - Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); - var expected = MockEvent.From(eventMessages[index]); - - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(expected, returned); - index++; - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_TwoConfigurations_PostsEventsToMultipleUrls(List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - using var capturedRequests = _handler.CapturedRequests.GetEnumerator(); - Assert.Equal(eventMessages.Count * 2, _handler.CapturedRequests.Count); - - foreach (var eventMessage in eventMessages) - { - var expected = MockEvent.From(eventMessage); - - Assert.True(capturedRequests.MoveNext()); - var request = capturedRequests.Current; - Assert.NotNull(request); - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - var returned = await request.Content.ReadFromJsonAsync(); - AssertHelper.AssertPropertyEqual(expected, returned); - - Assert.True(capturedRequests.MoveNext()); - request = capturedRequests.Current; - Assert.NotNull(request); - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl2, request.RequestUri.ToString()); - returned = await request.Content.ReadFromJsonAsync(); - AssertHelper.AssertPropertyEqual(expected, returned); - } - } -} - -public class MockEvent(string date, string type, string userId) -{ - public string Date { get; set; } = date; - public string Type { get; set; } = type; - public string UserId { get; set; } = userId; - - public static MockEvent From(EventMessage eventMessage) - { - return new MockEvent( - eventMessage.Date.ToString(), - eventMessage.Type.ToString(), - eventMessage.UserId.ToString() - ); - } -} diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs new file mode 100644 index 0000000000..79c7569ea3 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -0,0 +1,139 @@ +using System.Net; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Bit.Test.Common.MockedHttpClient; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class WebhookIntegrationHandlerTests +{ + private readonly MockedHttpMessageHandler _handler; + private readonly HttpClient _httpClient; + private const string _webhookUrl = "http://localhost/test/event"; + + public WebhookIntegrationHandlerTests() + { + _handler = new MockedHttpMessageHandler(); + _handler.Fallback + .WithStatusCode(HttpStatusCode.OK) + .WithContent(new StringContent("testtest")); + _httpClient = _handler.ToHttpClient(); + } + + private SutProvider GetSutProvider() + { + var clientFactory = Substitute.For(); + clientFactory.CreateClient(WebhookIntegrationHandler.HttpClientName).Returns(_httpClient); + + return new SutProvider() + .SetDependency(clientFactory) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = await request.Content.ReadAsStringAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); + } + + [Theory, BitAutoData] + public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Retry-After", "60") + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.True(result.DelayUntilDate.HasValue); + Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + } + + [Theory, BitAutoData] + public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123 + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.True(result.DelayUntilDate.HasValue); + Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + } + + [Theory, BitAutoData] + public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.InternalServerError) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.False(result.DelayUntilDate.HasValue); + } + + [Theory, BitAutoData] + public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TemporaryRedirect) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + Assert.Null(result.DelayUntilDate); + } +} diff --git a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs index abb49c25c6..1bc673426d 100644 --- a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs +++ b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Xunit; namespace Bit.Core.Test.AdminConsole.Shared; @@ -22,13 +22,11 @@ public class IValidatorTests { if (string.IsNullOrWhiteSpace(value.Name)) { - return Task.FromResult>(new Invalid - { - Errors = [new InvalidRequestError(value)] - }); + return Task.FromResult>( + new Invalid(new InvalidRequestError(value))); } - return Task.FromResult>(new Valid { Value = value }); + return Task.FromResult>(new Valid(value)); } } @@ -41,7 +39,7 @@ public class IValidatorTests Assert.IsType>(result); var invalidResult = result as Invalid; - Assert.Equal(InvalidRequestError.Code, invalidResult.Errors.First().Message); + Assert.Equal(InvalidRequestError.Code, invalidResult!.Error.Message); } [Fact] diff --git a/test/Core.Test/Models/Commands/CommandResultTests.cs b/test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs similarity index 92% rename from test/Core.Test/Models/Commands/CommandResultTests.cs rename to test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs index c500fef4f5..67ff59c95b 100644 --- a/test/Core.Test/Models/Commands/CommandResultTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs @@ -1,9 +1,9 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.Models.Commands; +namespace Bit.Core.Test.AdminConsole.Utilities.Commands; public class CommandResultTests { diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index 9ab3b592cb..155eceeb25 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Utilities; +#nullable enable + +using Bit.Core.AdminConsole.Utilities; using Bit.Core.Models.Data; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -76,17 +78,62 @@ public class IntegrationTemplateProcessorTests var expectedEmpty = ""; Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage)); - Assert.Null(IntegrationTemplateProcessor.ReplaceTokens(null, eventMessage)); } - [Fact] - public void ReplaceTokens_DataObjectIsNull_ReturnsOriginalString() + [Theory] + [InlineData("User name is #UserName#")] + [InlineData("Email: #UserEmail#")] + public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template) { - var template = "Event #Type#, User (id: #UserId#)."; - var expected = "Event #Type#, User (id: #UserId#)."; + var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); + Assert.True(result); + } - var result = IntegrationTemplateProcessor.ReplaceTokens(template, null); + [Theory] + [InlineData("#UserId#")] // This is on the base class, not fetched, so should be false + [InlineData("No User Tokens")] + [InlineData("")] + public void TemplateRequiresUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); + Assert.False(result); + } - Assert.Equal(expected, result); + [Theory] + [InlineData("Acting user is #ActingUserName#")] + [InlineData("Acting user's email is #ActingUserEmail#")] + public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); + Assert.True(result); + } + + [Theory] + [InlineData("No ActiveUser tokens")] + [InlineData("#ActiveUserId#")] // This is on the base class, not fetched, so should be false + [InlineData("")] + public void TemplateRequiresActingUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); + Assert.False(result); + } + + [Theory] + [InlineData("Organization: #OrganizationName#")] + [InlineData("Welcome to #OrganizationName#")] + public void TemplateRequiresOrganization_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresOrganization(template); + Assert.True(result); + } + + [Theory] + [InlineData("No organization tokens")] + [InlineData("#OrganizationId#")] // This is on the base class, not fetched, so should be false + [InlineData("")] + public void TemplateRequiresOrganization_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresOrganization(template); + Assert.False(result); } } diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index da2d4a282a..ff09e1f141 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -44,9 +44,6 @@ public abstract class BaseTokenProviderTests protected virtual void SetupUserService(IUserService userService, User user) { - userService - .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) - .Returns(true); userService .CanAccessPremium(user) .Returns(true); @@ -85,8 +82,6 @@ public abstract class BaseTokenProviderTests var userManager = SubstituteUserManager(); MockDatabase(user, metaData); - AdditionalSetup(sutProvider, user); - var response = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(userManager, user); Assert.Equal(expectedResponse, response); } diff --git a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs index 85c687119b..5715403974 100644 --- a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs @@ -83,6 +83,7 @@ public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) { // Arrange + AdditionalSetup(sutProvider, user); user.Premium = true; user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1); @@ -100,6 +101,8 @@ public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) { // Arrange + AdditionalSetup(sutProvider, user); + user.Premium = false; sutProvider.GetDependency() diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs deleted file mode 100644 index 56533bab7a..0000000000 --- a/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using AutoFixture.Xunit2; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Entities; -using Bit.Core.Tokens; -using Bit.Test.Common.AutoFixture.Attributes; -using Xunit; - -namespace Bit.Core.Test.Auth.Models.Business.Tokenables; - -public class HCaptchaTokenableTests -{ - [Fact] - public void CanHandleNullUser() - { - var token = new HCaptchaTokenable(null); - - Assert.Equal(default, token.Id); - Assert.Equal(default, token.Email); - } - - [Fact] - public void TokenWithNullUserIsInvalid() - { - var token = new HCaptchaTokenable(null) - { - ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1) - }; - - Assert.False(token.Valid); - } - - [Theory, BitAutoData] - public void TokenValidityCheckNullUserIdIsInvalid(User user) - { - var token = new HCaptchaTokenable(user) - { - ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1) - }; - - Assert.False(token.TokenIsValid(null)); - } - - [Theory, AutoData] - public void CanUpdateExpirationToNonStandard(User user) - { - var token = new HCaptchaTokenable(user) - { - ExpirationDate = DateTime.MinValue - }; - - Assert.Equal(DateTime.MinValue, token.ExpirationDate, TimeSpan.FromMilliseconds(10)); - } - - [Theory, AutoData] - public void SetsDataFromUser(User user) - { - var token = new HCaptchaTokenable(user); - - Assert.Equal(user.Id, token.Id); - Assert.Equal(user.Email, token.Email); - } - - [Theory, AutoData] - public void SerializationSetsCorrectDateTime(User user) - { - var expectedDateTime = DateTime.UtcNow.AddHours(-5); - var token = new HCaptchaTokenable(user) - { - ExpirationDate = expectedDateTime - }; - - var result = Tokenable.FromToken(token.ToToken()); - - Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10)); - } - - [Theory, AutoData] - public void IsInvalidIfIdentifierIsWrong(User user) - { - var token = new HCaptchaTokenable(user) - { - Identifier = "not correct" - }; - - Assert.False(token.Valid); - } -} diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs index 4d95a1c196..ab393203ab 100644 --- a/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs +++ b/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs @@ -67,7 +67,7 @@ public class SsoTokenableTests ExpirationDate = expectedDateTime }; - var result = Tokenable.FromToken(token.ToToken()); + var result = Tokenable.FromToken(token.ToToken()); Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10)); } diff --git a/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs b/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs index 6c2352ca00..006515aafd 100644 --- a/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs +++ b/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs @@ -1,11 +1,17 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Services; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -17,27 +23,21 @@ namespace Bit.Core.Test.Auth.Services; public class EmergencyAccessServiceTests { [Theory, BitAutoData] - public async Task SaveAsync_PremiumCannotUpdate( - SutProvider sutProvider, User savingUser) + public async Task InviteAsync_UserWithOutPremium_ThrowsBadRequest( + SutProvider sutProvider, User invitingUser, string email, int waitTime) { - savingUser.Premium = false; - var emergencyAccess = new EmergencyAccess - { - Type = EmergencyAccessType.Takeover, - GrantorId = savingUser.Id, - }; - - sutProvider.GetDependency().GetUserByIdAsync(savingUser.Id).Returns(savingUser); + sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(false); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime)); Assert.Contains("Not a premium user.", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().CreateAsync(default); } [Theory, BitAutoData] - public async Task InviteAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task InviteAsync_UserWithKeyConnector_ThrowsBadRequest( SutProvider sutProvider, User invitingUser, string email, int waitTime) { invitingUser.UsesKeyConnector = true; @@ -47,11 +47,461 @@ public class EmergencyAccessServiceTests () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime)); Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().CreateAsync(default); + } + + [Theory] + [BitAutoData(EmergencyAccessType.Takeover)] + [BitAutoData(EmergencyAccessType.View)] + public async Task InviteAsync_ReturnsEmergencyAccessObject( + EmergencyAccessType accessType, SutProvider sutProvider, User invitingUser, string email, int waitTime) + { + sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(true); + + var result = await sutProvider.Sut.InviteAsync(invitingUser, email, accessType, waitTime); + + Assert.NotNull(result); + Assert.Equal(accessType, result.Type); + Assert.Equal(invitingUser.Id, result.GrantorId); + Assert.Equal(email, result.Email); + Assert.Equal(EmergencyAccessStatusType.Invited, result.Status); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + sutProvider.GetDependency>() + .Received(1) + .Protect(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessInviteEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task GetAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, User user) + { + EmergencyAccessDetails emergencyAccess = null; + sutProvider.GetDependency() + .GetDetailsByIdGrantorIdAsync(Arg.Any(), Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetAsync(new Guid(), user.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + EmergencyAccess emergencyAccess = null; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_InvitingUserIdNotGrantorUserId_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Invited, + GrantorId = Guid.NewGuid(), + Type = EmergencyAccessType.Takeover, + }; ; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task ResendInviteAsync_EmergencyAccessStatusInvalid_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = statusType, + GrantorId = invitingUser.Id, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_SendsInviteAsync( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Invited, + GrantorId = invitingUser.Id, + Type = EmergencyAccessType.Takeover, + }; ; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + await sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId); + sutProvider.GetDependency>() + .Received(1) + .Protect(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUser.Name, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, User acceptingUser, string token) + { + EmergencyAccess emergencyAccess = null; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(new Guid(), acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_CannotUnprotectToken_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invalid token.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_TokenDataInvalid_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + EmergencyAccess wrongEmergencyAccess, + string token) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(wrongEmergencyAccess, 1); + return true; + }); + + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invalid token.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_AcceptedStatus_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invitation already accepted. You will receive an email when the grantor confirms you as an emergency access contact.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_NotInvitedStatus_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Confirmed; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invitation already accepted.", exception.Message); + } + + [Theory(Skip = "Code not reachable, Tokenable checks email match in IsValid()"), BitAutoData] + public async Task AcceptUserAsync_EmergencyAccessEmailDoesNotMatch_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("User email does not match invite.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_ReplaceEmergencyAccess_SendsEmail_Success( + SutProvider sutProvider, + User acceptingUser, + User invitingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetUserByIdAsync(Arg.Any()) + .Returns(invitingUser); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency()); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Accepted)); + + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessAcceptedEmailAsync(acceptingUser.Email, invitingUser.Email); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessGrantorIdNotEqual_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GrantorId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessGranteeIdNotEqual_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GranteeId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessIsDeleted_Success( + SutProvider sutProvider, + User user, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GranteeId = user.Id; + emergencyAccess.GrantorId = user.Id; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + await sutProvider.Sut.DeleteAsync(emergencyAccess.Id, user.Id); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessStatusIsNotAccepted_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessGrantorIdNotEqualToConfirmingUserId_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( SutProvider sutProvider, User confirmingUser, string key) { confirmingUser.UsesKeyConnector = true; @@ -62,8 +512,13 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(confirmingUser.Id).Returns(confirmingUser); - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(confirmingUser.Id) + .Returns(confirmingUser); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ConfirmUserAsync(new Guid(), key, confirmingUser.Id)); @@ -73,29 +528,210 @@ public class EmergencyAccessServiceTests } [Theory, BitAutoData] - public async Task SaveAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task ConfirmUserAsync_ConfirmsAndReplacesEmergencyAccess_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser, + User granteeUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantorUser.Id) + .Returns(grantorUser); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GranteeId.Value) + .Returns(granteeUser); + + await sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Confirmed)); + + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessConfirmedEmailAsync(grantorUser.Name, granteeUser.Email); + } + + [Theory, BitAutoData] + public async Task SaveAsync_PremiumCannotUpdate_ThrowsBadRequest( SutProvider sutProvider, User savingUser) { - savingUser.UsesKeyConnector = true; var emergencyAccess = new EmergencyAccess { Type = EmergencyAccessType.Takeover, GrantorId = savingUser.Id, }; - var userService = sutProvider.GetDependency(); - userService.GetUserByIdAsync(savingUser.Id).Returns(savingUser); - userService.CanAccessPremium(savingUser).Returns(true); + sutProvider.GetDependency() + .CanAccessPremium(savingUser) + .Returns(false); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + Assert.Contains("Not a premium user.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task SaveAsync_EmergencyAccessGrantorIdNotEqualToSavingUserId_ThrowsBadRequest( + SutProvider sutProvider, User savingUser) + { + savingUser.Premium = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = new Guid(), + }; + + sutProvider.GetDependency() + .GetUserByIdAsync(savingUser.Id) + .Returns(savingUser); + sutProvider.GetDependency() + .CanAccessPremium(savingUser) + .Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task SaveAsync_GrantorUserWithKeyConnectorCannotTakeover_ThrowsBadRequest( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser)); + Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } [Theory, BitAutoData] - public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task SaveAsync_GrantorUserWithKeyConnectorCanView_SavesEmergencyAccess( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.View, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ValidRequest_SavesEmergencyAccess( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = false; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessNull_ThrowBadRequest( + SutProvider sutProvider, User initiatingUser) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessGranteeIdNotEqual_ThrowBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User initiatingUser) + { + emergencyAccess.GranteeId = new Guid(); + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessStatusIsNotConfirmed_ThrowBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User initiatingUser) + { + emergencyAccess.GranteeId = initiatingUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( SutProvider sutProvider, User initiatingUser, User grantor) { grantor.UsesKeyConnector = true; @@ -107,40 +743,711 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); } [Theory, BitAutoData] - public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover( - SutProvider sutProvider, User requestingUser, User grantor) + public async Task InitiateAsync_UserWithKeyConnectorCanView_Success( + SutProvider sutProvider, User initiatingUser, User grantor) + { + grantor.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Confirmed, + GranteeId = initiatingUser.Id, + GrantorId = grantor.Id, + Type = EmergencyAccessType.View, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated)); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_RequestIsCorrect_Success( + SutProvider sutProvider, User initiatingUser, User grantor) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Confirmed, + GranteeId = initiatingUser.Id, + GrantorId = grantor.Id, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated)); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_EmergencyAccessNull_ThrowsBadrequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(new Guid(), null)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_EmergencyAccessGrantorIdNotEquatToApproving_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task ApproveAsync_EmergencyAccessStatusNotRecoveryInitiated_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser, + User granteeUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(granteeUser); + + await sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryApproved)); + } + + [Theory, BitAutoData] + public async Task RejectAsync_EmergencyAccessIdNull_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RejectAsync_EmergencyAccessGrantorIdNotEqualToRequestUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + public async Task RejectAsync_EmergencyAccessStatusNotValid_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task RejectAsync_Success( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser, + User GranteeUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(GranteeUser); + + await sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Confirmed)); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_RequestNotValidEmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(default, default)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task GetPoliciesAsync_RequestNotValidStatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_RequestNotValidType_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task GetPoliciesAsync_OrganizationUserTypeNotOwner_ReturnsNull( + OrganizationUserType userType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser grantorOrganizationUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + grantorOrganizationUser.UserId = grantorUser.Id; + grantorOrganizationUser.Type = userType; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([grantorOrganizationUser]); + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_OrganizationUserEmpty_ReturnsNull( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([]); + + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_ReturnsNotNull( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser grantorOrganizationUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + grantorOrganizationUser.UserId = grantorUser.Id; + grantorOrganizationUser.Type = OrganizationUserType.Owner; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([grantorOrganizationUser]); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(grantorUser.Id) + .Returns([]); + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.NotNull(result); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task TakeoverAsync_RequestNotValid_StatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_TypeIsView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( + SutProvider sutProvider, + User granteeUser, + User grantor) { grantor.UsesKeyConnector = true; var emergencyAccess = new EmergencyAccess { GrantorId = grantor.Id, - GranteeId = requestingUser.Id, + GranteeId = granteeUser.Id, Status = EmergencyAccessStatusType.RecoveryApproved, Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.TakeoverAsync(new Guid(), requestingUser)); + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message); } [Theory, BitAutoData] - public async Task PasswordAsync_Disables_2FA_Providers_On_The_Grantor( + public async Task TakeoverAsync_Success_ReturnsEmergencyAccessAndGrantorUser( + SutProvider sutProvider, + User granteeUser, + User grantor) + { + grantor.UsesKeyConnector = false; + var emergencyAccess = new EmergencyAccess + { + GrantorId = grantor.Id, + GranteeId = granteeUser.Id, + Status = EmergencyAccessStatusType.RecoveryApproved, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + var result = await sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser); + + Assert.Equal(result.Item1, emergencyAccess); + Assert.Equal(result.Item2, grantor); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(default, default, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task PasswordAsync_RequestNotValid_StatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_TypeIsView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_NonOrgUser_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task PasswordAsync_OrgUser_NotOrganizationOwner_RemovedFromOrganization_Success( + OrganizationUserType userType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser organizationUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + organizationUser.UserId = grantorUser.Id; + organizationUser.Type = userType; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([organizationUser]); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + await sutProvider.GetDependency() + .Received(1) + .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_OrgUser_IsOrganizationOwner_NotRemovedFromOrganization_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser organizationUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + organizationUser.UserId = grantorUser.Id; + organizationUser.Type = OrganizationUserType.Owner; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([organizationUser]); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + await sutProvider.GetDependency() + .Received(0) + .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_Disables_NewDeviceVerification_And_TwoFactorProviders_On_The_Grantor( SutProvider sutProvider, User requestingUser, User grantor) { grantor.UsesKeyConnector = true; @@ -160,12 +1467,49 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); await sutProvider.Sut.PasswordAsync(Guid.NewGuid(), requestingUser, "blablahash", "blablakey"); Assert.Empty(grantor.GetTwoFactorProviders()); + Assert.False(grantor.VerifyDevices); await sutProvider.GetDependency().Received().ReplaceAsync(grantor); } + + [Theory, BitAutoData] + public async Task ViewAsync_EmergencyAccessTypeNotView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ViewAsync(emergencyAccess.Id, granteeUser)); + } + + [Theory, BitAutoData] + public async Task GetAttachmentDownloadAsync_EmergencyAccessTypeNotView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetAttachmentDownloadAsync(emergencyAccess.Id, default, default, granteeUser)); + } } diff --git a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs index 02ecb4ecd7..ffc56e89b2 100644 --- a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs @@ -226,6 +226,11 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == default)); + + // Even if user doesn't have reference data, we should send them welcome email + await sutProvider.GetDependency() + .Received(1) + .SendWelcomeEmailAsync(user); } Assert.True(result.Succeeded); diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs index 8011c52ead..adeac45d06 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs @@ -5,6 +5,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -53,6 +54,39 @@ public class TwoFactorIsEnabledQueryTests } } + [Theory, BitAutoData] + public async Task TwoFactorIsEnabledQuery_DatabaseReturnsEmpty_ResultEmpty( + SutProvider sutProvider, + List usersWithCalculatedPremium) + { + // Arrange + var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList(); + + sutProvider.GetDependency() + .GetManyWithCalculatedPremiumAsync(Arg.Any>()) + .Returns([]); + + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds); + + // Assert + Assert.Empty(result); + } + + [Theory] + [BitAutoData((IEnumerable)null)] + [BitAutoData([])] + public async Task TwoFactorIsEnabledQuery_UserIdsNullorEmpty_ResultEmpty( + IEnumerable userIds, + SutProvider sutProvider) + { + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds); + + // Assert + Assert.Empty(result); + } + [Theory] [BitAutoData] public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsAllTwoFactorDisabled( @@ -122,8 +156,11 @@ public class TwoFactorIsEnabledQueryTests } [Theory] - [BitAutoData] - public async Task TwoFactorIsEnabledQuery_WithNullTwoFactorProviders_ReturnsAllTwoFactorDisabled( + [BitAutoData("")] + [BitAutoData("{}")] + [BitAutoData((string)null)] + public async Task TwoFactorIsEnabledQuery_WithNullOrEmptyTwoFactorProviders_ReturnsAllTwoFactorDisabled( + string twoFactorProviders, SutProvider sutProvider, List usersWithCalculatedPremium) { @@ -132,7 +169,7 @@ public class TwoFactorIsEnabledQueryTests foreach (var user in usersWithCalculatedPremium) { - user.TwoFactorProviders = null; // No two-factor providers configured + user.TwoFactorProviders = twoFactorProviders; // No two-factor providers configured } sutProvider.GetDependency() @@ -176,6 +213,24 @@ public class TwoFactorIsEnabledQueryTests .GetManyWithCalculatedPremiumAsync(default); } + [Theory] + [BitAutoData] + public async Task TwoFactorIsEnabledQuery_UserIdNull_ReturnsFalse( + SutProvider sutProvider) + { + // Arrange + var user = new TestTwoFactorProviderUser + { + Id = null + }; + + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); + + // Assert + Assert.False(result); + } + [Theory] [BitAutoData(TwoFactorProviderType.Authenticator)] [BitAutoData(TwoFactorProviderType.Email)] @@ -193,10 +248,8 @@ public class TwoFactorIsEnabledQueryTests { freeProviderType, new TwoFactorProvider { Enabled = true } } }; - user.Premium = false; user.SetTwoFactorProviders(twoFactorProviders); - // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); @@ -205,7 +258,7 @@ public class TwoFactorIsEnabledQueryTests await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .GetCalculatedPremiumAsync(default); } [Theory] @@ -230,7 +283,7 @@ public class TwoFactorIsEnabledQueryTests await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .GetCalculatedPremiumAsync(default); } [Theory] @@ -252,14 +305,18 @@ public class TwoFactorIsEnabledQueryTests user.SetTwoFactorProviders(twoFactorProviders); sutProvider.GetDependency() - .GetManyWithCalculatedPremiumAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new List { user }); + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); // Assert Assert.False(result); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -268,7 +325,7 @@ public class TwoFactorIsEnabledQueryTests public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithUserPremium_ReturnsTrue( TwoFactorProviderType premiumProviderType, SutProvider sutProvider, - User user) + UserWithCalculatedPremium user) { // Arrange var twoFactorProviders = new Dictionary @@ -276,9 +333,14 @@ public class TwoFactorIsEnabledQueryTests { premiumProviderType, new TwoFactorProvider { Enabled = true } } }; - user.Premium = true; + user.Premium = false; + user.HasPremiumAccess = true; user.SetTwoFactorProviders(twoFactorProviders); + sutProvider.GetDependency() + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); + // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); @@ -286,8 +348,8 @@ public class TwoFactorIsEnabledQueryTests Assert.True(result); await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -309,14 +371,18 @@ public class TwoFactorIsEnabledQueryTests user.SetTwoFactorProviders(twoFactorProviders); sutProvider.GetDependency() - .GetManyWithCalculatedPremiumAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new List { user }); + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); // Assert Assert.True(result); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -333,5 +399,29 @@ public class TwoFactorIsEnabledQueryTests // Assert Assert.False(result); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetCalculatedPremiumAsync(default); + } + + private class TestTwoFactorProviderUser : ITwoFactorProvidersUser + { + public Guid? Id { get; set; } + public string TwoFactorProviders { get; set; } + public bool Premium { get; set; } + public Dictionary GetTwoFactorProviders() + { + return JsonHelpers.LegacyDeserialize>(TwoFactorProviders); + } + + public Guid? GetUserId() + { + return Id; + } + + public bool GetPremium() + { + return Premium; + } } } diff --git a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs index c9278e4488..06a408c5a8 100644 --- a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs +++ b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs @@ -4,7 +4,6 @@ using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; @@ -22,8 +21,7 @@ public class PaymentHistoryServiceTests var stripeAdapter = Substitute.For(); stripeAdapter.InvoiceListAsync(Arg.Any()).Returns(invoices); var transactionRepository = Substitute.For(); - var logger = Substitute.For>(); - var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository, logger); + var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository); // Act var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber); @@ -40,8 +38,7 @@ public class PaymentHistoryServiceTests // Arrange var paymentHistoryService = new PaymentHistoryService( Substitute.For(), - Substitute.For(), - Substitute.For>()); + Substitute.For()); // Act var result = await paymentHistoryService.GetInvoiceHistoryAsync(null); @@ -59,8 +56,7 @@ public class PaymentHistoryServiceTests var transactionRepository = Substitute.For(); transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any(), Arg.Any()).Returns(transactions); var stripeAdapter = Substitute.For(); - var logger = Substitute.For>(); - var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository, logger); + var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository); // Act var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber); @@ -77,8 +73,7 @@ public class PaymentHistoryServiceTests // Arrange var paymentHistoryService = new PaymentHistoryService( Substitute.For(), - Substitute.For(), - Substitute.For>()); + Substitute.For()); // Act var result = await paymentHistoryService.GetTransactionHistoryAsync(null); diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 9e4be78787..3fb134fda8 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -3,13 +3,11 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Implementations; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Test.Billing.Stubs; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Braintree; @@ -194,7 +192,7 @@ public class SubscriberServiceTests await stripeAdapter .DidNotReceiveWithAnyArgs() - .SubscriptionCancelAsync(Arg.Any(), Arg.Any()); ; + .SubscriptionCancelAsync(Arg.Any(), Arg.Any()); } #endregion @@ -1028,7 +1026,7 @@ public class SubscriberServiceTests stripeAdapter .PaymentMethodListAutoPagingAsync(Arg.Any()) - .Returns(GetPaymentMethodsAsync(new List())); + .Returns(GetPaymentMethodsAsync(new List())); await sutProvider.Sut.RemovePaymentSource(organization); @@ -1060,7 +1058,7 @@ public class SubscriberServiceTests stripeAdapter .PaymentMethodListAutoPagingAsync(Arg.Any()) - .Returns(GetPaymentMethodsAsync(new List + .Returns(GetPaymentMethodsAsync(new List { new () { @@ -1085,8 +1083,8 @@ public class SubscriberServiceTests .PaymentMethodDetachAsync(cardId); } - private static async IAsyncEnumerable GetPaymentMethodsAsync( - IEnumerable paymentMethods) + private static async IAsyncEnumerable GetPaymentMethodsAsync( + IEnumerable paymentMethods) { foreach (var paymentMethod in paymentMethods) { @@ -1597,14 +1595,22 @@ public class SubscriberServiceTests City = "Example Town", State = "NY" }, - TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } + TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] }, + Subscriptions = new StripeList + { + Data = [ + new Subscription + { + Id = provider.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } }); var subscription = new Subscription { Items = new StripeList() }; sutProvider.GetDependency().SubscriptionGetAsync(Arg.Any()) .Returns(subscription); - sutProvider.GetDependency().CreateAsync(Arg.Any()) - .Returns(new FakeAutomaticTaxStrategy(true)); await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation); @@ -1622,6 +1628,98 @@ public class SubscriberServiceTests await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is( options => options.Type == "us_ein" && options.Value == taxInformation.TaxId)); + + await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + } + + [Theory, BitAutoData] + public async Task UpdateTaxInformation_NonUser_ReverseCharge_MakesCorrectInvocations( + Provider provider, + SutProvider sutProvider) + { + var stripeAdapter = sutProvider.GetDependency(); + + var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } }; + + stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is( + options => options.Expand.Contains("tax_ids"))).Returns(customer); + + var taxInformation = new TaxInformation( + "CA", + "12345", + "123456789", + "us_ein", + "123 Example St.", + null, + "Example Town", + "NY"); + + sutProvider.GetDependency() + .CustomerUpdateAsync( + Arg.Is(p => p == provider.GatewayCustomerId), + Arg.Is(options => + options.Address.Country == "CA" && + options.Address.PostalCode == "12345" && + options.Address.Line1 == "123 Example St." && + options.Address.Line2 == null && + options.Address.City == "Example Town" && + options.Address.State == "NY")) + .Returns(new Customer + { + Id = provider.GatewayCustomerId, + Address = new Address + { + Country = "CA", + PostalCode = "12345", + Line1 = "123 Example St.", + Line2 = null, + City = "Example Town", + State = "NY" + }, + TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] }, + Subscriptions = new StripeList + { + Data = [ + new Subscription + { + Id = provider.GatewaySubscriptionId, + CustomerId = provider.GatewayCustomerId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }); + + var subscription = new Subscription { Items = new StripeList() }; + sutProvider.GetDependency().SubscriptionGetAsync(Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation); + + await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is( + options => + options.Address.Country == taxInformation.Country && + options.Address.PostalCode == taxInformation.PostalCode && + options.Address.Line1 == taxInformation.Line1 && + options.Address.Line2 == taxInformation.Line2 && + options.Address.City == taxInformation.City && + options.Address.State == taxInformation.State)); + + await stripeAdapter.Received(1).TaxIdDeleteAsync(provider.GatewayCustomerId, "tax_id_1"); + + await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is( + options => options.Type == "us_ein" && + options.Value == taxInformation.TaxId)); + + await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, + Arg.Is(options => options.TaxExempt == StripeConstants.TaxExempt.Reverse)); + + await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); } #endregion diff --git a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs new file mode 100644 index 0000000000..c35dc275e6 --- /dev/null +++ b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs @@ -0,0 +1,346 @@ +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Tax.Commands; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Stripe; +using Xunit; +using static Bit.Core.Billing.Tax.Commands.OrganizationTrialParameters; + +namespace Bit.Core.Test.Billing.Tax.Commands; + +public class PreviewTaxAmountCommandTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IPricingClient _pricingClient = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ITaxService _taxService = Substitute.For(); + + private readonly PreviewTaxAmountCommand _command; + + public PreviewTaxAmountCommandTests() + { + _command = new PreviewTaxAmountCommand(_logger, _pricingClient, _stripeAdapter, _taxService); + } + + [Fact] + public async Task Run_WithSeatBasedPasswordManagerPlan_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_WithNonSeatBasedPasswordManagerPlan_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.FamiliesAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripePlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_WithSecretsManagerPlan_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.SecretsManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.SubscriptionDetails.Items[1].Price == plan.SecretsManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[1].Quantity == 1 && + options.Coupon == StripeConstants.CouponIDs.SecretsManagerStandalone && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_NonUSWithoutTaxId_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == false + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_NonUSWithTaxId_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345", + TaxId = "123456789" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId) + .Returns("ca_st"); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.CustomerDetails.TaxIds.Count == 1 && + options.CustomerDetails.TaxIds[0].Type == "ca_st" && + options.CustomerDetails.TaxIds[0].Value == "123456789" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_NonUSWithTaxId_UnknownTaxIdType_BadRequest() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345", + TaxId = "123456789" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId) + .Returns((string)null); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal(BillingErrorTranslationKeys.UnknownTaxIdType, badRequest.TranslationKey); + } + + [Fact] + public async Task Run_CustomerTaxLocationInvalid_BadRequest() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Throws(new StripeException + { + StripeError = new StripeError { Code = StripeConstants.ErrorCodes.CustomerTaxLocationInvalid } + }); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal(BillingErrorTranslationKeys.CustomerTaxLocationInvalid, badRequest.TranslationKey); + } + + [Fact] + public async Task Run_TaxIdInvalid_BadRequest() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Throws(new StripeException + { + StripeError = new StripeError { Code = StripeConstants.ErrorCodes.TaxIdInvalid } + }); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal(BillingErrorTranslationKeys.TaxIdInvalid, badRequest.TranslationKey); + } +} diff --git a/test/Core.Test/Billing/Services/Implementations/AutomaticTaxFactoryTests.cs b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs similarity index 95% rename from test/Core.Test/Billing/Services/Implementations/AutomaticTaxFactoryTests.cs rename to test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs index 7d5c9c3a26..d9d2679bca 100644 --- a/test/Core.Test/Billing/Services/Implementations/AutomaticTaxFactoryTests.cs +++ b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs @@ -2,15 +2,15 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Billing.Services.Implementations; +namespace Bit.Core.Test.Billing.Tax.Services; [SutProviderCustomize] public class AutomaticTaxFactoryTests diff --git a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategyTests.cs b/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs similarity index 99% rename from test/Core.Test/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategyTests.cs rename to test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs index dc40656275..dc10d222f1 100644 --- a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategyTests.cs +++ b/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -7,7 +7,7 @@ using NSubstitute; using Stripe; using Xunit; -namespace Bit.Core.Test.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Test.Billing.Tax.Services; [SutProviderCustomize] public class BusinessUseAutomaticTaxStrategyTests diff --git a/test/Core.Test/Billing/Stubs/FakeAutomaticTaxStrategy.cs b/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs similarity index 92% rename from test/Core.Test/Billing/Stubs/FakeAutomaticTaxStrategy.cs rename to test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs index 253aead5c7..2f3cbc98ee 100644 --- a/test/Core.Test/Billing/Stubs/FakeAutomaticTaxStrategy.cs +++ b/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services; using Stripe; -namespace Bit.Core.Test.Billing.Stubs; +namespace Bit.Core.Test.Billing.Tax.Services; /// /// Whether the subscription options will have automatic tax enabled or not. diff --git a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategyTests.cs b/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs similarity index 98% rename from test/Core.Test/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategyTests.cs rename to test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs index 2d50c9f75a..30614b94ba 100644 --- a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategyTests.cs +++ b/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -7,7 +7,7 @@ using NSubstitute; using Stripe; using Xunit; -namespace Bit.Core.Test.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Test.Billing.Tax.Services; [SutProviderCustomize] public class PersonalUseAutomaticTaxStrategyTests diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index cc19c50c35..c0f91a7bd3 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -2,8 +2,6 @@ false Bit.Core.Test - - $(WarningsNotAsErrors);CS4014 diff --git a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs similarity index 100% rename from test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs rename to test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs diff --git a/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs similarity index 100% rename from test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs rename to test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs diff --git a/test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs similarity index 100% rename from test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs rename to test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs diff --git a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs index de5fb25fca..08771df06a 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs +++ b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs @@ -28,14 +28,17 @@ public static class OrganizationLicenseFileFixtures private const string Version15 = "{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 14,\n 'Issued': '2023-12-14T02:03:33.374297Z',\n 'Refresh': '2023-12-07T22:42:33.970597Z',\n 'Expires': '2023-12-21T02:03:33.374297Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'LimitCollectionCreationDeletion': true,\n 'AllowAdminAccessToAllCollectionItems': true,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': 'EZl4IvJaa1E5mPmlfp4p5twAtlmaxlF1yoZzVYP4vog=',\n 'Signature': ''\n}"; - private static readonly Dictionary LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 } }; + private const string Version16 = + "{\n'LicenseKey': 'myLicenseKey',\n'InstallationId': '78900000-0000-0000-0000-000000000123',\n'Id': '12300000-0000-0000-0000-000000000456',\n'Name': 'myOrg',\n'BillingEmail': 'myBillingEmail',\n'BusinessName': 'myBusinessName',\n'Enabled': true,\n'Plan': 'myPlan',\n'PlanType': 11,\n'Seats': 10,\n'MaxCollections': 2,\n'UsePolicies': true,\n'UseSso': true,\n'UseKeyConnector': true,\n'UseScim': true,\n'UseGroups': true,\n'UseEvents': true,\n'UseDirectory': true,\n'UseTotp': true,\n'Use2fa': true,\n'UseApi': true,\n'UseResetPassword': true,\n'MaxStorageGb': 100,\n'SelfHost': true,\n'UsersGetPremium': true,\n'UseCustomPermissions': true,\n'Version': 15,\n'Issued': '2025-05-16T20:50:09.036931Z',\n'Refresh': '2025-05-23T20:50:09.036931Z',\n'Expires': '2025-05-23T20:50:09.036931Z',\n'ExpirationWithoutGracePeriod': null,\n'UsePasswordManager': true,\n'UseSecretsManager': true,\n'SmSeats': 5,\n'SmServiceAccounts': 8,\n'UseRiskInsights': false,\n'LimitCollectionCreationDeletion': true,\n'AllowAdminAccessToAllCollectionItems': true,\n'Trial': true,\n'LicenseType': 1,\n'UseOrganizationDomains': true,\n'UseAdminSponsoredFamilies': false,\n'Hash': 'k3M9SpHKUo0TmuSnNipeZleCHxgcEycKRXYl9BAg30Q=',\n'Signature': '',\n'Token': null\n}"; + + private static readonly Dictionary LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 }, { 16, Version16 } }; public static OrganizationLicense GetVersion(int licenseVersion) { if (!LicenseVersions.ContainsKey(licenseVersion)) { throw new Exception( - $"Cannot find serialized license version {licenseVersion}. You must add this to OrganizationLicenseFileFixtures when adding a new license version."); + $"Cannot find serialized license version {licenseVersion}. You must add this to OrganizationLicenseFileFixtures when adding a new license version."); } var json = LicenseVersions.GetValueOrDefault(licenseVersion).Replace("'", "\""); @@ -76,6 +79,7 @@ public static class OrganizationLicenseFileFixtures MaxCollections = 2, UsePolicies = true, UseSso = true, + UseOrganizationDomains = true, UseKeyConnector = true, UseScim = true, UseGroups = true, diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 420d330aaa..5ad6abd26a 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -86,7 +86,8 @@ public class UpdateOrganizationLicenseCommandTests "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion", - "LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems") && + "LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems", + "UseOrganizationDomains", "UseAdminSponsoredFamilies") && // Same property but different name, use explicit mapping org.ExpirationDate == license.Expires)); } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs index f6b6721bd2..7dc6b7360d 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs @@ -168,6 +168,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase }); sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId.Value); + // Setup for checking available seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(0); + await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null); @@ -293,6 +298,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase { sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; sponsoringOrg.UseAdminSponsoredFamilies = true; + sponsoringOrg.Seats = 10; sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); @@ -311,6 +317,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase } ]); + // Setup for checking available seats - organization has plenty of seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(5); + var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes); @@ -331,5 +342,121 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); + + // Verify we didn't need to add seats + await sutProvider.GetDependency().DidNotReceive() + .AutoAddSeatsAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_AutoscalesWhenNeeded( + OrganizationUserType organizationUserType, + Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail, + string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrg.UseAdminSponsoredFamilies = true; + sponsoringOrg.Seats = 10; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); + sutProvider.GetDependency().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo => + { + var sponsorship = callInfo.Arg(); + sponsorship.Id = sponsorshipId; + }); + sutProvider.GetDependency().UserId.Returns(currentUserId); + sutProvider.GetDependency().Organizations.Returns([ + new() + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = true }, + Type = organizationUserType + } + ]); + + // Setup for checking available seats - organization has no available seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(10); + + // Setup for checking if can scale + sutProvider.GetDependency() + .CanScaleAsync(sponsoringOrg, 1) + .Returns((true, "")); + + var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes); + + + var expectedSponsorship = new OrganizationSponsorship + { + Id = sponsorshipId, + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, + OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + IsAdminInitiated = true, + Notes = notes + }; + + Assert.True(SponsorshipValidator(expectedSponsorship, actual)); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); + + // Verify we needed to add seats + await sutProvider.GetDependency().Received(1) + .AutoAddSeatsAsync(sponsoringOrg, 1); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_ThrowsWhenCannotAutoscale( + OrganizationUserType organizationUserType, + Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail, + string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrg.UseAdminSponsoredFamilies = true; + sponsoringOrg.Seats = 10; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); + sutProvider.GetDependency().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo => + { + var sponsorship = callInfo.Arg(); + sponsorship.Id = sponsorshipId; + }); + sutProvider.GetDependency().UserId.Returns(currentUserId); + sutProvider.GetDependency().Organizations.Returns([ + new() + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = true }, + Type = organizationUserType + } + ]); + + // Setup for checking available seats - organization has no available seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(10); + + // Setup for checking if can scale - cannot scale + var failureReason = "Seat limit has been reached."; + sutProvider.GetDependency() + .CanScaleAsync(sponsoringOrg, 1) + .Returns((false, failureReason)); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes)); + + Assert.Equal(failureReason, exception.Message); } } diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 835f69b214..7d8a059d76 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,13 +1,12 @@ using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.Api.Requests; -using Bit.Core.Billing.Models.Api.Requests.Organizations; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Requests; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Test.Billing.Stubs; +using Bit.Core.Test.Billing.Tax.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 02ff24d9bf..a5bfe35152 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -2,13 +2,17 @@ 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.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -324,7 +328,9 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); @@ -341,27 +347,11 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse( - SutProvider sutProvider, Guid userId) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - var result = await sutProvider.Sut.IsClaimedByAnyOrganizationAsync(userId); - Assert.False(result); - } - - [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue( + public async Task IsClaimedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; - organization.UseSso = true; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); + organization.UseOrganizationDomains = true; sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) @@ -372,15 +362,11 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse( + public async Task IsClaimedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = false; - organization.UseSso = true; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); + organization.UseOrganizationDomains = true; sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) @@ -391,15 +377,11 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse( + public async Task IsClaimedByAnyOrganizationAsync_WithOrganizationUseOrganizationDomaisFalse_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; - organization.UseSso = false; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); + organization.UseOrganizationDomains = false; sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) @@ -410,97 +392,7 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail( - SutProvider sutProvider, User user, Organization organization) - { - // Arrange - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new() { Enabled = true } - }); - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) - .Returns( - [ - new OrganizationUserPolicyDetails - { - OrganizationId = organization.Id, - PolicyType = PolicyType.TwoFactorAuthentication, - PolicyEnabled = true - } - ]); - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); - - // Act - await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); - await sutProvider.GetDependency() - .Received(1) - .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - await sutProvider.GetDependency() - .Received(1) - .RemoveUserAsync(organization.Id, user.Id); - await sutProvider.GetDependency() - .Received(1) - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email); - } - - [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization( - SutProvider sutProvider, User user, Organization organization) - { - // Arrange - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new() { Enabled = true }, - [TwoFactorProviderType.Remember] = new() { Enabled = true } - }); - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) - .Returns( - [ - new OrganizationUserPolicyDetails - { - OrganizationId = organization.Id, - PolicyType = PolicyType.TwoFactorAuthentication, - PolicyEnabled = true - } - ]); - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary - { - [TwoFactorProviderType.Remember] = new() { Enabled = true } - }, JsonHelpers.LegacyEnumKeyResolver); - - // Act - await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); - await sutProvider.GetDependency() - .Received(1) - .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .RemoveUserAsync(default, default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default); - } - - [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( + public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( SutProvider sutProvider, User user, Organization organization1, Guid organizationUserId1, Organization organization2, Guid organizationUserId2) @@ -513,9 +405,6 @@ public class UserServiceTests organization1.Enabled = organization2.Enabled = true; organization1.UseSso = organization2.UseSso = true; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) .Returns( @@ -578,7 +467,79 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization( + public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( + SutProvider sutProvider, User user, + Organization organization1, Guid organizationUserId1, + Organization organization2, Guid organizationUserId2) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true } + }); + organization1.Enabled = organization2.Enabled = true; + organization1.UseSso = organization2.UseSso = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organization1.Id, + OrganizationUserId = organizationUserId1, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + }, + new PolicyDetails + { + OrganizationId = organization2.Id, + OrganizationUserId = organizationUserId2, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + sutProvider.GetDependency() + .GetManyByIdsAsync(Arg.Is>(ids => ids.Contains(organization1.Id) && ids.Contains(organization2.Id))) + .Returns(new[] { organization1, organization2 }); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); + + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); + + // Revoke the user from the first organization + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => r.OrganizationId == organization1.Id && + r.OrganizationUsers.First().Id == organizationUserId1 && + r.OrganizationUsers.First().OrganizationId == organization1.Id)); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email); + + // Remove the user from the second organization + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => r.OrganizationId == organization2.Id && + r.OrganizationUsers.First().Id == organizationUserId2 && + r.OrganizationUsers.First().OrganizationId == organization2.Id)); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email); + } + + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization( SutProvider sutProvider, User user, Organization organization) { // Arrange @@ -601,6 +562,9 @@ public class UserServiceTests sutProvider.GetDependency() .GetByIdAsync(organization.Id) .Returns(organization); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary { [TwoFactorProviderType.Remember] = new() { Enabled = true } @@ -621,6 +585,53 @@ public class UserServiceTests .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); } + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization( + SutProvider sutProvider, User user, Organization organization) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true }, + [TwoFactorProviderType.Remember] = new() { Enabled = true } + }); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organization.Id, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary + { + [TwoFactorProviderType.Remember] = new() { Enabled = true } + }, JsonHelpers.LegacyEnumKeyResolver); + + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RevokeNonCompliantOrganizationUsersAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); + } + [Theory, BitAutoData] public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled( SutProvider sutProvider, string email, string secret) @@ -911,7 +922,9 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency(), + sutProvider.GetDependency() ); } } diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 89e6d152cc..f73a628940 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -49,7 +49,7 @@ public class ImportCiphersAsyncCommandTests await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); // Assert - await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>()); + await sutProvider.GetDependency().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any>()); await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } @@ -77,7 +77,7 @@ public class ImportCiphersAsyncCommandTests await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>()); + await sutProvider.GetDependency().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any>()); await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } diff --git a/test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs new file mode 100644 index 0000000000..3101273225 --- /dev/null +++ b/test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs @@ -0,0 +1,118 @@ +using System.Text.Json; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.Services; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +public class AnonymousSendCommandTests +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendAuthorizationService _sendAuthorizationService; + private readonly AnonymousSendCommand _anonymousSendCommand; + + public AnonymousSendCommandTests() + { + _sendRepository = Substitute.For(); + _sendFileStorageService = Substitute.For(); + _pushNotificationService = Substitute.For(); + _sendAuthorizationService = Substitute.For(); + + _anonymousSendCommand = new AnonymousSendCommand( + _sendRepository, + _sendFileStorageService, + _pushNotificationService, + _sendAuthorizationService); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_Success_ReturnsDownloadUrl() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + AccessCount = 0, + Data = JsonSerializer.Serialize(new { Id = "fileId123" }) + }; + var fileId = "fileId123"; + var password = "testPassword"; + var expectedUrl = "https://example.com/download"; + + _sendAuthorizationService + .SendCanBeAccessed(send, password) + .Returns(SendAccessResult.Granted); + + _sendFileStorageService + .GetSendFileDownloadUrlAsync(send, fileId) + .Returns(expectedUrl); + + // Act + var result = + await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password); + + // Assert + Assert.Equal(expectedUrl, result.Item1); + Assert.Equal(1, send.AccessCount); + + await _sendRepository.Received(1).ReplaceAsync(send); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_AccessDenied_ReturnsNullWithReasons() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + AccessCount = 0 + }; + var fileId = "fileId123"; + var password = "wrongPassword"; + + _sendAuthorizationService + .SendCanBeAccessed(send, password) + .Returns(SendAccessResult.Denied); + + // Act + var result = + await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password); + + // Assert + Assert.Null(result.Item1); + Assert.Equal(SendAccessResult.Denied, result.Item2); + Assert.Equal(0, send.AccessCount); + + await _sendRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncSendUpdateAsync(default); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_NotFileSend_ThrowsBadRequestException() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text + }; + var fileId = "fileId123"; + var password = "testPassword"; + + // Act & Assert + await Assert.ThrowsAsync(() => + _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password)); + } +} diff --git a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs new file mode 100644 index 0000000000..15e7d57651 --- /dev/null +++ b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs @@ -0,0 +1,1111 @@ +using System.Text.Json; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.CurrentContextFixtures; +using Bit.Core.Test.Tools.AutoFixture.SendFixtures; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures; +using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +[SutProviderCustomize] +[CurrentContextCustomize] +[UserSendCustomize] +public class NonAnonymousSendCommandTests +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendAuthorizationService _sendAuthorizationService; + private readonly ISendValidationService _sendValidationService; + private readonly IFeatureService _featureService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly ISendCoreHelperService _sendCoreHelperService; + private readonly NonAnonymousSendCommand _nonAnonymousSendCommand; + + public NonAnonymousSendCommandTests() + { + _sendRepository = Substitute.For(); + _sendFileStorageService = Substitute.For(); + _pushNotificationService = Substitute.For(); + _sendAuthorizationService = Substitute.For(); + _featureService = Substitute.For(); + _sendValidationService = Substitute.For(); + _referenceEventService = Substitute.For(); + _currentContext = Substitute.For(); + _sendCoreHelperService = Substitute.For(); + + _nonAnonymousSendCommand = new NonAnonymousSendCommand( + _sendRepository, + _sendFileStorageService, + _pushNotificationService, + _sendAuthorizationService, + _sendValidationService, + _referenceEventService, + _currentContext, + _sendCoreHelperService + ); + } + + // Disable Send policy check + [Theory] + [InlineData(SendType.File)] + [InlineData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType) + { + // Arrange + var send = new Send + { + Id = default, + Type = sendType, + UserId = Guid.NewGuid() + }; + + var user = new User + { + Id = send.UserId.Value, + Email = "test@example.com" + }; + + // Configure validation service to throw when DisableSend policy applies + _sendValidationService.ValidateUserCanSaveAsync(send.UserId.Value, send) + .Throws(new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("Enterprise Policy", exception.Message); + + // Verify the validation service was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(send.UserId.Value, send); + + // Verify repository was not called since exception was thrown + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableSend_DoesntApply_success(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + Data = "Text with Notes" + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Configure validation service to NOT throw (policy doesn't apply) + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was checked + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.SendHasNotes == true && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_Applies_throws(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = true + }; + + // Configure validation service to throw when HideEmail policy applies + _sendValidationService.ValidateUserCanSaveAsync(userId, send) + .Throws(new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("hide your email address", exception.Message); + + // Verify validation was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository was not called (exception prevented save) + if (isNewSend) + { + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + } + else + { + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + } + + // Verify push notification wasn't sent + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = true // Setting HideEmail to true + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Configure validation service to NOT throw (policy doesn't apply) + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was checked + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.HasPassword == false && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + [Theory] + [InlineData(SendType.File)] + [InlineData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = default, + Type = sendType, + UserId = userId + }; + + // Configure validation service to throw when DisableSend policy applies in vNext implementation + _sendValidationService.ValidateUserCanSaveAsync(userId, send) + .Returns(Task.FromException(new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."))); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("Enterprise Policy", exception.Message); + + // Verify validation service was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository and notification methods were not called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + Data = "Text with Notes" + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Configure validation service to return success for vNext implementation + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Enable feature flag for policy requirements (vNext path) + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was checked with vNext path + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.SendHasNotes == true && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + // Send Options Policy - Disable Hide Email check + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = true + }; + + // Enable feature flag for policy requirements (vNext path) + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Configure validation service to throw when DisableHideEmail policy applies in vNext implementation + _sendValidationService.ValidateUserCanSaveAsync(userId, send) + .Throws(new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("hide your email address", exception.Message); + + // Verify validation was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository was not called (exception prevented save) + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + + // Verify push notification wasn't sent + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + + // Verify reference event service wasn't called + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = false // Email is not hidden, so policy doesn't block + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Enable feature flag for policy requirements (vNext path) + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Configure validation service to allow saves when HideEmail is false + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was called with vNext path + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + [Fact] + public async Task SaveSendAsync_ExistingSend_Updates() + { + // Arrange + var userId = Guid.NewGuid(); + var sendId = Guid.NewGuid(); + var send = new Send + { + Id = sendId, + Type = SendType.Text, + UserId = userId, + Data = "Some text data" + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository was called with updated send + await _sendRepository.Received(1).UpsertAsync(send); + + // Check that the revision date was updated + Assert.NotEqual(initialDate, send.RevisionDate); + + // Verify push notification was sent for the update + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + + // Verify no reference event was raised (only happens for new sends) + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_TextType_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text, // Text type instead of File + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("not of type \"file\"", exception.Message); + + // Verify no further methods were called + await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 0L; // Empty file + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("No file data", exception.Message); + + // Verify no methods were called after validation failed + await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to throw when checking storage + _sendValidationService.StorageRemainingForSendAsync(send) + .Throws(new BadRequestException("You must have premium status to use file Sends.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("premium status", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to pass storage check + _sendValidationService.StorageRemainingForSendAsync(send).Returns(10240L); // 10KB remaining + + // Configure validation service to throw when checking user can save + _sendValidationService.When(x => x.ValidateUserCanSaveAsync(userId, send)) + .Throw(new BadRequestException("You must confirm your email before creating a Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("confirm your email", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify SaveSendAsync attempted to be called, triggering email validation + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify no repository or notification methods were called after validation failed + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to return 0 storage remaining + _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to return less storage remaining than needed + _sendValidationService.StorageRemainingForSendAsync(send).Returns(512L); // Only 512 bytes available + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 15L * 1024L * 1024L * 1024L; // 15GB + + // Configure validation service to return large but insufficient storage (10GB for self-hosted non-premium) + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(10L * 1024L * 1024L * 1024L); // 10GB remaining (self-hosted default) + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + + // Configure validation service to return 1GB storage (cloud non-premium default) + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining (cloud default) + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest() + { + // Arrange + var organizationId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + OrganizationId = organizationId + }; + + var fileData = new SendFileData + { + FileName = "test.txt" + }; + + const long fileLength = 1000; + + // Set up validation service to return 0 storage remaining + // This simulates the case when an organization's max storage is null + _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Equal("Not enough storage available.", exception.Message); + + // Verify the method was called exactly once + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + } + + [Fact] + public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest() + { + // Arrange + var orgId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + OrganizationId = orgId, + UserId = null + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + + // Configure validation service to throw BadRequest when checking storage for org without storage + _sendValidationService.StorageRemainingForSendAsync(send) + .Throws(new BadRequestException("This organization cannot use file sends.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("This organization cannot use file sends", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest() + { + // Arrange + var orgId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + OrganizationId = orgId, + UserId = null + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + + // Configure validation service to return 1GB storage (org's max storage limit) + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_HasEnoughStorage_Success() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 500L * 1024L; // 500KB + var expectedFileId = "generatedfileid"; + var expectedUploadUrl = "https://upload.example.com/url"; + + // Configure storage validation to return more storage than needed + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1024L * 1024L); // 1MB remaining + + // Configure file storage service to return upload URL + _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()) + .Returns(expectedUploadUrl); + + // Set up string generator to return predictable file ID + _sendCoreHelperService.SecureRandomString(32, false, false) + .Returns(expectedFileId); + + // Act + var result = await _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength); + + // Assert + Assert.Equal(expectedUploadUrl, result); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify upload URL was requested + await _sendFileStorageService.Received(1).GetSendFileUploadUrlAsync(send, expectedFileId); + } + + [Fact] + public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 500L * 1024L; // 500KB + var expectedFileId = "generatedfileid"; + + // Configure storage validation to return more storage than needed + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1024L * 1024L); // 1MB remaining + + // Set up string generator to return predictable file ID + _sendCoreHelperService.SecureRandomString(32, false, false) + .Returns(expectedFileId); + + // Configure file storage service to throw exception when getting upload URL + _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()) + .Throws(new Exception("Storage service unavailable")); + + // Act & Assert + await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify file was cleaned up after failure + await _sendFileStorageService.Received(1).DeleteFileAsync(send, expectedFileId); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest() + { + // Arrange + Stream stream = new MemoryStream(); + Send send = null; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("Send does not have file data", exception.Message); + + // Verify no interactions with storage service + await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest() + { + // Arrange + Stream stream = new MemoryStream(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Data = null // Send exists but has null Data property + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("Send does not have file data", exception.Message); + + // Verify no interactions with storage service + await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest() + { + // Arrange + Stream stream = new MemoryStream(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text, // Not a file type + UserId = Guid.NewGuid(), + Data = "{\"someData\":\"value\"}" // Has data, but not file data + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("Not a File Type Send.", exception.Message); + + // Verify no interactions with storage service + await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_StreamPositionRestToZero_Success() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + stream.Position = 2; + var sendId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var fileId = "existingfileid123"; + + var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; + var send = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(sendFileData) + }; + + // Setup validation to succeed + _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); + + // Act + await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); + + // Assert + // Verify file was uploaded with correct parameters + await _sendFileStorageService.Received(1).UploadNewFileAsync( + Arg.Is(s => s == stream && s.Position == 0), // Ensure stream position is reset + Arg.Is(s => s.Id == sendId && s.UserId == userId), + Arg.Is(id => id == fileId) + ); + } + + + [Fact] + public async Task UploadFileToExistingSendAsync_Success() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + stream.Position = 2; // Simulate a non-zero position + var sendId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var fileId = "existingfileid123"; + + var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; + var send = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(sendFileData) + }; + + _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); + + // Act + await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); + + // Assert + // Verify file was uploaded with correct parameters + await _sendFileStorageService.Received(1).UploadNewFileAsync( + Arg.Is(s => s == stream && s.Position == 0), // Ensure stream position is reset + Arg.Is(s => s.Id == sendId && s.UserId == userId), + Arg.Is(id => id == fileId) + ); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_InvalidSize_ThrowsBadRequest() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var sendId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var fileId = "existingfileid123"; + + var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; + var send = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(sendFileData) + }; + + // Configure storage service to upload successfully + _sendFileStorageService.UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + // Configure validation to fail due to file size mismatch + _nonAnonymousSendCommand.ConfirmFileSize(send) + .Returns(false); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("File received does not match expected file length.", exception.Message); + } +} diff --git a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs new file mode 100644 index 0000000000..c34afc42bd --- /dev/null +++ b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs @@ -0,0 +1,135 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Queries; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +public class SendAuthenticationQueryTests +{ + private readonly ISendRepository _sendRepository; + private readonly SendAuthenticationQuery _sendAuthenticationQuery; + + public SendAuthenticationQueryTests() + { + _sendRepository = Substitute.For(); + _sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository); + } + + [Fact] + public void Constructor_WithNullRepository_ThrowsArgumentNullException() + { + // Act & Assert + var exception = Assert.Throws(() => new SendAuthenticationQuery(null)); + Assert.Equal("sendRepository", exception.ParamName); + } + + [Theory] + [MemberData(nameof(AuthenticationMethodTestCases))] + public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType) + { + // Arrange + var sendId = Guid.NewGuid(); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + Assert.IsType(expectedType, result); + } + + [Theory] + [MemberData(nameof(EmailParsingTestCases))] + public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails) + { + // Arrange + var sendId = Guid.NewGuid(); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + var emailOtp = Assert.IsType(result); + Assert.Equal(expectedEmails, emailOtp.Emails); + } + + [Fact] + public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp() + { + // Arrange + var sendId = Guid.NewGuid(); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword"); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId() + { + // Arrange + var sendId = Guid.NewGuid(); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Fact] + public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException() + { + // Arrange + var sendId = Guid.NewGuid(); + var expectedException = new InvalidOperationException("Repository error"); + _sendRepository.GetByIdAsync(sendId).Returns(Task.FromException(expectedException)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _sendAuthenticationQuery.GetAuthenticationMethod(sendId)); + Assert.Same(expectedException, exception); + } + + public static IEnumerable AuthenticationMethodTestCases() + { + yield return new object[] { null, typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) }; + } + + public static IEnumerable EmailParsingTestCases() + { + yield return new object[] { "test@example.com", new[] { "test@example.com" } }; + yield return new object[] { "test1@example.com,test2@example.com", new[] { "test1@example.com", "test2@example.com" } }; + yield return new object[] { " test@example.com , other@example.com ", new[] { "test@example.com", "other@example.com" } }; + yield return new object[] { "test@example.com,,other@example.com", new[] { "test@example.com", "other@example.com" } }; + yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } }; + } + + private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password) + { + return new Send + { + Id = Guid.NewGuid(), + AccessCount = accessCount, + MaxAccessCount = maxAccessCount, + Emails = emails, + Password = password + }; + } +} diff --git a/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs b/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs new file mode 100644 index 0000000000..9b2637d030 --- /dev/null +++ b/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs @@ -0,0 +1,175 @@ +using Bit.Core.Context; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.Services; +using Microsoft.AspNetCore.Identity; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +public class SendAuthorizationServiceTests +{ + private readonly ISendRepository _sendRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IPushNotificationService _pushNotificationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly SendAuthorizationService _sendAuthorizationService; + + public SendAuthorizationServiceTests() + { + _sendRepository = Substitute.For(); + _passwordHasher = Substitute.For>(); + _pushNotificationService = Substitute.For(); + _referenceEventService = Substitute.For(); + _currentContext = Substitute.For(); + + _sendAuthorizationService = new SendAuthorizationService( + _sendRepository, + _passwordHasher, + _pushNotificationService, + _referenceEventService, + _currentContext); + } + + + [Fact] + public void SendCanBeAccessed_Success_ReturnsTrue() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = 10, + AccessCount = 5, + ExpirationDate = DateTime.UtcNow.AddYears(1), + DeletionDate = DateTime.UtcNow.AddYears(1), + Disabled = false, + Password = "hashedPassword123" + }; + + const string password = "TEST"; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), send.Password, password) + .Returns(PasswordVerificationResult.Success); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(send, password); + + // Assert + Assert.Equal(SendAccessResult.Granted, result); + } + + [Fact] + public void SendCanBeAccessed_NullMaxAccess_Success() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = null, + AccessCount = 5, + ExpirationDate = DateTime.UtcNow.AddYears(1), + DeletionDate = DateTime.UtcNow.AddYears(1), + Disabled = false, + Password = "hashedPassword123" + }; + + const string password = "TEST"; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), send.Password, password) + .Returns(PasswordVerificationResult.Success); + + // Act + var result = _sendAuthorizationService.SendCanBeAccessed(send, password); + + // Assert + Assert.Equal(SendAccessResult.Granted, result); + } + + [Fact] + public void SendCanBeAccessed_NullSend_DoesNotGrantAccess() + { + // Arrange + _passwordHasher + .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") + .Returns(PasswordVerificationResult.Success); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(null, "TEST"); + + // Assert + Assert.Equal(SendAccessResult.Denied, result); + } + + [Fact] + public void SendCanBeAccessed_RehashNeeded_RehashesPassword() + { + // Arrange + var now = DateTime.UtcNow; + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = null, + AccessCount = 5, + ExpirationDate = now.AddYears(1), + DeletionDate = now.AddYears(1), + Disabled = false, + Password = "TEST" + }; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") + .Returns(PasswordVerificationResult.SuccessRehashNeeded); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(send, "TEST"); + + // Assert + _passwordHasher + .Received(1) + .HashPassword(Arg.Any(), "TEST"); + + Assert.Equal(SendAccessResult.Granted, result); + } + + [Fact] + public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue() + { + // Arrange + var now = DateTime.UtcNow; + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = null, + AccessCount = 5, + ExpirationDate = now.AddYears(1), + DeletionDate = now.AddYears(1), + Disabled = false, + Password = "TEST" + }; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") + .Returns(PasswordVerificationResult.Failed); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(send, "TEST"); + + // Assert + Assert.Equal(SendAccessResult.PasswordInvalid, result); + } +} diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs deleted file mode 100644 index 86d476340d..0000000000 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ /dev/null @@ -1,867 +0,0 @@ -using System.Text; -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; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Platform.Push; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Test.AutoFixture.CurrentContextFixtures; -using Bit.Core.Test.Entities; -using Bit.Core.Test.Tools.AutoFixture.SendFixtures; -using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Data; -using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Services; -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; - -namespace Bit.Core.Test.Tools.Services; - -[SutProviderCustomize] -[CurrentContextCustomize] -[UserSendCustomize] -public class SendServiceTests -{ - private void SaveSendAsync_Setup(SendType sendType, bool disableSendPolicyAppliesToUser, - SutProvider sutProvider, Send send) - { - send.Id = default; - send.Type = sendType; - - sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync( - Arg.Any(), PolicyType.DisableSend).Returns(disableSendPolicyAppliesToUser); - } - - // Disable Send policy check - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType, - SutProvider sutProvider, Send send) - { - SaveSendAsync_Setup(sendType, disableSendPolicyAppliesToUser: true, sutProvider, send); - - await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableSend_DoesntApply_success(SendType sendType, - SutProvider sutProvider, Send send) - { - SaveSendAsync_Setup(sendType, disableSendPolicyAppliesToUser: false, sutProvider, send); - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - // Send Options Policy - Disable Hide Email check - - private void SaveSendAsync_HideEmail_Setup(bool disableHideEmailAppliesToUser, - SutProvider sutProvider, Send send, Policy policy) - { - send.HideEmail = true; - - var sendOptions = new SendOptionsPolicyData - { - DisableHideEmail = disableHideEmailAppliesToUser - }; - policy.Data = JsonSerializer.Serialize(sendOptions, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - - sutProvider.GetDependency().GetPoliciesApplicableToUserAsync( - Arg.Any(), PolicyType.SendOptions).Returns(new List() - { - new() { PolicyType = policy.Type, PolicyData = policy.Data, OrganizationId = policy.OrganizationId, PolicyEnabled = policy.Enabled } - }); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_Applies_throws(SendType sendType, - SutProvider sutProvider, Send send, Policy policy) - { - SaveSendAsync_Setup(sendType, false, sutProvider, send); - SaveSendAsync_HideEmail_Setup(true, sutProvider, send, policy); - - await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(SendType sendType, - SutProvider sutProvider, Send send, Policy policy) - { - SaveSendAsync_Setup(sendType, false, sutProvider, send); - SaveSendAsync_HideEmail_Setup(false, sutProvider, send, policy); - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - // Disable Send policy check - vNext - private void SaveSendAsync_Setup_vNext(SutProvider sutProvider, Send send, - DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement) - { - 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 - 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 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.", - 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 DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); - - 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 DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { 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 DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { 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 DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); - 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, - Send send) - { - send.Id = Guid.NewGuid(); - - var now = DateTime.UtcNow; - await sutProvider.Sut.SaveSendAsync(send); - - Assert.True(send.RevisionDate - now < TimeSpan.FromSeconds(1)); - - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .PushSyncSendUpdateAsync(send); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_TextType_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - send.Type = SendType.Text; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 0) - ); - - Assert.Contains("not of type \"file\"", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - send.Type = SendType.File; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 0) - ); - - Assert.Contains("no file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(false); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("must have premium", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = false, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("must confirm your email", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = true, - MaxStorageGb = null, - Storage = 0, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = true, - MaxStorageGb = 2, - Storage = 2 * UserTests.Multiplier, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = false, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .SelfHosted = true; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 11000 * UserTests.Multiplier) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = false, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .SelfHosted = false; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 2 * UserTests.Multiplier) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var org = new Organization - { - Id = Guid.NewGuid(), - MaxStorageGb = null, - }; - - send.UserId = null; - send.OrganizationId = org.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(org.Id) - .Returns(org); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("organization cannot use file sends", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var org = new Organization - { - Id = Guid.NewGuid(), - MaxStorageGb = null, - }; - - send.UserId = null; - send.OrganizationId = org.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(org.Id) - .Returns(org); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("organization cannot use file sends", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var org = new Organization - { - Id = Guid.NewGuid(), - MaxStorageGb = 1, - }; - - send.UserId = null; - send.OrganizationId = org.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(org.Id) - .Returns(org); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 2 * UserTests.Multiplier) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_HasEnoughStorage_Success(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - MaxStorageGb = 10, - }; - - var data = new SendFileData - { - - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - var testUrl = "https://test.com/"; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .GetSendFileUploadUrlAsync(send, Arg.Any()) - .Returns(testUrl); - - var utcNow = DateTime.UtcNow; - - var url = await sutProvider.Sut.SaveFileSendAsync(send, data, 1 * UserTests.Multiplier); - - Assert.Equal(testUrl, url); - Assert.True(send.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - - await sutProvider.GetDependency() - .Received(1) - .GetSendFileUploadUrlAsync(send, Arg.Any()); - - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .PushSyncSendUpdateAsync(send); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - MaxStorageGb = 10, - }; - - var data = new SendFileData - { - - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .GetSendFileUploadUrlAsync(send, Arg.Any()) - .Returns(callInfo => throw new Exception("Problem")); - - var utcNow = DateTime.UtcNow; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, data, 1 * UserTests.Multiplier) - ); - - Assert.True(send.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.Equal("Problem", exception.Message); - - await sutProvider.GetDependency() - .Received(1) - .GetSendFileUploadUrlAsync(send, Arg.Any()); - - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .PushSyncSendUpdateAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .DeleteFileAsync(send, Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest(SutProvider sutProvider) - { - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), null) - ); - - Assert.Contains("does not have file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - send.Data = null; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), send) - ); - - Assert.Contains("does not have file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), send) - ); - - Assert.Contains("not a file type send", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_Success(SutProvider sutProvider, - Send send) - { - var fileContents = "Test file content"; - - var sendFileData = new SendFileData - { - Id = "TEST", - Size = fileContents.Length, - Validated = false, - }; - - send.Type = SendType.File; - send.Data = JsonSerializer.Serialize(sendFileData); - - sutProvider.GetDependency() - .ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, Arg.Any()) - .Returns((true, sendFileData.Size)); - - await sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(Encoding.UTF8.GetBytes(fileContents)), send); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_InvalidSize(SutProvider sutProvider, - Send send) - { - var fileContents = "Test file content"; - - var sendFileData = new SendFileData - { - Id = "TEST", - Size = fileContents.Length, - }; - - send.Type = SendType.File; - send.Data = JsonSerializer.Serialize(sendFileData); - - sutProvider.GetDependency() - .ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, Arg.Any()) - .Returns((false, sendFileData.Size)); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(Encoding.UTF8.GetBytes(fileContents)), send) - ); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_Success(SutProvider sutProvider, Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = 10; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), send.Password, "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - Assert.True(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_NullMaxAccess_Success(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), send.Password, "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - Assert.True(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_NullSend_DoesNotGrantAccess(SutProvider sutProvider) - { - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(null, "TEST"); - - Assert.False(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_NullPassword_PasswordRequiredErrorReturnsTrue(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - send.Password = "HASH"; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, null); - - Assert.False(grant); - Assert.True(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_RehashNeeded_RehashesPassword(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - send.Password = "TEST"; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.SuccessRehashNeeded); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - sutProvider.GetDependency>() - .Received(1) - .HashPassword(Arg.Any(), "TEST"); - - Assert.True(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - send.Password = "TEST"; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.Failed); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - Assert.False(grant); - Assert.False(passwordRequiredError); - Assert.True(passwordInvalidError); - } -} diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index e5e2da6a82..05c6d358e5 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -28,4 +28,31 @@ public class StaticStoreTests Assert.NotNull(plan); Assert.Equal(planType, plan.Type); } + + [Fact] + public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed() + { + // Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/ + // URLs can contain unicode characters that to a computer would point to completely seperate domains but to the + // naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a + // URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a + // url update that could be missed in code review and then if they got a user to that URL Bitwarden could + // consider it equivalent with a cipher in the users vault and offer autofill when we should not. + // GitHub does now show a warning on non-ascii characters but it could still be missed. + // https://github.blog/changelog/2025-05-01-github-now-provides-a-warning-about-hidden-unicode-text/ + + // To defend against this: + // Loop through all equivalent domains and fail if any contain a non-ascii character + // non-ascii character can make a valid URL so it's possible that in the future we have a domain + // we want to allow list, that should be done through `continue`ing in the below foreach loop + // only if the domain strictly equals (do NOT use InvariantCulture comparison) the one added to our allow list. + foreach (var domain in StaticStore.GlobalDomains.SelectMany(p => p.Value)) + { + for (var i = 0; i < domain.Length; i++) + { + var character = domain[i]; + Assert.True(char.IsAscii(character), $"Domain: {domain} contains non-ASCII character: '{character}' at index: {i}"); + } + } + } } diff --git a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs index 59ec7350da..f72a1f5f82 100644 --- a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs +++ b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs @@ -40,12 +40,12 @@ public class GetTasksForOrganizationQueryTests var result = await sutProvider.Sut.GetTasksAsync(org.Id, status); Assert.Equal(2, result.Count); - sutProvider.GetDependency().Received(1).AuthorizeAsync( + await sutProvider.GetDependency().Received(1).AuthorizeAsync( Arg.Any(), org, Arg.Is>( e => e.Contains(SecurityTaskOperations.ListAllForOrganization) ) ); - sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); } [Theory, BitAutoData] @@ -82,11 +82,11 @@ public class GetTasksForOrganizationQueryTests await Assert.ThrowsAsync(() => sutProvider.Sut.GetTasksAsync(org.Id)); - sutProvider.GetDependency().Received(1).AuthorizeAsync( + await sutProvider.GetDependency().Received(1).AuthorizeAsync( Arg.Any(), org, Arg.Is>( e => e.Contains(SecurityTaskOperations.ListAllForOrganization) ) ); - sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + await sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); } } diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index 192ad87240..f288bc47d4 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -56,8 +56,7 @@ public class IdentityServerTests : IClassFixture var localFactory = new IdentityApplicationFactory(); var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash); using var body = await AssertDefaultTokenBodyAsync(context); var root = body.RootElement; @@ -71,71 +70,6 @@ public class IdentityServerTests : IClassFixture AssertUserDecryptionOptions(root); } - [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails( - RegisterFinishRequestModel requestModel) - { - requestModel.Email = "test+noauthemailheader@email.com"; - - var localFactory = new IdentityApplicationFactory(); - var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, null); - - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString(); - Assert.Equal("invalid_grant", error); - AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); - } - - [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails( - RegisterFinishRequestModel requestModel) - { - requestModel.Email = "test+badauthheader@email.com"; - - var localFactory = new IdentityApplicationFactory(); - var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, - context => context.Request.Headers.Append("Auth-Email", "bad_value")); - - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString(); - Assert.Equal("invalid_grant", error); - AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); - } - - [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails( - RegisterFinishRequestModel requestModel) - { - requestModel.Email = "test+badauthheader@email.com"; - - var localFactory = new IdentityApplicationFactory(); - var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail("bad_value")); - - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString(); - Assert.Equal("invalid_grant", error); - AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); - } - [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] @@ -156,8 +90,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } @@ -183,8 +116,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync( localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } @@ -208,8 +140,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); await AssertRequiredSsoAuthenticationResponseAsync(context); @@ -233,8 +164,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } @@ -257,8 +187,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); await AssertRequiredSsoAuthenticationResponseAsync(context); @@ -341,7 +270,7 @@ public class IdentityServerTests : IClassFixture { "grant_type", "password" }, { "username", model.Email }, { "password", model.MasterPasswordHash }, - }), context => context.SetAuthEmail(model.Email)); + })); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); @@ -554,12 +483,12 @@ public class IdentityServerTests : IClassFixture { "grant_type", "password" }, { "username", user.Email}, { "password", "master_password_hash" }, - }), context => context.SetAuthEmail(user.Email).SetIp("1.1.1.2")); + }), context => context.SetIp("1.1.1.2")); } } private async Task PostLoginAsync( - TestServer server, User user, string MasterPasswordHash, Action extraConfiguration) + TestServer server, User user, string MasterPasswordHash) { return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { @@ -571,7 +500,7 @@ public class IdentityServerTests : IClassFixture { "grant_type", "password" }, { "username", user.Email }, { "password", MasterPasswordHash }, - }), extraConfiguration); + })); } private async Task CreateOrganizationWithSsoPolicyAsync( diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 53116960f6..553decd542 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -143,7 +143,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); @@ -263,7 +263,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); @@ -307,7 +307,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); Assert.Equal(StatusCodes.Status400BadRequest, failedTokenContext.Response.StatusCode); Assert.NotNull(emailToken); @@ -326,7 +326,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert @@ -363,7 +363,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 9a1b8141ae..537aae0935 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -29,8 +29,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + GetFormUrlEncodedContent()); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -40,27 +39,6 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); - Assert.Equal("Auth-Email header invalid.", error); - } - [Theory, BitAutoData] public async Task ValidateAsync_UserNull_Failure(string username) { @@ -68,8 +46,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(username)); + GetFormUrlEncodedContent(username: username)); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -106,8 +83,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + GetFormUrlEncodedContent(password: badPassword)); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -155,7 +131,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + })); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -197,7 +173,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + })); // Assert diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index e36f7f37b6..a045490862 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -3,7 +3,6 @@ using System.Text; using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; @@ -38,7 +37,6 @@ public class AccountsControllerTests : IDisposable private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IRegisterUserCommand _registerUserCommand; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; @@ -54,7 +52,6 @@ public class AccountsControllerTests : IDisposable _logger = Substitute.For>(); _userRepository = Substitute.For(); _registerUserCommand = Substitute.For(); - _captchaValidationService = Substitute.For(); _assertionOptionsDataProtector = Substitute.For>(); _getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For(); _sendVerificationEmailForRegistrationCommand = Substitute.For(); @@ -68,7 +65,6 @@ public class AccountsControllerTests : IDisposable _logger, _userRepository, _registerUserCommand, - _captchaValidationService, _assertionOptionsDataProtector, _getWebAuthnLoginCredentialAssertionOptionsCommand, _sendVerificationEmailForRegistrationCommand, diff --git a/test/Identity.Test/Identity.Test.csproj b/test/Identity.Test/Identity.Test.csproj index 34010d811b..fc0cf07b63 100644 --- a/test/Identity.Test/Identity.Test.csproj +++ b/test/Identity.Test/Identity.Test.csproj @@ -2,8 +2,6 @@ false - - $(WarningsNotAsErrors);CS0672;CS1998 diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 1d58b62b02..9eb17da88a 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -33,7 +33,6 @@ public class BaseRequestValidatorTests private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -54,7 +53,6 @@ public class BaseRequestValidatorTests _deviceValidator = Substitute.For(); _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _mailService = Substitute.For(); _logger = Substitute.For>(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); @@ -72,7 +70,6 @@ public class BaseRequestValidatorTests _deviceValidator, _twoFactorAuthenticationValidator, _organizationUserRepository, - _mailService, _logger, _currentContext, _globalSettings, @@ -84,36 +81,6 @@ public class BaseRequestValidatorTests _policyRequirementQuery); } - /* Logic path - * ValidateAsync -> _Logger.LogInformation - * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - * |-> SetErrorResult - */ - [Theory, BitAutoData] - public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = true; - _sut.isValid = true; - - // Act - await _sut.ValidateAsync(context); - - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - - // Assert - await _eventService.Received(1) - .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, - EventType.User_FailedLogIn); - Assert.True(context.GrantResult.IsError); - Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); - } - /* Logic path * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync @@ -128,8 +95,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - _globalSettings.Captcha.Returns(new GlobalSettings.CaptchaSettings()); _globalSettings.SelfHosted = true; _sut.isValid = false; @@ -142,44 +107,6 @@ public class BaseRequestValidatorTests Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); } - /* Logic path - * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - * |-> SetErrorResult - */ - [Theory, BitAutoData] - public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - // This needs to be n-1 of the max failed login attempts - context.CustomValidatorRequestContext.User.FailedLoginCount = 2; - context.CustomValidatorRequestContext.KnownDevice = false; - - _globalSettings.Captcha.Returns( - new GlobalSettings.CaptchaSettings - { - MaximumFailedLoginAttempts = 3 - }); - _sut.isValid = false; - - // Act - await _sut.ValidateAsync(context); - - // Assert - await _mailService.Received(1) - .SendFailedLoginAttemptsEmailAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - Assert.True(context.GrantResult.IsError); - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); - } - [Theory, BitAutoData] public async Task ValidateAsync_DeviceNotValidated_ShouldLogError( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -189,7 +116,6 @@ public class BaseRequestValidatorTests // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); // 1 -> to pass - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; // 2 -> will result to false with no extra configuration @@ -226,7 +152,6 @@ public class BaseRequestValidatorTests // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); // 1 -> to pass - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; // 2 -> will result to false with no extra configuration @@ -263,7 +188,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -294,7 +218,6 @@ public class BaseRequestValidatorTests // Arrange _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -326,7 +249,6 @@ public class BaseRequestValidatorTests // Arrange _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -363,7 +285,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -401,7 +322,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -439,7 +359,6 @@ public class BaseRequestValidatorTests var user = context.CustomValidatorRequestContext.User; user.Key = null; - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; _twoFactorAuthenticationValidator diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index b71dd6c230..9e20e630cd 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; @@ -28,7 +27,7 @@ public class DeviceValidatorTests private readonly IUserService _userService; private readonly IDistributedCache _distributedCache; private readonly Logger _logger; - private readonly IFeatureService _featureService; + private readonly DeviceValidator _sut; public DeviceValidatorTests() @@ -41,7 +40,6 @@ public class DeviceValidatorTests _userService = Substitute.For(); _distributedCache = Substitute.For(); _logger = new Logger(Substitute.For()); - _featureService = Substitute.For(); _sut = new DeviceValidator( _deviceService, _deviceRepository, @@ -50,8 +48,7 @@ public class DeviceValidatorTests _currentContext, _userService, _distributedCache, - _logger, - _featureService); + _logger); } [Theory, BitAutoData] @@ -312,8 +309,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); request.GrantType = grantType; @@ -336,8 +331,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); request.Raw.Add("AuthRequest", "authRequest"); @@ -360,8 +353,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); context.TwoFactorRequired = true; @@ -384,8 +375,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); context.SsoRequired = true; @@ -404,7 +393,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User = null; @@ -430,7 +418,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User.VerifyDevices = false; @@ -454,7 +441,6 @@ public class DeviceValidatorTests { // 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); @@ -479,7 +465,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); @@ -503,7 +488,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); @@ -535,7 +519,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); @@ -564,7 +547,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]); @@ -590,7 +572,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]); _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index fb4d7c321a..53e9a00c9f 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -27,11 +28,11 @@ public class TwoFactorAuthenticationValidatorTests private readonly IUserService _userService; private readonly UserManagerTestWrapper _userManager; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; - private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokenable; + private readonly ITwoFactorIsEnabledQuery _twoFactorenabledQuery; private readonly ICurrentContext _currentContext; private readonly TwoFactorAuthenticationValidator _sut; @@ -40,22 +41,22 @@ public class TwoFactorAuthenticationValidatorTests _userService = Substitute.For(); _userManager = SubstituteUserManager(); _organizationDuoUniversalTokenProvider = Substitute.For(); - _featureService = Substitute.For(); _applicationCacheService = Substitute.For(); _organizationUserRepository = Substitute.For(); _organizationRepository = Substitute.For(); _ssoEmail2faSessionTokenable = Substitute.For>(); + _twoFactorenabledQuery = Substitute.For(); _currentContext = Substitute.For(); _sut = new TwoFactorAuthenticationValidator( _userService, _userManager, _organizationDuoUniversalTokenProvider, - _featureService, _applicationCacheService, _organizationUserRepository, _organizationRepository, _ssoEmail2faSessionTokenable, + _twoFactorenabledQuery, _currentContext); } @@ -251,9 +252,9 @@ public class TwoFactorAuthenticationValidatorTests [Theory] [BitAutoData(TwoFactorProviderType.Email)] - public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull( - TwoFactorProviderType providerType, - User user) + public async void BuildTwoFactorResultAsync_SetsSsoToken_ReturnsNotNull( + TwoFactorProviderType providerType, + User user) { // Arrange var providerTypeInt = (int)providerType; @@ -263,9 +264,6 @@ public class TwoFactorAuthenticationValidatorTests _userManager.SUPPORTS_TWO_FACTOR = true; _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; - _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) - .Returns(true); - // Act var result = await _sut.BuildTwoFactorResultAsync(user, null); @@ -278,8 +276,6 @@ public class TwoFactorAuthenticationValidatorTests Assert.True(providers.ContainsKey(providerTypeInt.ToString())); Assert.True(result.ContainsKey("SsoEmail2faSessionToken")); Assert.True(result.ContainsKey("Email")); - - await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any()); } [Theory] @@ -322,9 +318,6 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.Email, user).Returns(true); - _userManager.TWO_FACTOR_PROVIDERS = ["email"]; // Act @@ -342,10 +335,8 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.Email, user).Returns(false); - _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + user.TwoFactorProviders = ""; // Act var result = await _sut.VerifyTwoFactorAsync( @@ -362,9 +353,6 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.OrganizationDuo, user).Returns(false); - _userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"]; // Act @@ -387,11 +375,9 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - providerType, user).Returns(true); - _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); // Act var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); @@ -412,11 +398,9 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - providerType, user).Returns(true); - _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); // Act var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index 89940275b0..25182743e5 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -18,6 +18,7 @@ public class UserDecryptionOptionsBuilderTests private readonly ICurrentContext _currentContext; private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private readonly UserDecryptionOptionsBuilder _builder; public UserDecryptionOptionsBuilderTests() @@ -25,7 +26,8 @@ public class UserDecryptionOptionsBuilderTests _currentContext = Substitute.For(); _deviceRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); - _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository); + _loginApprovingClientTypes = Substitute.For(); + _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes); } [Theory] @@ -102,12 +104,39 @@ public class UserDecryptionOptionsBuilderTests Assert.Equal(device.EncryptedUserKey, result.TrustedDeviceOption?.EncryptedUserKey); } - [Theory, BitAutoData] - public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + [Theory] + // Desktop + [BitAutoData(DeviceType.LinuxDesktop)] + [BitAutoData(DeviceType.MacOsDesktop)] + [BitAutoData(DeviceType.WindowsDesktop)] + [BitAutoData(DeviceType.UWP)] + // Mobile + [BitAutoData(DeviceType.Android)] + [BitAutoData(DeviceType.iOS)] + [BitAutoData(DeviceType.AndroidAmazon)] + // Web + [BitAutoData(DeviceType.ChromeBrowser)] + [BitAutoData(DeviceType.FirefoxBrowser)] + [BitAutoData(DeviceType.OperaBrowser)] + [BitAutoData(DeviceType.EdgeBrowser)] + [BitAutoData(DeviceType.IEBrowser)] + [BitAutoData(DeviceType.SafariBrowser)] + [BitAutoData(DeviceType.VivaldiBrowser)] + [BitAutoData(DeviceType.UnknownBrowser)] + public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) { + _loginApprovingClientTypes.TypesThatCanApprove.Returns(new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + }); + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; ssoConfig.Data = configurationData.Serialize(); - approvingDevice.Type = LoginApprovingDeviceTypes.Types.First(); + approvingDevice.Type = deviceType; _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); @@ -115,6 +144,80 @@ public class UserDecryptionOptionsBuilderTests Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice); } + [Theory] + // Desktop + [BitAutoData(DeviceType.LinuxDesktop)] + [BitAutoData(DeviceType.MacOsDesktop)] + [BitAutoData(DeviceType.WindowsDesktop)] + [BitAutoData(DeviceType.UWP)] + // Mobile + [BitAutoData(DeviceType.Android)] + [BitAutoData(DeviceType.iOS)] + [BitAutoData(DeviceType.AndroidAmazon)] + // Web + [BitAutoData(DeviceType.ChromeBrowser)] + [BitAutoData(DeviceType.FirefoxBrowser)] + [BitAutoData(DeviceType.OperaBrowser)] + [BitAutoData(DeviceType.EdgeBrowser)] + [BitAutoData(DeviceType.IEBrowser)] + [BitAutoData(DeviceType.SafariBrowser)] + [BitAutoData(DeviceType.VivaldiBrowser)] + [BitAutoData(DeviceType.UnknownBrowser)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] + public async Task Build_WhenHasLoginApprovingDeviceFeatureFlag_ShouldApprovingDeviceTrue( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + { + _loginApprovingClientTypes.TypesThatCanApprove.Returns(new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }); + + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + ssoConfig.Data = configurationData.Serialize(); + approvingDevice.Type = deviceType; + _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); + + var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); + + Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice); + } + + [Theory] + // CLI + [BitAutoData(DeviceType.WindowsCLI)] + [BitAutoData(DeviceType.MacOsCLI)] + [BitAutoData(DeviceType.LinuxCLI)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] + public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceFalse( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + { + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + ssoConfig.Data = configurationData.Serialize(); + approvingDevice.Type = deviceType; + _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); + + var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); + + Assert.False(result.TrustedDeviceOption?.HasLoginApprovingDevice); + } + [Theory, BitAutoData] public async Task Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue( SsoConfig ssoConfig, diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index ed28f00ce7..4c14de2d73 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -54,7 +54,6 @@ IBaseRequestValidatorTestWrapper IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -71,7 +70,6 @@ IBaseRequestValidatorTestWrapper deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, logger, currentContext, globalSettings, @@ -96,6 +94,7 @@ IBaseRequestValidatorTestWrapper return context.ValidatedTokenRequest.Subject ?? new ClaimsPrincipal(); } + [Obsolete] protected override void SetErrorResult( BaseRequestValidationContextFake context, Dictionary customResponse) @@ -103,6 +102,7 @@ IBaseRequestValidatorTestWrapper context.GrantResult = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse); } + [Obsolete] protected override void SetSsoResult( BaseRequestValidationContextFake context, Dictionary customResponse) @@ -121,6 +121,7 @@ IBaseRequestValidatorTestWrapper return Task.CompletedTask; } + [Obsolete] protected override void SetTwoFactorResult( BaseRequestValidationContextFake context, Dictionary customResponse) diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs index f1207a4b9a..3152f2327f 100644 --- a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -56,9 +56,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl ///
    /// /// - public override async Task GetTwoFactorEnabledAsync(TUser user) + public override Task GetTwoFactorEnabledAsync(TUser user) { - return TWO_FACTOR_ENABLED; + return Task.FromResult(TWO_FACTOR_ENABLED); } /// @@ -66,9 +66,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task> GetValidTwoFactorProvidersAsync(TUser user) + public override Task> GetValidTwoFactorProvidersAsync(TUser user) { - return TWO_FACTOR_PROVIDERS; + return Task.FromResult(TWO_FACTOR_PROVIDERS); } /// @@ -77,9 +77,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) + public override Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) { - return TWO_FACTOR_TOKEN; + return Task.FromResult(TWO_FACTOR_TOKEN); } /// @@ -89,8 +89,8 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) + public override Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) { - return TWO_FACTOR_TOKEN_VERIFIED; + return Task.FromResult(TWO_FACTOR_TOKEN_VERIFIED); } } diff --git a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs index e8bafaea5b..e5ad4f505a 100644 --- a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -7,6 +7,7 @@ using Bit.Core.Test.AutoFixture.Attributes; using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Xunit; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using Organization = Bit.Core.AdminConsole.Entities.Organization; @@ -161,7 +162,7 @@ public class OrganizationRepositoryTests [CiSkippedTheory, EfOrganizationUserAutoData] public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org, - List efOrgUserRepos, List efOrgRepos, List efUserRepos, + List efOrgUserRepos, List efOrgRepos, List efUserRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo) { orgUser.Type = OrganizationUserType.Owner; diff --git a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 21d4ca3476..b1f9968e14 100644 --- a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -24,7 +24,7 @@ public class OrganizationUserRepositoryTests { [CiSkippedTheory, EfOrganizationUserAutoData] public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, - OrganizationUserCompare equalityComparer, List suts, + OrganizationUserCompare equalityComparer, List suts, List efOrgRepos, List efUserRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) @@ -67,7 +67,7 @@ public class OrganizationUserRepositoryTests User user, Organization org, OrganizationUserCompare equalityComparer, - List suts, + List suts, List efUserRepos, List efOrgRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, @@ -113,7 +113,7 @@ public class OrganizationUserRepositoryTests } [CiSkippedTheory, EfOrganizationUserAutoData] - public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List suts, + public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List suts, List efUserRepos, List efOrgRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) @@ -188,7 +188,7 @@ public class OrganizationUserRepositoryTests List efPolicyRepository, List efUserRepository, List efOrganizationRepository, - List suts, + List suts, List efProviderRepository, List efProviderOrganizationRepository, List efProviderUserRepository, diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs index 191b48852b..8435f2734a 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs @@ -7,6 +7,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs b/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs index 65b4e4f6d0..7eb4a91ee9 100644 --- a/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs @@ -5,6 +5,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Repositories; using Bit.Test.Common.AutoFixture; diff --git a/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs index 3618d5dd0e..689bd5e243 100644 --- a/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs @@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Bit.Test.Common.AutoFixture.Attributes; using LinqToDB; using Xunit; +using EfAdminConsoleRepo = Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories; using SqlRepo = Bit.Infrastructure.Dapper.Repositories; @@ -112,7 +113,7 @@ public class CipherRepositoryTests [CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData] public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List users, List orgUsers, Collection collection, Organization org, List suts, List efUserRepos, List efOrgRepos, - List efOrgUserRepos, List efCollectionRepos) + List efOrgUserRepos, List efCollectionRepos) { var savedCiphers = new List(); foreach (var sut in suts) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs index e631280bb3..10361877d8 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; @@ -26,15 +27,23 @@ public static class OrganizationTestHelpers }); } + /// + /// Creates an Enterprise organization. + /// public static Task CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository, string identifier = "test") => organizationRepository.CreateAsync(new Organization { Name = $"{identifier}-{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 + Plan = "Enterprise (Annually)", // TODO: EF does not enforce this being NOT NULl + PlanType = PlanType.EnterpriseAnnually }); + /// + /// Creates a confirmed Owner for the specified organization and user. + /// Does not include any cryptographic material. + /// public static Task CreateTestOrganizationUserAsync( this IOrganizationUserRepository organizationUserRepository, Organization organization, @@ -47,6 +56,17 @@ public static class OrganizationTestHelpers Type = OrganizationUserType.Owner }); + public static Task CreateTestOrganizationUserInviteAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, // Invites are not linked to a UserId + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Owner + }); + public static Task CreateTestGroupAsync( this IGroupRepository groupRepository, Organization organization, @@ -54,4 +74,14 @@ public static class OrganizationTestHelpers => groupRepository.CreateAsync( new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" } ); + + public static Task CreateTestCollectionAsync( + this ICollectionRepository collectionRepository, + Organization organization, + string identifier = "test") + => collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = $"{identifier} {Guid.NewGuid()}" + }); } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs new file mode 100644 index 0000000000..8a51f201dc --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs @@ -0,0 +1,105 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class CollectionRepositoryCreateTests +{ + [DatabaseTheory, DatabaseData] + public async Task CreateAsync_WithAccess_Works( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user1 = await userRepository.CreateTestUserAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + + var user2 = await userRepository.CreateTestUserAsync(); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + var group1 = await groupRepository.CreateTestGroupAsync(organization); + var group2 = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + // Act + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, + ] + ); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Test Collection Name", actualCollection.Name); + + var groups = actualAccess.Groups.ToArray(); + Assert.Equal(2, groups.Length); + Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly); + Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly); + + var users = actualAccess.Users.ToArray(); + Assert.Equal(2, users.Length); + Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly); + Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly); + + // Clean up data + await userRepository.DeleteAsync(user1); + await userRepository.DeleteAsync(user2); + await organizationRepository.DeleteAsync(organization); + await groupRepository.DeleteManyAsync([group1.Id, group2.Id]); + await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id]); + } + + /// + /// Makes sure that the sproc handles empty sets. + /// + [DatabaseTheory, DatabaseData] + public async Task CreateAsync_WithNoAccess_Works( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + // Act + await collectionRepository.CreateAsync(collection, [], []); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Test Collection Name", actualCollection.Name); + + Assert.Empty(actualAccess.Groups); + Assert.Empty(actualAccess.Users); + + // Clean up + await organizationRepository.DeleteAsync(organization); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs new file mode 100644 index 0000000000..df01276493 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs @@ -0,0 +1,147 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class CollectionRepositoryReplaceTests +{ + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithAccess_Works( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user1 = await userRepository.CreateTestUserAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + + var user2 = await userRepository.CreateTestUserAsync(); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + var user3 = await userRepository.CreateTestUserAsync(); + var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3); + + var group1 = await groupRepository.CreateTestGroupAsync(organization); + var group2 = await groupRepository.CreateTestGroupAsync(organization); + var group3 = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, + ] + ); + + // Act + collection.Name = "Updated Collection Name"; + + await collectionRepository.ReplaceAsync(collection, + [ + // Delete group1 + // Update group2: + new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + // Add group3: + new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + // Delete orgUser1 + // Update orgUser2: + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true }, + // Add orgUser3: + new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + ] + ); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Updated Collection Name", actualCollection.Name); + + var groups = actualAccess.Groups.ToArray(); + Assert.Equal(2, groups.Length); + Assert.Single(groups, g => g.Id == group2.Id && g.Manage && g.HidePasswords && !g.ReadOnly); + Assert.Single(groups, g => g.Id == group3.Id && !g.Manage && !g.HidePasswords && g.ReadOnly); + + var users = actualAccess.Users.ToArray(); + + Assert.Equal(2, users.Length); + Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && !u.HidePasswords && u.ReadOnly); + Assert.Single(users, u => u.Id == orgUser3.Id && u.Manage && !u.HidePasswords && u.ReadOnly); + + // Clean up data + await userRepository.DeleteAsync(user1); + await userRepository.DeleteAsync(user2); + await userRepository.DeleteAsync(user3); + await organizationRepository.DeleteAsync(organization); + } + + /// + /// Makes sure that the sproc handles empty sets. + /// + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithNoAccess_Works( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + var group = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + ], + [ + new CollectionAccessSelection { Id = orgUser.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + ]); + + // Act + collection.Name = "Updated Collection Name"; + + await collectionRepository.ReplaceAsync(collection, [], []); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Updated Collection Name", actualCollection.Name); + + Assert.Empty(actualAccess.Groups); + Assert.Empty(actualAccess.Users); + + // Clean up + await userRepository.DeleteAsync(user); + await organizationRepository.DeleteAsync(organization); + } +} diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs similarity index 76% rename from test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs rename to test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs index 268d46ef6b..b96998415d 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs @@ -7,7 +7,7 @@ using Bit.Core.Models.Data; using Bit.Core.Repositories; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; public class CollectionRepositoryTests { @@ -463,147 +463,4 @@ public class CollectionRepositoryTests Assert.False(c3.Unmanaged); }); } - - [DatabaseTheory, DatabaseData] - public async Task ReplaceAsync_Works( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - IGroupRepository groupRepository, - ICollectionRepository collectionRepository) - { - 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 Org", - PlanType = PlanType.EnterpriseAnnually, - Plan = "Test Plan", - BillingEmail = "billing@email.com" - }); - - var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - var orgUser3 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - var group1 = await groupRepository.CreateAsync(new Group - { - Name = "Test Group #1", - OrganizationId = organization.Id, - }); - - var group2 = await groupRepository.CreateAsync(new Group - { - Name = "Test Group #2", - OrganizationId = organization.Id, - }); - - var group3 = await groupRepository.CreateAsync(new Group - { - Name = "Test Group #3", - OrganizationId = organization.Id, - }); - - var collection = new Collection - { - Name = "Test Collection Name", - OrganizationId = organization.Id, - }; - - await collectionRepository.CreateAsync(collection, - [ - new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, - new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, - ], - [ - new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, - new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, - ] - ); - - collection.Name = "Updated Collection Name"; - - await collectionRepository.ReplaceAsync(collection, - [ - // Should delete group1 - new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, - // Should add group3 - new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, - ], - [ - // Should delete orgUser1 - new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true }, - // Should add orgUser3 - new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true }, - ] - ); - - // Assert it - var info = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true); - - Assert.NotNull(info); - - Assert.Equal("Updated Collection Name", info.Name); - - var groups = info.Groups.ToArray(); - - Assert.Equal(2, groups.Length); - - var actualGroup2 = Assert.Single(groups.Where(g => g.Id == group2.Id)); - - Assert.True(actualGroup2.Manage); - Assert.True(actualGroup2.HidePasswords); - Assert.False(actualGroup2.ReadOnly); - - var actualGroup3 = Assert.Single(groups.Where(g => g.Id == group3.Id)); - - Assert.False(actualGroup3.Manage); - Assert.False(actualGroup3.HidePasswords); - Assert.True(actualGroup3.ReadOnly); - - var users = info.Users.ToArray(); - - Assert.Equal(2, users.Length); - - var actualOrgUser2 = Assert.Single(users.Where(u => u.Id == orgUser2.Id)); - - Assert.False(actualOrgUser2.Manage); - Assert.False(actualOrgUser2.HidePasswords); - Assert.True(actualOrgUser2.ReadOnly); - - var actualOrgUser3 = Assert.Single(users.Where(u => u.Id == orgUser3.Id)); - - Assert.True(actualOrgUser3.Manage); - Assert.False(actualOrgUser3.HidePasswords); - Assert.True(actualOrgUser3.ReadOnly); - - // Clean up data - await userRepository.DeleteAsync(user); - await organizationRepository.DeleteAsync(organization); - await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]); - await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]); - } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs new file mode 100644 index 0000000000..0f6393fec0 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs @@ -0,0 +1,85 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; + +public class OrganizationUserReplaceTests +{ + /// + /// Specifically tests OrganizationUsers in the invited state, which is unique because + /// they're not linked to a UserId. + /// + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsInvited_Success( + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); + + // Act: update the user, including collection access so we test this overloaded method + orgUser.Type = OrganizationUserType.Admin; + orgUser.AccessSecretsManager = true; + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + + await organizationUserRepository.ReplaceAsync(orgUser, [ + new CollectionAccessSelection { Id = collection.Id, Manage = true } + ]); + + // Assert + var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id); + Assert.NotNull(actualOrgUser); + Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type); + Assert.True(actualOrgUser.AccessSecretsManager); + + var collectionAccess = Assert.Single(actualCollections); + Assert.Equal(collection.Id, collectionAccess.Id); + Assert.True(collectionAccess.Manage); + } + + /// + /// Tests OrganizationUsers in the Confirmed status, which is a stand-in for all other + /// non-Invited statuses (which are all linked to a UserId). + /// + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user = await userRepository.CreateTestUserAsync(); + // OrganizationUser is linked with the User in the Confirmed status + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + // Act: update the user, including collection access so we test this overloaded method + orgUser.Type = OrganizationUserType.Admin; + orgUser.AccessSecretsManager = true; + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + + await organizationUserRepository.ReplaceAsync(orgUser, [ + new CollectionAccessSelection { Id = collection.Id, Manage = true } + ]); + + // Assert + var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id); + Assert.NotNull(actualOrgUser); + Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type); + Assert.True(actualOrgUser.AccessSecretsManager); + + var collectionAccess = Assert.Single(actualCollections); + Assert.Equal(collection.Id, collectionAccess.Id); + Assert.True(collectionAccess.Manage); + + // Account revision date should be updated to a later date + var actualUser = await userRepository.GetByIdAsync(user.Id); + Assert.NotNull(actualUser); + Assert.True(actualUser.AccountRevisionDate.CompareTo(user.AccountRevisionDate) > 0); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs similarity index 96% rename from test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs rename to test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 637e970f8f..0df5dcfb50 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -8,7 +8,7 @@ using Bit.Core.Repositories; using Bit.Core.Utilities; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; public class OrganizationUserRepositoryTests { @@ -316,6 +316,29 @@ public class OrganizationUserRepositoryTests BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl Plan = "Test", // TODO: EF does not enforce this being NOT NULl PrivateKey = "privatekey", + UsePolicies = false, + UseSso = false, + UseKeyConnector = false, + UseScim = false, + UseGroups = false, + UseDirectory = false, + UseEvents = false, + UseTotp = false, + Use2fa = false, + UseApi = false, + UseResetPassword = false, + UseSecretsManager = false, + SelfHost = false, + UsersGetPremium = false, + UseCustomPermissions = false, + Enabled = true, + UsePasswordManager = false, + LimitCollectionCreation = false, + LimitCollectionDeletion = false, + LimitItemDeletion = false, + AllowAdminAccessToAllCollectionItems = false, + UseRiskInsights = false, + UseAdminSponsoredFamilies = false }); var organizationDomain = new OrganizationDomain @@ -335,6 +358,7 @@ public class OrganizationUserRepositoryTests UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); await organizationUserRepository.CreateAsync(new OrganizationUser diff --git a/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs new file mode 100644 index 0000000000..2d212d4e39 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs @@ -0,0 +1,46 @@ +using Bit.Core.Platform.Installations; +using Bit.Infrastructure.IntegrationTest.Comparers; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Platform.Installations; + +public class InstallationRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task GetByIdAsync_Works(IInstallationRepository installationRepository) + { + var installation = await installationRepository.CreateAsync(new Installation + { + Email = "test@email.com", + Key = "installation_key", + Enabled = true, + }); + + var retrievedInstallation = await installationRepository.GetByIdAsync(installation.Id); + + Assert.NotNull(retrievedInstallation); + Assert.Equal("installation_key", retrievedInstallation.Key); + } + + [DatabaseTheory, DatabaseData] + public async Task UpdateAsync_Works(IInstallationRepository installationRepository) + { + var installation = await installationRepository.CreateAsync(new Installation + { + Email = "test@email.com", + Key = "installation_key", + Enabled = true, + }); + + var now = DateTime.UtcNow; + + installation.LastActivityDate = now; + + await installationRepository.ReplaceAsync(installation); + + var retrievedInstallation = await installationRepository.GetByIdAsync(installation.Id); + + Assert.NotNull(retrievedInstallation.LastActivityDate); + Assert.Equal(now, retrievedInstallation.LastActivityDate.Value, LaxDateTimeComparer.Default); + } +} diff --git a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs index a686605836..eced27f937 100644 --- a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs +++ b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs @@ -5,10 +5,8 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Utilities; using Bit.Identity; using Bit.Test.Common.Helpers; -using HandlebarsDotNet; using LinqToDB; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -98,7 +96,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase { "grant_type", "password" }, { "username", username }, { "password", password }, - }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username))); + })); return context; } @@ -126,7 +124,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase { "TwoFactorToken", twoFactorToken }, { "TwoFactorProvider", twoFactorProviderType }, { "TwoFactorRemember", "1" }, - }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username))); + })); return context; } diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index c1089608da..76fa0f03d1 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -1,5 +1,4 @@ using AspNetCoreRateLimit; -using Bit.Core.Auth.Services; using Bit.Core.Billing.Services; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; @@ -207,8 +206,6 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory Replace(services); - Replace(services); - // TODO: Install and use azurite in CI pipeline Replace(services); diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs index 562156b09e..128b38ff9a 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs @@ -1,5 +1,4 @@ using System.Net; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Primitives; @@ -62,12 +61,6 @@ public static class WebApplicationFactoryExtensions Action extraConfiguration = null) => SendAsync(server, HttpMethod.Delete, requestUri, content: content, extraConfiguration); - public static HttpContext SetAuthEmail(this HttpContext context, string username) - { - context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username)); - return context; - } - public static HttpContext SetIp(this HttpContext context, string ip) { context.Connection.RemoteIpAddress = IPAddress.Parse(ip); diff --git a/util/DbSeederUtility/DbSeederUtility.csproj b/util/DbSeederUtility/DbSeederUtility.csproj new file mode 100644 index 0000000000..90ac7f22b4 --- /dev/null +++ b/util/DbSeederUtility/DbSeederUtility.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + Bit.DbSeederUtility + DbSeeder + true + 2294c6ba-7cd0-4293-a797-3882e41c61cb + + + + + + + + + + + diff --git a/util/DbSeederUtility/GlobalSettingsFactory.cs b/util/DbSeederUtility/GlobalSettingsFactory.cs new file mode 100644 index 0000000000..e4ad275a0e --- /dev/null +++ b/util/DbSeederUtility/GlobalSettingsFactory.cs @@ -0,0 +1,34 @@ +using Bit.Core.Settings; +using Microsoft.Extensions.Configuration; + +namespace Bit.DbSeederUtility; + +public static class GlobalSettingsFactory +{ + private static GlobalSettings? _globalSettings; + + public static GlobalSettings GlobalSettings + { + get { return _globalSettings ??= LoadGlobalSettings(); } + } + + private static GlobalSettings LoadGlobalSettings() + { + Console.WriteLine("Loading global settings..."); + + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true) + .AddUserSecrets("bitwarden-Api") // Load user secrets from the API project + .AddEnvironmentVariables(); + + var configuration = configBuilder.Build(); + var globalSettingsSection = configuration.GetSection("globalSettings"); + + var settings = new GlobalSettings(); + globalSettingsSection.Bind(settings); + + return settings; + } +} diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs new file mode 100644 index 0000000000..2d75b31934 --- /dev/null +++ b/util/DbSeederUtility/Program.cs @@ -0,0 +1,39 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Recipes; +using CommandDotNet; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.DbSeederUtility; + +public class Program +{ + private static int Main(string[] args) + { + return new AppRunner() + .Run(args); + } + + [Command("organization", Description = "Seed an organization and organization users")] + public void Organization( + [Option('n', "Name", Description = "Name of organization")] + string name, + [Option('u', "users", Description = "Number of users to generate")] + int users, + [Option('d', "domain", Description = "Email domain for users")] + string domain + ) + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ServiceCollectionExtension.ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get a scoped DB context + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + var db = scopedServices.GetRequiredService(); + + var recipe = new OrganizationWithUsersRecipe(db); + recipe.Seed(name, users, domain); + } +} diff --git a/util/DbSeederUtility/README.md b/util/DbSeederUtility/README.md new file mode 100644 index 0000000000..0eb21ae6c5 --- /dev/null +++ b/util/DbSeederUtility/README.md @@ -0,0 +1,40 @@ +# Bitwarden Database Seeder Utility + +A command-line utility for generating and managing test data for Bitwarden databases. + +## Overview + +DbSeederUtility is an executable wrapper around the Seeder class library that provides a convenient command-line +interface for executing seed-recipes in your local environment. + +## Installation + +The utility can be built and run as a .NET 8 application: + +``` +dotnet build +dotnet run -- [options] +``` + +Or directly using the compiled executable: + +``` +DbSeeder.exe [options] +``` + +## Examples + +### Generate and load test organization + +```bash +# Generate an organization called "seeded" with 10000 users using the @large.test email domain. +# Login using "admin@large.test" with password "asdfasdfasdf" +DbSeeder.exe organization -n seeded -u 10000 -d large.test +``` + +## Dependencies + +This utility depends on: +- The Seeder class library +- CommandDotNet for command-line parsing +- .NET 8.0 runtime diff --git a/util/DbSeederUtility/ServiceCollectionExtension.cs b/util/DbSeederUtility/ServiceCollectionExtension.cs new file mode 100644 index 0000000000..0653bb1801 --- /dev/null +++ b/util/DbSeederUtility/ServiceCollectionExtension.cs @@ -0,0 +1,25 @@ +using Bit.SharedWeb.Utilities; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.DbSeederUtility; + +public static class ServiceCollectionExtension +{ + public static void ConfigureServices(ServiceCollection services) + { + // Load configuration using the GlobalSettingsFactory + var globalSettings = GlobalSettingsFactory.GlobalSettings; + + // Register services + services.AddLogging(builder => builder.AddConsole()); + services.AddSingleton(globalSettings); + + // Add Data Protection services + services.AddDataProtection() + .SetApplicationName("Bitwarden"); + + services.AddDatabaseRepositories(globalSettings); + } +} diff --git a/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql b/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql new file mode 100644 index 0000000000..9398845dc4 --- /dev/null +++ b/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql @@ -0,0 +1,350 @@ +CREATE OR ALTER PROCEDURE [dbo].[Cipher_UpdateAttachment] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50), + @AttachmentData NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + -- Validate that AttachmentData is valid JSON + IF ISJSON(@AttachmentData) = 0 + BEGIN + THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1; + RETURN; + END + + -- Validate that AttachmentData has the expected structure + -- Check for required fields + IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR + JSON_VALUE(@AttachmentData, '$.Size') IS NULL + BEGIN + THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1; + RETURN; + END + + -- Validate data types for critical fields + DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT) + IF @Size IS NULL OR @Size <= 0 + BEGIN + THROW 50000, 'AttachmentData has invalid Size value', 1; + RETURN; + END + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + DECLARE @NewAttachments NVARCHAR(MAX) + + -- Get current attachments + DECLARE @CurrentAttachments NVARCHAR(MAX) + SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id + + -- Prepare the new attachments value based on current state + IF @CurrentAttachments IS NULL + BEGIN + -- Create new JSON object with the attachment + SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') + + -- Validate the constructed JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when adding new attachment', 1; + RETURN; + END + END + ELSE + BEGIN + -- Validate existing attachments + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current attachments data is not valid JSON', 1; + RETURN; + END + + -- Modify existing JSON + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) + + -- Validate the modified JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1; + RETURN; + END + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteAttachment] + @Id UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @CurrentAttachments NVARCHAR(MAX) + DECLARE @NewAttachments NVARCHAR(MAX) + + -- Get current cipher data + SELECT + @UserId = [UserId], + @OrganizationId = [OrganizationId], + @CurrentAttachments = [Attachments] + FROM + [dbo].[Cipher] + WHERE [Id] = @Id + + -- If there are no attachments, nothing to do + IF @CurrentAttachments IS NULL + BEGIN + RETURN; + END + + -- Validate the initial JSON + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current initial attachments data is not valid JSON', 1; + RETURN; + END + + -- Check if the attachment exists before trying to remove it + IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL + BEGIN + -- Attachment doesn't exist, nothing to do + RETURN; + END + + -- Create the new attachments JSON with the specified attachment removed + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL) + + -- Validate the resulting JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when removing attachment', 1; + RETURN; + END + + -- Check if we've removed all attachments and have an empty object + IF @NewAttachments = '{}' + BEGIN + -- If we have an empty JSON object, set to NULL instead + SET @NewAttachments = NULL; + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +-- Remove [Attachments] assignment from Cipher_Create, Cipher_Update, and CipherDetails_Update procedures + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + 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, + @Favorites, + @Folders, + @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_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), -- 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(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, + [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 + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Favorites] = @Favorites, + [Folders] = @Folders, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Reprompt] = @Reprompt, + [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 diff --git a/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql b/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql new file mode 100644 index 0000000000..7260c9c6d4 --- /dev/null +++ b/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql @@ -0,0 +1,41 @@ +IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + ( + SELECT COUNT(1) + FROM [dbo].[OrganizationUserView] + WHERE OrganizationId = @OrganizationId + AND Status >= 0 --Invited + ) + + ( + SELECT COUNT(1) + FROM [dbo].[OrganizationSponsorship] + WHERE SponsoringOrganizationId = @OrganizationId + AND IsAdminInitiated = 1 + AND ( + -- Not marked for deletion - always count + (ToDelete = 0) + OR + -- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status) + (ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE()) + ) + AND ( + -- SENT status: When SponsoredOrganizationId is null + SponsoredOrganizationId IS NULL + OR + -- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE())) + ) + ) +END +GO diff --git a/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql b/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql new file mode 100644 index 0000000000..8fd465025c --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql @@ -0,0 +1,221 @@ +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[UseAdminSponsoredFamilies], + O.[LimitItemDeletion], + OS.[IsAdminInitiated] +FROM + [dbo].[OrganizationUser] OU + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] + LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] + LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +--Manually refresh [dbo].[OrganizationUserOrganizationDetailsView] +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorshipView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorshipView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUserDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationUserDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_CreateMany]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_CreateMany]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteExpired]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteExpired]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Update]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_Update]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Create]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_Create]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_UpdateMany]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_UpdateMany]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteByIds]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUser_DeleteById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUser_DeleteByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[Organization_DeleteById]'; +END +GO diff --git a/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql b/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql new file mode 100644 index 0000000000..bb3bdee9b9 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql @@ -0,0 +1,20 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +ALTER PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] + @SponsoringOrganizationUserId UNIQUEIDENTIFIER, + @IsAdminInitiated BIT = 0 +AS +BEGIN + SET NOCOUNT ON; + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId + and [IsAdminInitiated] = @IsAdminInitiated +END +GO diff --git a/util/Migrator/DbScripts/2025-05-13-00_AddUseOrganizationDomainsToOrganization.sql b/util/Migrator/DbScripts/2025-05-13-00_AddUseOrganizationDomainsToOrganization.sql new file mode 100644 index 0000000000..9bc205bfed --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-13-00_AddUseOrganizationDomainsToOrganization.sql @@ -0,0 +1,364 @@ +/* adds new column "UseOrganizationDomains" not nullable with default of 0 */ + +ALTER TABLE [dbo].[Organization] ADD [UseOrganizationDomains] bit NOT NULL CONSTRAINT [DF_Organization_UseOrganizationDomains] default (0) +GO + +/* add column to Organization_Create*/ + +CREATE OR ALTER PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT= null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, + @UseAdminSponsoredFamilies BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector], + [UseScim], + [UseCustomPermissions], + [UseSecretsManager], + [Status], + [UsePasswordManager], + [SmSeats], + [SmServiceAccounts], + [MaxAutoscaleSmSeats], + [MaxAutoscaleSmServiceAccounts], + [SecretsManagerBeta], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector, + @UseScim, + @UseCustomPermissions, + @UseSecretsManager, + @Status, + @UsePasswordManager, + @SmSeats, + @SmServiceAccounts, + @MaxAutoscaleSmSeats, + @MaxAutoscaleSmServiceAccounts, + @SecretsManagerBeta, + @LimitCollectionCreation, + @LimitCollectionDeletion, + @AllowAdminAccessToAllCollectionItems, + @UseRiskInsights, + @LimitItemDeletion, + @UseOrganizationDomains, + @UseAdminSponsoredFamilies + ) +END +GO + +/* add column to Organization_ReadAbilities*/ +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Use2fa], + CASE + WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN + 1 + ELSE + 0 + END AS [Using2fa], + [UsersGetPremium], + [UseCustomPermissions], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UsePolicies], + [Enabled], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies] + FROM + [dbo].[Organization] +END +GO + +/* add column to Organization_Update*/ +CREATE OR ALTER PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT = null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, + @UseAdminSponsoredFamilies BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector, + [UseScim] = @UseScim, + [UseCustomPermissions] = @UseCustomPermissions, + [UseSecretsManager] = @UseSecretsManager, + [Status] = @Status, + [UsePasswordManager] = @UsePasswordManager, + [SmSeats] = @SmSeats, + [SmServiceAccounts] = @SmServiceAccounts, + [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, + [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, + [SecretsManagerBeta] = @SecretsManagerBeta, + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, + [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, + [UseRiskInsights] = @UseRiskInsights, + [LimitItemDeletion] = @LimitItemDeletion, + [UseOrganizationDomains] = @UseOrganizationDomains, + [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2025-05-13-01_AddUseOrganizationDomainsToViews.sql b/util/Migrator/DbScripts/2025-05-13-01_AddUseOrganizationDomainsToViews.sql new file mode 100644 index 0000000000..2e53bb2473 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-13-01_AddUseOrganizationDomainsToViews.sql @@ -0,0 +1,131 @@ +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[LimitItemDeletion], + O.[UseAdminSponsoredFamilies], + O.[UseOrganizationDomains], + OS.[IsAdminInitiated] +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +AS +SELECT + PU.[UserId], + PO.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + PO.[Key], + O.[PublicKey], + O.[PrivateKey], + PU.[Status], + PU.[Type], + PO.[ProviderId], + PU.[Id] ProviderUserId, + P.[Name] ProviderName, + O.[PlanType], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[UseAdminSponsoredFamilies], + P.[Type] ProviderType, + O.[LimitItemDeletion], + O.[UseOrganizationDomains] +FROM + [dbo].[ProviderUser] PU +INNER JOIN + [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId] +INNER JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] +INNER JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationView] +AS +SELECT + * +FROM + [dbo].[Organization] +GO diff --git a/util/Migrator/DbScripts/2025-05-13-02_AddUseOrganizationDomainsDataMigration.sql b/util/Migrator/DbScripts/2025-05-13-02_AddUseOrganizationDomainsDataMigration.sql new file mode 100644 index 0000000000..505a667e8f --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-13-02_AddUseOrganizationDomainsDataMigration.sql @@ -0,0 +1,9 @@ +/* update the new column to have the value used in UseSso to preserve existing orgs ability */ + +UPDATE + [dbo].[Organization] +SET + [UseOrganizationDomains] = [UseSso] +WHERE + [UseSso] = 1 +GO diff --git a/util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql b/util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql new file mode 100644 index 0000000000..25969c1018 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql @@ -0,0 +1,139 @@ +-- Add `Emails` field that stores a comma-separated list of email addresses for +-- email/OTP authentication to table and write methods. The read methods +-- don't need to be updated because they all use `*`. +IF NOT EXISTS( + SELECT * + FROM [sys].[columns] + WHERE [object_id] = OBJECT_ID(N'[dbo].[Send]') + AND [name] = 'Emails') +BEGIN + ALTER TABLE [dbo].[Send] ADD [Emails] NVARCHAR(1024) NULL; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Send_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data VARCHAR(MAX), + @Key VARCHAR(MAX), + @Password NVARCHAR(300), + @MaxAccessCount INT, + @AccessCount INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7), + @DeletionDate DATETIME2(7), + @Disabled BIT, + @HideEmail BIT, + @CipherId UNIQUEIDENTIFIER = NULL, + @Emails NVARCHAR(1024) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Send] + SET + [UserId] = @UserId, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Key] = @Key, + [Password] = @Password, + [MaxAccessCount] = @MaxAccessCount, + [AccessCount] = @AccessCount, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [ExpirationDate] = @ExpirationDate, + [DeletionDate] = @DeletionDate, + [Disabled] = @Disabled, + [HideEmail] = @HideEmail, + [CipherId] = @CipherId, + [Emails] = @Emails + WHERE + [Id] = @Id + + IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + -- TODO: OrganizationId bump? +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Send_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data VARCHAR(MAX), + @Key VARCHAR(MAX), + @Password NVARCHAR(300), + @MaxAccessCount INT, + @AccessCount INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7), + @DeletionDate DATETIME2(7), + @Disabled BIT, + @HideEmail BIT, + @CipherId UNIQUEIDENTIFIER = NULL, + @Emails NVARCHAR(1024) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Send] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Key], + [Password], + [MaxAccessCount], + [AccessCount], + [CreationDate], + [RevisionDate], + [ExpirationDate], + [DeletionDate], + [Disabled], + [HideEmail], + [CipherId], + [Emails] + ) + VALUES + ( + @Id, + @UserId, + @OrganizationId, + @Type, + @Data, + @Key, + @Password, + @MaxAccessCount, + @AccessCount, + @CreationDate, + @RevisionDate, + @ExpirationDate, + @DeletionDate, + @Disabled, + @HideEmail, + @CipherId, + @Emails + ) + + IF @UserId IS NOT NULL + BEGIN + IF @Type = 1 --File + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + -- TODO: OrganizationId bump? +END +GO diff --git a/util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql b/util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql new file mode 100644 index 0000000000..3307f9e535 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql @@ -0,0 +1,67 @@ +IF EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'dbo' + AND TABLE_NAME = 'SsoUser' + AND COLUMN_NAME = 'ExternalId' + AND DATA_TYPE = 'nvarchar' + AND CHARACTER_MAXIMUM_LENGTH < 300 +) +BEGIN + -- Update table ExternalId column size + ALTER TABLE [dbo].[SsoUser] + ALTER COLUMN [ExternalId] NVARCHAR(300) NOT NULL +END +GO + +-- Update stored procedures to handle the new ExternalId column size +CREATE OR ALTER PROCEDURE [dbo].[SsoUser_Create] + @Id BIGINT OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SsoUser] + ( + [UserId], + [OrganizationId], + [ExternalId], + [CreationDate] + ) + VALUES + ( + @UserId, + @OrganizationId, + @ExternalId, + @CreationDate + ) + + SET @Id = SCOPE_IDENTITY(); +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[SsoUser_Update] + @Id BIGINT OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[SsoUser] + SET + [UserId] = @UserId, + [OrganizationId] = @OrganizationId, + [ExternalId] = @ExternalId, + [CreationDate] = @CreationDate + WHERE + [Id] = @Id +END +GO diff --git a/util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql b/util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql new file mode 100644 index 0000000000..3d5cc8dd84 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql @@ -0,0 +1,78 @@ +CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteAttachment] + @Id UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @CurrentAttachments NVARCHAR(MAX) + DECLARE @NewAttachments NVARCHAR(MAX) + + -- Get current cipher data + SELECT + @UserId = [UserId], + @OrganizationId = [OrganizationId], + @CurrentAttachments = [Attachments] + FROM + [dbo].[Cipher] + WHERE [Id] = @Id + + -- If there are no attachments, nothing to do + IF @CurrentAttachments IS NULL + BEGIN + RETURN; + END + + -- Validate the initial JSON + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current initial attachments data is not valid JSON', 1; + RETURN; + END + + -- Check if the attachment exists before trying to remove it + IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL + BEGIN + -- Attachment doesn't exist, nothing to do + RETURN; + END + + -- Create the new attachments JSON with the specified attachment removed + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL) + + -- Validate the resulting JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when removing attachment', 1; + RETURN; + END + + -- Check if we've removed all attachments and have an empty object + IF @NewAttachments = '{}' + BEGIN + -- If we have an empty JSON object, set to NULL instead + SET @NewAttachments = NULL; + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO diff --git a/util/MySqlMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql b/util/MySqlMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql new file mode 100644 index 0000000000..7959d838d3 --- /dev/null +++ b/util/MySqlMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql @@ -0,0 +1,3 @@ +UPDATE Organization +SET UseOrganizationDomains = UseSso +WHERE UseSso = 1 diff --git a/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs new file mode 100644 index 0000000000..5db28d0542 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs @@ -0,0 +1,3112 @@ +// +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("20250429113731_SsoExternalId")] + partial class SsoExternalId + { + /// + 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("UseAdminSponsoredFamilies") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + 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(300) + .HasColumnType("varchar(300)"); + + 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("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250429113731_SsoExternalId.cs b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.cs new file mode 100644 index 0000000000..2e7937b910 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class SsoExternalId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "varchar(300)", + maxLength: 300, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(50)", + oldMaxLength: 50, + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "varchar(50)", + maxLength: 50, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(300)", + oldMaxLength: 300, + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } +} diff --git a/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.Designer.cs b/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.Designer.cs new file mode 100644 index 0000000000..29fcb2e342 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.Designer.cs @@ -0,0 +1,3115 @@ +// +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("20250513151140_AddUseOrganizationDomains")] + partial class AddUseOrganizationDomains + { + /// + 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("UseAdminSponsoredFamilies") + .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("UseOrganizationDomains") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + 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("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250513151140_AddUseOrganizationDomains.cs b/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.cs new file mode 100644 index 0000000000..3f363d5f2c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomains : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseOrganizationDomains", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs b/util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs new file mode 100644 index 0000000000..e5ec2538bb --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs @@ -0,0 +1,23 @@ +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomainsData : Migration +{ + private const string _addUseOrganizationDomainsMigrationScript = "MySqlMigrations.HelperScripts.2025-05-13_00_AddUseOrganizationDomains.sql"; + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_addUseOrganizationDomainsMigrationScript)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration"); + } +} diff --git a/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs new file mode 100644 index 0000000000..b8ea87b21e --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs @@ -0,0 +1,3119 @@ +// +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("20250522205018_2025-05-20_00_AddSendEmails")] + partial class _20250520_00_AddSendEmails + { + /// + 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("UseAdminSponsoredFamilies") + .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("UseOrganizationDomains") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + 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("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + 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("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250522205018_2025-05-20_00_AddSendEmails.cs b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.cs new file mode 100644 index 0000000000..59669b9566 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250520_00_AddSendEmails : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Emails", + table: "Send", + type: "varchar(1024)", + maxLength: 1024, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Emails", + table: "Send"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 8addf3f1dd..48015e2cf4 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -185,6 +185,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("UseKeyConnector") .HasColumnType("tinyint(1)"); + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + b.Property("UsePasswordManager") .HasColumnType("tinyint(1)"); @@ -676,8 +679,8 @@ namespace Bit.MySqlMigrations.Migrations .HasColumnType("datetime(6)"); b.Property("ExternalId") - .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasMaxLength(300) + .HasColumnType("varchar(300)"); b.Property("OrganizationId") .HasColumnType("char(36)"); @@ -1434,6 +1437,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Disabled") .HasColumnType("tinyint(1)"); + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + b.Property("ExpirationDate") .HasColumnType("datetime(6)"); diff --git a/util/MySqlMigrations/MySqlMigrations.csproj b/util/MySqlMigrations/MySqlMigrations.csproj index f6739f5b68..641ad90924 100644 --- a/util/MySqlMigrations/MySqlMigrations.csproj +++ b/util/MySqlMigrations/MySqlMigrations.csproj @@ -1,4 +1,4 @@ - + 9f1cd3e0-70f2-4921-8068-b2538fd7c3f7 @@ -32,5 +32,6 @@ + diff --git a/util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql b/util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql new file mode 100644 index 0000000000..befdf36558 --- /dev/null +++ b/util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql @@ -0,0 +1,3 @@ +UPDATE "Organization" +SET "UseOrganizationDomains" = "UseSso" +WHERE "UseSso" IS true diff --git a/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs new file mode 100644 index 0000000000..253769ad49 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs @@ -0,0 +1,3118 @@ +// +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("20250429113739_SsoExternalId")] + partial class SsoExternalId + { + /// + 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("UseAdminSponsoredFamilies") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + 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("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + 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(300) + .HasColumnType("character varying(300)") + .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("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250429113739_SsoExternalId.cs b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.cs new file mode 100644 index 0000000000..76ffeff061 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class SsoExternalId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "character varying(300)", + maxLength: 300, + nullable: true, + collation: "postgresIndetermanisticCollation", + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50, + oldNullable: true, + oldCollation: "postgresIndetermanisticCollation"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "character varying(50)", + maxLength: 50, + nullable: true, + collation: "postgresIndetermanisticCollation", + oldClrType: typeof(string), + oldType: "character varying(300)", + oldMaxLength: 300, + oldNullable: true, + oldCollation: "postgresIndetermanisticCollation"); + } +} diff --git a/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs new file mode 100644 index 0000000000..895306bb58 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs @@ -0,0 +1,3121 @@ +// +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("20250513151148_AddUseOrganizationDomains")] + partial class AddUseOrganizationDomains + { + /// + 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("UseAdminSponsoredFamilies") + .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("UseOrganizationDomains") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + 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("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + 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("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250513151148_AddUseOrganizationDomains.cs b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.cs new file mode 100644 index 0000000000..130bbd38d8 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomains : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseOrganizationDomains", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs b/util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs new file mode 100644 index 0000000000..d525c8513f --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs @@ -0,0 +1,25 @@ +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomainsData : Migration +{ + + private const string _addUseOrganizationDomainsMigrationScript = "PostgresMigrations.HelperScripts.2025-05-13_00_AddUseOrganizationDomains.psql"; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_addUseOrganizationDomainsMigrationScript)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs new file mode 100644 index 0000000000..9746663eab --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs @@ -0,0 +1,3125 @@ +// +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("20250520201209_2025-05-20_00_AddSendEmails")] + partial class _20250520_00_AddSendEmails + { + /// + 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("UseAdminSponsoredFamilies") + .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("UseOrganizationDomains") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + 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("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + 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("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + 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("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250520201209_2025-05-20_00_AddSendEmails.cs b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.cs new file mode 100644 index 0000000000..d99a2b63d9 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250520_00_AddSendEmails : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Emails", + table: "Send", + type: "character varying(1024)", + maxLength: 1024, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Emails", + table: "Send"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index bd9c99ff80..09f4e910c2 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -187,6 +187,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("UseKeyConnector") .HasColumnType("boolean"); + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + b.Property("UsePasswordManager") .HasColumnType("boolean"); @@ -679,8 +682,8 @@ namespace Bit.PostgresMigrations.Migrations .HasColumnType("timestamp with time zone"); b.Property("ExternalId") - .HasMaxLength(50) - .HasColumnType("character varying(50)") + .HasMaxLength(300) + .HasColumnType("character varying(300)") .UseCollation("postgresIndetermanisticCollation"); b.Property("OrganizationId") @@ -1439,6 +1442,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Disabled") .HasColumnType("boolean"); + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + b.Property("ExpirationDate") .HasColumnType("timestamp with time zone"); diff --git a/util/PostgresMigrations/PostgresMigrations.csproj b/util/PostgresMigrations/PostgresMigrations.csproj index d446f0597a..3496ff67c1 100644 --- a/util/PostgresMigrations/PostgresMigrations.csproj +++ b/util/PostgresMigrations/PostgresMigrations.csproj @@ -27,5 +27,6 @@ + diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs new file mode 100644 index 0000000000..5e5cb17419 --- /dev/null +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -0,0 +1,44 @@ +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class OrganizationSeeder +{ + public static Organization CreateEnterprise(string name, string domain, int seats) + { + return new Organization + { + Id = Guid.NewGuid(), + Name = name, + BillingEmail = $"billing@{domain}", + Plan = "Enterprise (Annually)", + PlanType = PlanType.EnterpriseAnnually, + Seats = seats, + + // Currently hardcoded to the values from https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/client/test_accounts.rs. + // TODO: These should be dynamically generated by the SDK. + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB", + PrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=", + }; + } +} + +public static class OrgnaizationExtensions +{ + public static OrganizationUser CreateOrganizationUser(this Organization organization, User user) + { + return new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + UserId = user.Id, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Type = OrganizationUserType.Admin, + Status = OrganizationUserStatusType.Confirmed + }; + } +} diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs new file mode 100644 index 0000000000..90cadf0b78 --- /dev/null +++ b/util/Seeder/Factories/UserSeeder.cs @@ -0,0 +1,25 @@ +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class UserSeeder +{ + public static User CreateUser(string email) + { + return new User + { + Id = Guid.NewGuid(), + Email = email, + MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==", + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", + PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=", + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 600_000, + }; + } +} diff --git a/util/Seeder/README.md b/util/Seeder/README.md new file mode 100644 index 0000000000..8597ad6e39 --- /dev/null +++ b/util/Seeder/README.md @@ -0,0 +1,18 @@ +# Bitwarden Database Seeder + +A class library for generating and inserting test data. + +## Project Structure + +The project is organized into these main components: + +### Factories + +Factories are helper classes for creating domain entities and populating them with realistic data. This assist in +decreasing the amount of boilerplate code needed to create test data in recipes. + +### Recipes + +Recipes are pre-defined data sets which can be run to generate and load data into the database. They often allow a allow +for a few arguments to customize the data slightly. Recipes should be kept simple and focused on a single task. Default +to creating more recipes rather than adding complexity to existing ones. diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs new file mode 100644 index 0000000000..fb06c091ae --- /dev/null +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -0,0 +1,37 @@ +using Bit.Infrastructure.EntityFramework.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Factories; +using LinqToDB.EntityFrameworkCore; + +namespace Bit.Seeder.Recipes; + +public class OrganizationWithUsersRecipe(DatabaseContext db) +{ + public Guid Seed(string name, int users, string domain) + { + var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); + var user = UserSeeder.CreateUser($"admin@{domain}"); + var orgUser = organization.CreateOrganizationUser(user); + + var additionalUsers = new List(); + var additionalOrgUsers = new List(); + for (var i = 0; i < users; i++) + { + var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); + additionalUsers.Add(additionalUser); + additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); + } + + db.Add(organization); + db.Add(user); + db.Add(orgUser); + + db.SaveChanges(); + + // Use LinqToDB's BulkCopy for significant better performance + db.BulkCopy(additionalUsers); + db.BulkCopy(additionalOrgUsers); + + return organization.Id; + } +} diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj new file mode 100644 index 0000000000..392f6434cc --- /dev/null +++ b/util/Seeder/Seeder.csproj @@ -0,0 +1,29 @@ + + + + + net8.0 + enable + enable + Bit.Seeder + Bit.Seeder + Core library for generating and managing test data for Bitwarden + library + false + + + + + + + + + + + + + + + + + diff --git a/util/Setup/Configuration.cs b/util/Setup/Configuration.cs index 264eef05b2..3372652d03 100644 --- a/util/Setup/Configuration.cs +++ b/util/Setup/Configuration.cs @@ -31,9 +31,6 @@ public class Configuration "Learn more: https://docs.docker.com/compose/compose-file/#ports")] public string HttpsPort { get; set; } = "443"; - [Description("Configure Nginx for Captcha.")] - public bool Captcha { get; set; } = false; - [Description("Configure Nginx for SSL.")] public bool Ssl { get; set; } = true; diff --git a/util/Setup/NginxConfigBuilder.cs b/util/Setup/NginxConfigBuilder.cs index 865b8bdd69..1315ffaba7 100644 --- a/util/Setup/NginxConfigBuilder.cs +++ b/util/Setup/NginxConfigBuilder.cs @@ -73,7 +73,6 @@ public class NginxConfigBuilder public TemplateModel(Context context) { - Captcha = context.Config.Captcha; Ssl = context.Config.Ssl; EnableKeyConnector = context.Config.EnableKeyConnector; EnableScim = context.Config.EnableScim; @@ -127,7 +126,6 @@ public class NginxConfigBuilder } } - public bool Captcha { get; set; } public bool Ssl { get; set; } public bool EnableKeyConnector { get; set; } public bool EnableScim { get; set; } diff --git a/util/Setup/Templates/NginxConfig.hbs b/util/Setup/Templates/NginxConfig.hbs index 115c79c72a..f37987ca70 100644 --- a/util/Setup/Templates/NginxConfig.hbs +++ b/util/Setup/Templates/NginxConfig.hbs @@ -100,16 +100,6 @@ server { proxy_pass http://web:5000/sso-connector.html; } -{{#if Captcha}} - location = /captcha-connector.html { - proxy_pass http://web:5000/captcha-connector.html; - } - - location = /captcha-mobile-connector.html { - proxy_pass http://web:5000/captcha-mobile-connector.html; - } -{{/if}} - location /attachments/ { proxy_pass http://attachments:5000/; } diff --git a/util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql b/util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql new file mode 100644 index 0000000000..1d501246db --- /dev/null +++ b/util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql @@ -0,0 +1,3 @@ +UPDATE [Organization] +SET [UseOrganizationDomains] = [UseSso] +WHERE [UseSso] = 1 diff --git a/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs new file mode 100644 index 0000000000..85e3707d82 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs @@ -0,0 +1,3101 @@ +// +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("20250429113747_SsoExternalId")] + partial class SsoExternalId + { + /// + 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("UseAdminSponsoredFamilies") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + 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("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .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(300) + .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("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250429113747_SsoExternalId.cs b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.cs new file mode 100644 index 0000000000..220c739a59 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class SsoExternalId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs new file mode 100644 index 0000000000..5902f5f9b6 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs @@ -0,0 +1,3104 @@ +// +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("20250513151144_AddUseOrganizationDomains")] + partial class AddUseOrganizationDomains + { + /// + 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("UseAdminSponsoredFamilies") + .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("UseOrganizationDomains") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + 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("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .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("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250513151144_AddUseOrganizationDomains.cs b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.cs new file mode 100644 index 0000000000..50bbec5902 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomains : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseOrganizationDomains", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs b/util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs new file mode 100644 index 0000000000..248b306d97 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs @@ -0,0 +1,25 @@ +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomainsData : Migration +{ + private const string _addUseOrganizationDomainsMigrationScript = "SqliteMigrations.HelperScripts.2025-05-13_00_AddUseOrganizationDomains.sql"; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_addUseOrganizationDomainsMigrationScript)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + throw new Exception("Irreversible migration."); + } +} diff --git a/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs new file mode 100644 index 0000000000..4a3c95d89b --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs @@ -0,0 +1,3108 @@ +// +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("20250520201216_2025-05-20_00_AddSendEmails")] + partial class _20250520_00_AddSendEmails + { + /// + 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("UseAdminSponsoredFamilies") + .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("UseOrganizationDomains") + .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.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + 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("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (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("RequestCountryName") + .HasMaxLength(200) + .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("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .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("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + 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.OrganizationIntegration", 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.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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/20250520201216_2025-05-20_00_AddSendEmails.cs b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.cs new file mode 100644 index 0000000000..a7e9393a2d --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250520_00_AddSendEmails : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Emails", + table: "Send", + type: "TEXT", + maxLength: 1024, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Emails", + table: "Send"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 5e82f311a8..6068941f2b 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -180,6 +180,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("UseKeyConnector") .HasColumnType("INTEGER"); + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + b.Property("UsePasswordManager") .HasColumnType("INTEGER"); @@ -665,7 +668,7 @@ namespace Bit.SqliteMigrations.Migrations .HasColumnType("TEXT"); b.Property("ExternalId") - .HasMaxLength(50) + .HasMaxLength(300) .HasColumnType("TEXT"); b.Property("OrganizationId") @@ -1423,6 +1426,10 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Disabled") .HasColumnType("INTEGER"); + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + b.Property("ExpirationDate") .HasColumnType("TEXT"); diff --git a/util/SqliteMigrations/SqliteMigrations.csproj b/util/SqliteMigrations/SqliteMigrations.csproj index d58498ee7a..dce863036f 100644 --- a/util/SqliteMigrations/SqliteMigrations.csproj +++ b/util/SqliteMigrations/SqliteMigrations.csproj @@ -1,32 +1,33 @@ - - - - enable - enable - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - + + + + enable + enable + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + +