1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-06 11:10:32 -05:00

Merge remote-tracking branch 'origin/make-client-retrieval-more-extensible' into auth/pm-20532/tech-breakdown-poc-token-based-send-authn-and-authz

This commit is contained in:
Jared Snider 2025-05-29 12:38:08 -04:00
commit 086b2d4e01
No known key found for this signature in database
GPG Key ID: A149DDD612516286
628 changed files with 44545 additions and 7323 deletions

11
.github/CODEOWNERS vendored
View File

@ -43,8 +43,16 @@ src/Core/IdentityServer @bitwarden/team-auth-dev
# Key Management team # Key Management team
**/KeyManagement @bitwarden/team-key-management-dev **/KeyManagement @bitwarden/team-key-management-dev
# Tools team
**/Tools @bitwarden/team-tools-dev **/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 team
**/Vault @bitwarden/team-vault-dev **/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 **/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-database.yml @bitwarden/team-platform-dev
.github/workflows/test.yml @bitwarden/team-platform-dev .github/workflows/test.yml @bitwarden/team-platform-dev
**/*Platform* @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) # Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json **/packages.lock.json

View File

@ -15,12 +15,11 @@
matchManagers: ["github-actions"], matchManagers: ["github-actions"],
matchFileNames: [ matchFileNames: [
".github/workflows/publish.yml", ".github/workflows/publish.yml",
".github/workflows/release.yml", ".github/workflows/release.yml"
".github/workflows/repository-management.yml"
], ],
commitMessagePrefix: "[deps] BRE:", commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"], reviewers: ["team:dept-bre"],
addLabels: ["hold"] addLabels: ["hold"],
}, },
{ {
groupName: "dockerfile minor", groupName: "dockerfile minor",
@ -37,6 +36,16 @@
matchManagers: ["github-actions"], matchManagers: ["github-actions"],
matchUpdateTypes: ["minor"], 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"], matchManagers: ["dockerfile", "docker-compose"],
commitMessagePrefix: "[deps] BRE:", commitMessagePrefix: "[deps] BRE:",
@ -59,6 +68,7 @@
"DuoUniversal", "DuoUniversal",
"Fido2.AspNet", "Fido2.AspNet",
"Duende.IdentityServer", "Duende.IdentityServer",
"Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.Extensions.Identity.Stores", "Microsoft.Extensions.Identity.Stores",
"Otp.NET", "Otp.NET",
"Sustainsys.Saml2.AspNetCore2", "Sustainsys.Saml2.AspNetCore2",
@ -79,8 +89,6 @@
"CsvHelper", "CsvHelper",
"Kralizek.AutoFixture.Extensions.MockHttp", "Kralizek.AutoFixture.Extensions.MockHttp",
"Microsoft.AspNetCore.Mvc.Testing", "Microsoft.AspNetCore.Mvc.Testing",
"Microsoft.Extensions.Logging",
"Microsoft.Extensions.Logging.Console",
"Newtonsoft.Json", "Newtonsoft.Json",
"NSubstitute", "NSubstitute",
"Sentry.Serilog", "Sentry.Serilog",
@ -100,9 +108,9 @@
reviewers: ["team:team-billing-dev"], reviewers: ["team:team-billing-dev"],
}, },
{ {
matchPackagePatterns: ["^Microsoft.Extensions.Logging"], matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
groupName: "Microsoft.Extensions.Logging", groupName: "EntityFrameworkCore",
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset", description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
}, },
{ {
matchPackageNames: [ matchPackageNames: [
@ -117,9 +125,6 @@
"Microsoft.EntityFrameworkCore.Relational", "Microsoft.EntityFrameworkCore.Relational",
"Microsoft.EntityFrameworkCore.Sqlite", "Microsoft.EntityFrameworkCore.Sqlite",
"Microsoft.EntityFrameworkCore.SqlServer", "Microsoft.EntityFrameworkCore.SqlServer",
"Microsoft.Extensions.Caching.Cosmos",
"Microsoft.Extensions.Caching.SqlServer",
"Microsoft.Extensions.Caching.StackExchangeRedis",
"Npgsql.EntityFrameworkCore.PostgreSQL", "Npgsql.EntityFrameworkCore.PostgreSQL",
"Pomelo.EntityFrameworkCore.MySql", "Pomelo.EntityFrameworkCore.MySql",
], ],
@ -128,8 +133,8 @@
reviewers: ["team:dept-dbops"], reviewers: ["team:dept-dbops"],
}, },
{ {
matchPackageNames: ["CommandDotNet", "YamlDotNet"], matchPackageNames: ["YamlDotNet"],
description: "DevOps owned dependencies", description: "BRE owned dependencies",
commitMessagePrefix: "[deps] BRE:", commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"], reviewers: ["team:dept-bre"],
}, },
@ -142,56 +147,40 @@
"Azure.Messaging.ServiceBus", "Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs", "Azure.Storage.Blobs",
"Azure.Storage.Queues", "Azure.Storage.Queues",
"Microsoft.AspNetCore.Authentication.JwtBearer", "LaunchDarkly.ServerSdk",
"Microsoft.AspNetCore.Http", "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", "Quartz",
], ],
description: "Platform owned dependencies", description: "Platform owned dependencies",
commitMessagePrefix: "[deps] Platform:", commitMessagePrefix: "[deps] Platform:",
reviewers: ["team:team-platform-dev"], reviewers: ["team:team-platform-dev"],
}, },
{
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
groupName: "EntityFrameworkCore",
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
},
{ {
matchPackageNames: [ matchPackageNames: [
"AutoMapper.Extensions.Microsoft.DependencyInjection", "AutoMapper.Extensions.Microsoft.DependencyInjection",
"AWSSDK.SimpleEmail", "AWSSDK.SimpleEmail",
"AWSSDK.SQS", "AWSSDK.SQS",
"Handlebars.Net", "Handlebars.Net",
"LaunchDarkly.ServerSdk",
"MailKit", "MailKit",
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
"Microsoft.Azure.NotificationHubs", "Microsoft.Azure.NotificationHubs",
"Microsoft.Extensions.Configuration.EnvironmentVariables",
"Microsoft.Extensions.Configuration.UserSecrets",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.DependencyInjection.Abstractions",
"Microsoft.Extensions.DependencyInjection",
"SendGrid", "SendGrid",
], ],
description: "Tools owned dependencies", description: "Tools owned dependencies",
commitMessagePrefix: "[deps] Tools:", commitMessagePrefix: "[deps] Tools:",
reviewers: ["team:team-tools-dev"], 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: [ matchPackageNames: [
"AngleSharp", "AngleSharp",

View File

@ -14,6 +14,7 @@ on:
env: env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io" _AZ_REGISTRY: "bitwardenprod.azurecr.io"
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
jobs: jobs:
lint: lint:
@ -234,12 +235,18 @@ jobs:
- name: Generate Docker image tag - name: Generate Docker image tag
id: tag id: tag
run: | run: |
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only
else else
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
fi 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 if [[ "$IMAGE_TAG" == "main" ]]; then
IMAGE_TAG=dev IMAGE_TAG=dev
fi fi
@ -629,7 +636,9 @@ jobs:
setup-ephemeral-environment: setup-ephemeral-environment:
name: Setup Ephemeral Environment name: Setup Ephemeral Environment
needs: build-docker needs:
- build-artifacts
- build-docker
if: | if: |
needs.build-artifacts.outputs.has_secrets == 'true' needs.build-artifacts.outputs.has_secrets == 'true'
&& github.event_name == 'pull_request' && github.event_name == 'pull_request'

View File

@ -2,7 +2,9 @@ name: Build on PR Target
on: on:
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize, reopened]
branches:
- "main"
defaults: defaults:
run: run:

View File

@ -1,7 +1,10 @@
name: Collect code references name: Collect code references
on: on:
pull_request: push:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
check-ld-secret: check-ld-secret:
@ -37,12 +40,11 @@ jobs:
- name: Collect - name: Collect
id: 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: with:
project-key: default accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
environment-key: dev projKey: default
access-token: ${{ secrets.LD_ACCESS_TOKEN }} allowTags: true
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Add label - name: Add label
if: steps.collect.outputs.any-changed == 'true' if: steps.collect.outputs.any-changed == 'true'

View File

@ -7,8 +7,14 @@ on:
- "main" - "main"
- "rc" - "rc"
- "hotfix-rc" - "hotfix-rc"
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- main
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize, reopened]
branches:
- "main"
jobs: jobs:
check-run: check-run:

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Version>2025.4.3</Version> <Version>2025.5.2</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace> <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>

View File

@ -5,9 +5,6 @@
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank"> <a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank">
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" /> <img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" />
</a> </a>
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<img src="https://img.shields.io/docker/pulls/bitwarden/api.svg" alt="DockerHub" />
</a>
<a href="https://gitter.im/bitwarden/Lobby" target="_blank"> <a href="https://gitter.im/bitwarden/Lobby" target="_blank">
<img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" /> <img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" />
</a> </a>
@ -26,12 +23,12 @@ Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/gett
## Deploy ## Deploy
<p align="center"> <p align="center">
<a href="https://hub.docker.com/u/bitwarden/" target="_blank"> <a href="https://github.com/orgs/bitwarden/packages" target="_blank">
<img src="https://i.imgur.com/SZc8JnH.png" alt="docker" /> <img src="https://i.imgur.com/SZc8JnH.png" alt="docker" />
</a> </a>
</p> </p>
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/ Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/

View File

@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -377,6 +389,8 @@ Global
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{3631BA42-6731-4118-A917-DAA43C5032B9} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

View File

@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; 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.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;

View File

@ -7,13 +7,12 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Providers; namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -23,7 +22,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter; private readonly IStripeAdapter _stripeAdapter;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
@ -31,26 +29,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IAutomaticTaxStrategy _automaticTaxStrategy;
public RemoveOrganizationFromProviderCommand( public RemoveOrganizationFromProviderCommand(
IEventService eventService, IEventService eventService,
IMailService mailService, IMailService mailService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
IFeatureService featureService, IFeatureService featureService,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient, IPricingClient pricingClient)
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
{ {
_eventService = eventService; _eventService = eventService;
_mailService = mailService; _mailService = mailService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationService = organizationService;
_providerOrganizationRepository = providerOrganizationRepository; _providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter; _stripeAdapter = stripeAdapter;
_featureService = featureService; _featureService = featureService;
@ -58,7 +52,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_subscriberService = subscriberService; _subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_automaticTaxStrategy = automaticTaxStrategy;
} }
public async Task RemoveOrganizationFromProvider( public async Task RemoveOrganizationFromProvider(
@ -76,7 +69,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId, providerOrganization.OrganizationId,
Array.Empty<Guid>(), [],
includeProvider: false)) includeProvider: false))
{ {
throw new BadRequestException("Organization must have at least one confirmed owner."); throw new BadRequestException("Organization must have at least one confirmed owner.");
@ -101,7 +94,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
/// <summary> /// <summary>
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled /// 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 /// 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. /// we email the organization owners letting them know they need to add a new payment method.
/// </summary> /// </summary>
private async Task ResetOrganizationBillingAsync( private async Task ResetOrganizationBillingAsync(
@ -141,15 +134,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] 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( await _mailService.SendProviderUpdatePaymentMethod(
organization.Id, organization.Id,
organization.Name, organization.Name,
provider.Name, provider.Name!,
organizationOwnerEmails); organizationOwnerEmails);
} }
} }

View File

@ -5,11 +5,13 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -52,6 +54,7 @@ public class ProviderService : IProviderService
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderBillingService _providerBillingService; private readonly IProviderBillingService _providerBillingService;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
@ -60,7 +63,8 @@ public class ProviderService : IProviderService
IOrganizationRepository organizationRepository, GlobalSettings globalSettings, IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory, IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient) IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,
IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_providerUserRepository = providerUserRepository; _providerUserRepository = providerUserRepository;
@ -80,9 +84,10 @@ public class ProviderService : IProviderService
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_providerBillingService = providerBillingService; _providerBillingService = providerBillingService;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
} }
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
{ {
var owner = await _userService.GetUserByIdAsync(ownerUserId); var owner = await _userService.GetUserByIdAsync(ownerUserId);
if (owner == null) 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."); 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; provider.GatewayCustomerId = customer.Id;
var subscription = await _providerBillingService.SetupSubscription(provider); var subscription = await _providerBillingService.SetupSubscription(provider);
provider.GatewaySubscriptionId = subscription.Id; provider.GatewaySubscriptionId = subscription.Id;
@ -546,12 +564,12 @@ public class ProviderService : IProviderService
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan); ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);
var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup); var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);
var providerOrganization = new ProviderOrganization var providerOrganization = new ProviderOrganization
{ {
ProviderId = providerId, ProviderId = providerId,
OrganizationId = organization.Id, OrganizationId = signUpResponse.Organization.Id,
Key = organizationSignup.OwnerKey, Key = organizationSignup.OwnerKey,
}; };
@ -560,12 +578,12 @@ public class ProviderService : IProviderService
// Give the owner Can Manage access over the default collection // 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 // 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 new CollectionAccessSelection
{ {
Id = defaultCollection.Id, Id = signUpResponse.DefaultCollection.Id,
HidePasswords = false, HidePasswords = false,
ReadOnly = false, ReadOnly = false,
Manage = true Manage = true
@ -573,7 +591,7 @@ public class ProviderService : IProviderService
] ]
: Array.Empty<CollectionAccessSelection>(); : Array.Empty<CollectionAccessSelection>();
await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null, await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null,
new (OrganizationUserInvite, string)[] new (OrganizationUserInvite, string)[]
{ {
( (

View File

@ -1,8 +1,8 @@
using System.Globalization; using System.Globalization;
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Providers.Entities;
using CsvHelper.Configuration.Attributes; using CsvHelper.Configuration.Attributes;
namespace Bit.Commercial.Core.Billing.Models; namespace Bit.Commercial.Core.Billing.Providers.Models;
public class ProviderClientInvoiceReportRow public class ProviderClientInvoiceReportRow
{ {

View File

@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing; using Bit.Core.Billing;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; 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.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging;
using OneOf; using OneOf;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.Billing; namespace Bit.Commercial.Core.Billing.Providers.Services;
[RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)] [RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)]
public class BusinessUnitConverter( public class BusinessUnitConverter(
@ -67,6 +68,7 @@ public class BusinessUnitConverter(
organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb; organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;
organization.UsePolicies = updatedPlan.HasPolicies; organization.UsePolicies = updatedPlan.HasPolicies;
organization.UseSso = updatedPlan.HasSso; organization.UseSso = updatedPlan.HasSso;
organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains;
organization.UseGroups = updatedPlan.HasGroups; organization.UseGroups = updatedPlan.HasGroups;
organization.UseEvents = updatedPlan.HasEvents; organization.UseEvents = updatedPlan.HasEvents;
organization.UseDirectory = updatedPlan.HasDirectory; organization.UseDirectory = updatedPlan.HasDirectory;

View File

@ -1,34 +1,42 @@
using System.Globalization; using System.Globalization;
using Bit.Commercial.Core.Billing.Models; using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing; using Bit.Core.Billing;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; 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;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Services.Implementations.AutomaticTax; using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Braintree;
using CsvHelper; using CsvHelper;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Stripe; 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( public class ProviderBillingService(
IBraintreeGateway braintreeGateway,
IEventService eventService, IEventService eventService,
IFeatureService featureService, IFeatureService featureService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
@ -39,10 +47,10 @@ public class ProviderBillingService(
IProviderOrganizationRepository providerOrganizationRepository, IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository, IProviderPlanRepository providerPlanRepository,
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService, ISubscriberService subscriberService,
ITaxService taxService, ITaxService taxService)
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
: IProviderBillingService : IProviderBillingService
{ {
public async Task AddExistingOrganization( public async Task AddExistingOrganization(
@ -88,6 +96,7 @@ public class ProviderBillingService(
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
organization.UsePolicies = plan.HasPolicies; organization.UsePolicies = plan.HasPolicies;
organization.UseSso = plan.HasSso; organization.UseSso = plan.HasSso;
organization.UseOrganizationDomains = plan.HasOrganizationDomains;
organization.UseGroups = plan.HasGroups; organization.UseGroups = plan.HasGroups;
organization.UseEvents = plan.HasEvents; organization.UseEvents = plan.HasEvents;
organization.UseDirectory = plan.HasDirectory; organization.UseDirectory = plan.HasDirectory;
@ -116,7 +125,7 @@ public class ProviderBillingService(
/* /*
* We have to scale the provider's seats before the ProviderOrganization * 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); await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
@ -224,7 +233,7 @@ public class ProviderBillingService(
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
{ {
Expand = ["tax_ids"] Expand = ["tax", "tax_ids"]
}); });
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault(); 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); var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
organization.GatewayCustomerId = customer.Id; organization.GatewayCustomerId = customer.Id;
@ -463,7 +479,8 @@ public class ProviderBillingService(
public async Task<Customer> SetupCustomer( public async Task<Customer> SetupCustomer(
Provider provider, Provider provider,
TaxInfo taxInfo) TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource = null)
{ {
if (taxInfo is not 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)) if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
{ {
var taxIdType = taxService.GetStripeTaxCode( 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}'.", logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
taxInfo.BillingAddressCountry, taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber); taxInfo.TaxIdNumber);
throw new BadRequestException("billingTaxIdTypeInferenceError"); throw new BadRequestException("billingTaxIdTypeInferenceError");
} }
@ -532,13 +557,97 @@ public class ProviderBillingService(
options.Coupon = provider.DiscountId; 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 try
{ {
return await stripeAdapter.CustomerCreateAsync(options); 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 var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, CollectionMethod = usePaymentMethod ?
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
Customer = customer.Id, Customer = customer.Id,
DaysUntilDue = 30, DaysUntilDue = usePaymentMethod ? null : 30,
Items = subscriptionItemOptionsList, Items = subscriptionItemOptionsList,
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
{ {
{ "providerId", provider.Id.ToString() } { "providerId", provider.Id.ToString() }
}, },
OffSession = true, OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
TrialPeriodDays = trialPeriodDays
}; };
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) var setNonUSBusinessUseToReverseCharge =
{ featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
} if (setNonUSBusinessUseToReverseCharge)
else
{ {
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
else if (customer.HasRecognizedTaxLocation())
{
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = customer.Address.Country == "US" ||
customer.TaxIds.Any()
};
}
try try
{ {
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); 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; return subscription;
} }

View File

@ -6,7 +6,7 @@ using Bit.Core.Billing;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Stripe; using Stripe;
namespace Bit.Commercial.Core.Billing; namespace Bit.Commercial.Core.Billing.Providers.Services;
public static class ProviderPriceAdapter public static class ProviderPriceAdapter
{ {

View File

@ -1,9 +1,10 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Utilities; using Bit.Core.Settings;
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects; namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
@ -11,36 +12,43 @@ public class MaxProjectsQuery : IMaxProjectsQuery
{ {
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IProjectRepository _projectRepository; private readonly IProjectRepository _projectRepository;
private readonly IGlobalSettings _globalSettings;
private readonly IPricingClient _pricingClient;
public MaxProjectsQuery( public MaxProjectsQuery(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IProjectRepository projectRepository) IProjectRepository projectRepository,
IGlobalSettings globalSettings,
IPricingClient pricingClient)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_projectRepository = projectRepository; _projectRepository = projectRepository;
_globalSettings = globalSettings;
_pricingClient = pricingClient;
} }
public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd) 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); var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null) if (org == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122 var plan = await _pricingClient.GetPlan(org.PlanType);
var plan = StaticStore.GetPlan(org.PlanType);
if (plan?.SecretsManager == null) 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));
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);
} }
} }

View File

@ -1,9 +1,9 @@
using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Commercial.Core.AdminConsole.Services; 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.Providers.Interfaces;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Commercial.Core.Utilities; namespace Bit.Commercial.Core.Utilities;

View File

@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Utilities.Commands;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Commands;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -76,9 +76,8 @@ public class PostUserCommand(
var invitedOrganizationUserId = result switch var invitedOrganizationUserId = result switch
{ {
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id, Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null, Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
_ => throw new InvalidOperationException() _ => throw new InvalidOperationException()
}; };

View File

@ -9,17 +9,17 @@
"version": "0.0.0", "version": "0.0.0",
"license": "-", "license": "-",
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1" "jquery": "3.7.1"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
}, },
@ -455,13 +455,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
@ -748,9 +748,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -781,9 +781,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -801,10 +801,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@ -821,9 +821,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -975,9 +975,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.128", "version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1009,9 +1009,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1083,9 +1083,9 @@
} }
}, },
"node_modules/expose-loader": { "node_modules/expose-loader": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1106,13 +1106,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -1248,9 +1241,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1754,16 +1747,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1877,9 +1860,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.85.0", "version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1898,9 +1881,9 @@
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "16.0.4", "version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1939,9 +1922,9 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1959,9 +1942,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2078,9 +2061,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2088,14 +2071,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -2156,9 +2139,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.20.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2193,16 +2176,6 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2211,9 +2184,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2225,14 +2198,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.97.1", "version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@ -2249,9 +2223,9 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },
@ -2352,59 +2326,6 @@
"node": ">=10.13.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -8,17 +8,17 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1" "jquery": "3.7.1"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
} }

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.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.Constants;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -223,31 +225,115 @@ public class RemoveOrganizationFromProviderCommandTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>(); var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(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<SubscriptionCreateOptions>()).Returns(new Subscription stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{ {
Id = "subscription_id" Id = "subscription_id"
}); });
sutProvider.GetDependency<IAutomaticTaxStrategy>() await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options => await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId && options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 && options.DaysUntilDue == 30 &&
options.Metadata["organizationId"] == organization.Id.ToString() && options.AutomaticTax.Enabled == true &&
options.OffSession == true && options.Metadata["organizationId"] == organization.Id.ToString() &&
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && options.OffSession == true &&
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId && options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
options.Items.First().Quantity == organization.Seats) options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
, Arg.Any<Customer>())) options.Items.First().Quantity == organization.Seats));
.Do(x =>
await sutProvider.GetDependency<IProviderBillingService>().Received(1)
.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
org =>
org.BillingEmail == "a@example.com" &&
org.GatewaySubscriptionId == "subscription_id" &&
org.Status == OrganizationStatusType.Created));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
await sutProvider.GetDependency<IMailService>().Received(1)
.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> 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<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
.Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([
"a@example.com",
"b@example.com"
]);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Description == string.Empty &&
options.Email == organization.BillingEmail &&
options.Expand[0] == "tax" &&
options.Expand[1] == "tax_ids")).Returns(new Customer
{ {
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions Id = "customer_id",
Address = new Address
{ {
Enabled = true Country = "US"
}; }
}); });
stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription
{
Id = "subscription_id"
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options => await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>

View File

@ -1,14 +1,17 @@
using Bit.Commercial.Core.AdminConsole.Services; using Bit.Commercial.Core.AdminConsole.Services;
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture; using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -38,7 +41,7 @@ public class ProviderServiceTests
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider) public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
{ {
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default)); () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
Assert.Contains("Invalid owner.", exception.Message); Assert.Contains("Invalid owner.", exception.Message);
} }
@ -50,12 +53,85 @@ public class ProviderServiceTests
userService.GetUserByIdAsync(user.Id).Returns(user); userService.GetUserByIdAsync(user.Id).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>( var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default)); () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
Assert.Contains("Invalid token.", exception.Message); Assert.Contains("Invalid token.", exception.Message);
} }
[Theory, BitAutoData] [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<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().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<BadRequestException>(() =>
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<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
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, [ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider) SutProvider<ProviderService> sutProvider)
{ {
@ -75,7 +151,7 @@ public class ProviderServiceTests
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>(); var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
var customer = new Customer { Id = "customer_id" }; 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" }; var subscription = new Subscription { Id = "subscription_id" };
providerBillingService.SetupSubscription(provider).Returns(subscription); 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)}"); 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<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>( await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
p => p =>
@ -642,8 +718,8 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection())); .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
var providerOrganization = var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
@ -680,8 +756,8 @@ public class ProviderServiceTests
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection())); .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
await Assert.ThrowsAsync<BadRequestException>(() => await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user)); sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
@ -707,8 +783,8 @@ public class ProviderServiceTests
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection())); .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
@ -746,8 +822,8 @@ public class ProviderServiceTests
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup) sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, defaultCollection)); .Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));
var providerOrganization = var providerOrganization =
await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);

View File

@ -1,16 +1,16 @@
#nullable enable #nullable enable
using System.Text; 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;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing; using Bit.Core.Billing;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing; 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.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -25,7 +25,7 @@ using NSubstitute;
using Stripe; using Stripe;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Providers;
public class BusinessUnitConverterTests public class BusinessUnitConverterTests
{ {

View File

@ -1,19 +1,23 @@
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Commercial.Core.Billing.Models; using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; 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;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -24,13 +28,19 @@ using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Braintree;
using CsvHelper; using CsvHelper;
using NSubstitute; using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Stripe; using Stripe;
using Xunit; using Xunit;
using static Bit.Core.Test.Billing.Utilities; 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] [SutProviderCustomize]
public class ProviderBillingServiceTests public class ProviderBillingServiceTests
@ -252,7 +262,7 @@ public class ProviderBillingServiceTests
}; };
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>( sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
options => options.Expand.FirstOrDefault() == "tax_ids")) options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
.Returns(providerCustomer); .Returns(providerCustomer);
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
@ -302,6 +312,91 @@ public class ProviderBillingServiceTests
org => org.GatewayCustomerId == "customer_id")); org => org.GatewayCustomerId == "customer_id"));
} }
[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds(
Provider provider,
Organization organization,
SutProvider<ProviderBillingService> 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<TaxId>
{
Data =
[
new TaxId { Type = "TYPE", Value = "VALUE" }
]
}
};
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
.Returns(providerCustomer);
sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
.Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())
{
CloudRegion = "US"
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
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<IStripeAdapter>().Received(1).CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(
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<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.GatewayCustomerId == "customer_id"));
}
#endregion #endregion
#region GenerateClientInvoiceReport #region GenerateClientInvoiceReport
@ -833,7 +928,7 @@ public class ProviderBillingServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SetupCustomer_Success( public async Task SetupCustomer_NoPaymentMethod_Success(
SutProvider<ProviderBillingService> sutProvider, SutProvider<ProviderBillingService> sutProvider,
Provider provider, Provider provider,
TaxInfo taxInfo) TaxInfo taxInfo)
@ -877,6 +972,357 @@ public class ProviderBillingServiceTests
Assert.Equivalent(expected, actual); Assert.Equivalent(expected, actual);
} }
[Theory, BitAutoData]
public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
sutProvider.GetDependency<IFeatureService>()
.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<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
new SetupIntent { Id = "setup_intent_id" }
]);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(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<StripeException>();
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
await Assert.ThrowsAsync<StripeException>(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
options.CancellationReason == "abandoned"));
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithPayPal_Error_Reverts(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
.Returns("braintree_customer_id");
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(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<StripeException>();
await Assert.ThrowsAsync<StripeException>(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithBankAccount_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
new SetupIntent { Id = "setup_intent_id" }
]);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(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<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithPayPal_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
.Returns("braintree_customer_id");
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(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<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(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<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var expected = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(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] [Theory, BitAutoData]
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
SutProvider<ProviderBillingService> sutProvider, SutProvider<ProviderBillingService> sutProvider,
@ -1002,7 +1448,7 @@ public class ProviderBillingServiceTests
.Returns(new Customer .Returns(new Customer
{ {
Id = "customer_id", Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } Address = new Address { Country = "US" }
}); });
var providerPlans = new List<ProviderPlan> var providerPlans = new List<ProviderPlan>
@ -1044,7 +1490,7 @@ public class ProviderBillingServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task SetupSubscription_Succeeds( public async Task SetupSubscription_SendInvoice_Succeeds(
SutProvider<ProviderBillingService> sutProvider, SutProvider<ProviderBillingService> sutProvider,
Provider provider) Provider provider)
{ {
@ -1054,7 +1500,7 @@ public class ProviderBillingServiceTests
var customer = new Customer var customer = new Customer
{ {
Id = "customer_id", Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } Address = new Address { Country = "US" }
}; };
sutProvider.GetDependency<ISubscriberService>() sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow( .GetCustomerOrThrow(
@ -1094,19 +1540,6 @@ public class ProviderBillingServiceTests
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IAutomaticTaxStrategy>()
.When(x => x.SetCreateOptions(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>( sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub => sub =>
sub.AutomaticTax.Enabled == true && sub.AutomaticTax.Enabled == true &&
@ -1127,6 +1560,350 @@ public class ProviderBillingServiceTests
Assert.Equivalent(expected, actual); Assert.Equivalent(expected, actual);
} }
[Theory, BitAutoData]
public async Task SetupSubscription_ChargeAutomatically_HasCard_Succeeds(
SutProvider<ProviderBillingService> 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<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
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<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
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<ProviderBillingService> 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<string, string>()
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
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<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
const string setupIntentId = "seti_123";
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(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<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
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<ProviderBillingService> 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<string, string>
{
["btCustomerId"] = "braintree_customer_id"
}
};
sutProvider.GetDependency<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
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<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
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<ProviderBillingService> 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<ISubscriberService>()
.GetCustomerOrThrow(
provider,
Arg.Is<CustomerGetOptions>(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer);
var providerPlans = new List<ProviderPlan>
{
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<IPricingClient>().GetPlanOrThrow(plan.PlanType)
.Returns(StaticStore.GetPlan(plan.PlanType));
}
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
.Returns(providerPlans);
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
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 #endregion
#region UpdateSeatMinimums #region UpdateSeatMinimums

View File

@ -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.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Stripe; using Stripe;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Providers;
public class ProviderPriceAdapterTests public class ProviderPriceAdapterTests
{ {

View File

@ -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;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
namespace Bit.Commercial.Core.Test.Billing; namespace Bit.Commercial.Core.Test.Billing.Tax;
[SutProviderCustomize] [SutProviderCustomize]
public class TaxServiceTests public class TaxServiceTests

View File

@ -1,9 +1,12 @@
using Bit.Commercial.Core.SecretsManager.Queries.Projects; using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
@ -15,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
[SutProviderCustomize] [SutProviderCustomize]
public class MaxProjectsQueryTests public class MaxProjectsQueryTests
{ {
[Theory]
[BitAutoData]
public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);
Assert.Null(max);
Assert.Null(overMax);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider, public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
Guid organizationId) Guid organizationId)
{ {
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull(); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1)); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));
@ -28,26 +46,6 @@ public class MaxProjectsQueryTests
.GetProjectCountByOrganizationIdAsync(organizationId); .GetProjectCountByOrganizationIdAsync(organizationId);
} }
[Theory]
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
await sutProvider.GetDependency<IProjectRepository>()
.DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory] [Theory]
[BitAutoData(PlanType.TeamsMonthly2019)] [BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)] [BitAutoData(PlanType.TeamsMonthly2020)]
@ -65,9 +63,14 @@ public class MaxProjectsQueryTests
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType, public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization) SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{ {
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
organization.PlanType = planType; organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);
Assert.Null(limit); Assert.Null(limit);
@ -110,6 +113,9 @@ public class MaxProjectsQueryTests
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id) sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects); .Returns(projects);
sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)
.Returns(StaticStore.GetPlan(organization.PlanType));
var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);
Assert.NotNull(max); Assert.NotNull(max);

View File

@ -11,6 +11,7 @@ MAILCATCHER_PORT=1080
# Alternative databases # Alternative databases
POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123 POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123
MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123 MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
MARIADB_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123
# IdP configuration # IdP configuration
# Complete using the values from the Manage SSO page in the web vault # Complete using the values from the Manage SSO page in the web vault

View File

@ -70,6 +70,20 @@ services:
profiles: profiles:
- mysql - 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: idp:
image: kenchan0130/simplesamlphp:1.19.8 image: kenchan0130/simplesamlphp:1.19.8
container_name: idp container_name: idp
@ -124,8 +138,20 @@ services:
profiles: profiles:
- servicebus - servicebus
redis:
image: redis:alpine
container_name: bw-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
profiles:
- redis
volumes: volumes:
mssql_dev_data: mssql_dev_data:
postgres_dev_data: postgres_dev_data:
mysql_dev_data: mysql_dev_data:
rabbitmq_data: rabbitmq_data:
redis_data:

View File

@ -5,6 +5,7 @@ param(
[switch]$all, [switch]$all,
[switch]$postgres, [switch]$postgres,
[switch]$mysql, [switch]$mysql,
[switch]$mariadb,
[switch]$mssql, [switch]$mssql,
[switch]$sqlite, [switch]$sqlite,
[switch]$selfhost, [switch]$selfhost,
@ -15,11 +16,15 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$currentDir = Get-Location $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; $mssql = $true;
} }
if ($all -or $postgres -or $mysql -or $sqlite) { if ($all -or $(Get-IsEFDatabase)) {
dotnet ef *> $null dotnet ef *> $null
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install" 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 @( Foreach ($item in @(
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0), @($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) { if (!$item[0] -and !$all) {
continue continue

View File

@ -33,6 +33,39 @@
"Name": "events-webhook-subscription" "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"
}
}
}
]
}
]
} }
] ]
} }

View File

@ -4,6 +4,7 @@
"rollForward": "latestFeature" "rollForward": "latestFeature"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.0" "Microsoft.Build.Traversal": "4.1.0",
"Microsoft.Build.Sql": "0.1.9-preview"
} }
} }

View File

@ -40,8 +40,6 @@ export function authenticate(
payload["deviceName"] = "chrome"; payload["deviceName"] = "chrome";
payload["username"] = username; payload["username"] = username;
payload["password"] = password; payload["password"] = password;
params.headers["Auth-Email"] = encoding.b64encode(username);
} else { } else {
payload["scope"] = "api.organization"; payload["scope"] = "api.organization";
payload["grant_type"] = "client_credentials"; payload["grant_type"] = "client_credentials";

View File

@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Models.OrganizationConnectionConfigs;
@ -462,6 +462,7 @@ public class OrganizationsController : Controller
organization.UsersGetPremium = model.UsersGetPremium; organization.UsersGetPremium = model.UsersGetPremium;
organization.UseSecretsManager = model.UseSecretsManager; organization.UseSecretsManager = model.UseSecretsManager;
organization.UseRiskInsights = model.UseRiskInsights; organization.UseRiskInsights = model.UseRiskInsights;
organization.UseOrganizationDomains = model.UseOrganizationDomains;
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies; organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
//secrets //secrets

View File

@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;

View File

@ -102,7 +102,7 @@ public class OrganizationEditModel : OrganizationViewModel
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
SmServiceAccounts = org.SmServiceAccounts; SmServiceAccounts = org.SmServiceAccounts;
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
UseOrganizationDomains = org.UseOrganizationDomains;
_plans = plans; _plans = plans;
} }
@ -186,6 +186,8 @@ public class OrganizationEditModel : OrganizationViewModel
public int? SmServiceAccounts { get; set; } public int? SmServiceAccounts { get; set; }
[Display(Name = "Max Autoscale Machine Accounts")] [Display(Name = "Max Autoscale Machine Accounts")]
public int? MaxAutoscaleSmServiceAccounts { get; set; } public int? MaxAutoscaleSmServiceAccounts { get; set; }
[Display(Name = "Use Organization Domains")]
public bool UseOrganizationDomains { get; set; }
/** /**
* Creates a Plan[] object for use in Javascript * Creates a Plan[] object for use in Javascript
@ -215,6 +217,7 @@ public class OrganizationEditModel : OrganizationViewModel
Has2fa = p.Has2fa, Has2fa = p.Has2fa,
HasApi = p.HasApi, HasApi = p.HasApi,
HasSso = p.HasSso, HasSso = p.HasSso,
HasOrganizationDomains = p.HasOrganizationDomains,
HasKeyConnector = p.HasKeyConnector, HasKeyConnector = p.HasKeyConnector,
HasScim = p.HasScim, HasScim = p.HasScim,
HasResetPassword = p.HasResetPassword, HasResetPassword = p.HasResetPassword,
@ -315,6 +318,7 @@ public class OrganizationEditModel : OrganizationViewModel
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats; existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
existingOrganization.SmServiceAccounts = SmServiceAccounts; existingOrganization.SmServiceAccounts = SmServiceAccounts;
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts; existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
return existingOrganization; return existingOrganization;
} }
} }

View File

@ -44,6 +44,8 @@ public class OrganizationViewModel
orgUsers orgUsers
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus) .Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
.Select(u => u.Email)); .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; SecretsCount = secretsCount;
ProjectsCount = projectCount; ProjectsCount = projectCount;
ServiceAccountsCount = serviceAccountsCount; ServiceAccountsCount = serviceAccountsCount;
@ -70,4 +72,6 @@ public class OrganizationViewModel
public int OccupiedSmSeatsCount { get; set; } public int OccupiedSmSeatsCount { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager; public bool UseSecretsManager => Organization.UseSecretsManager;
public bool UseRiskInsights => Organization.UseRiskInsights; public bool UseRiskInsights => Organization.UseRiskInsights;
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
} }

View File

@ -2,8 +2,8 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;

View File

@ -2,8 +2,8 @@
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Providers.Entities;
namespace Bit.Admin.AdminConsole.Models; namespace Bit.Admin.AdminConsole.Models;
@ -19,7 +19,7 @@ public class ProviderViewModel
{ {
Provider = provider; Provider = provider;
UserCount = providerUsers.Count(); UserCount = providerUsers.Count();
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); ProviderUsers = providerUsers;
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
if (Provider.Type == ProviderType.Msp) if (Provider.Type == ProviderType.Msp)
@ -61,7 +61,7 @@ public class ProviderViewModel
public int UserCount { get; set; } public int UserCount { get; set; }
public Provider Provider { get; set; } public Provider Provider { get; set; }
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; } public IEnumerable<ProviderUserUserDetails> ProviderUsers { get; set; }
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; } public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = []; public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];
} }

View File

@ -19,12 +19,6 @@
<span id="org-confirmed-users" title="Confirmed">@Model.UserConfirmedCount</span>) <span id="org-confirmed-users" title="Confirmed">@Model.UserConfirmedCount</span>)
</dd> </dd>
<dt class="col-sm-4 col-lg-3">Owners</dt>
<dd id="org-owner" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)</dd>
<dt class="col-sm-4 col-lg-3">Admins</dt>
<dd id="org-admins" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)</dd>
<dt class="col-sm-4 col-lg-3">Using 2FA</dt> <dt class="col-sm-4 col-lg-3">Using 2FA</dt>
<dd id="org-2fa" class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd> <dd id="org-2fa" class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd>
@ -76,3 +70,49 @@
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt> <dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
<dd id="sm-seat-count" class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd> <dd id="sm-seat-count" class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd>
</dl> </dl>
<h2>Administrators</h2>
<dl class="row">
<div class="table-responsive">
<div class="col-8">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 190px;">Email</th>
<th style="width: 60px;">Role</th>
<th style="width: 40px;">Status</th>
</tr>
</thead>
<tbody>
@if(!Model.Admins.Any() && !Model.Owners.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var owner in Model.OwnersDetails)
{
<tr>
<td class="align-middle">@owner.Email</td>
<td class="align-middle">Owner</td>
<td class="align-middle">@owner.Status</td>
</tr>
}
@foreach(var admin in Model.AdminsDetails)
{
<tr>
<td class="align-middle">@admin.Email</td>
<td class="align-middle">Admin</td>
<td class="align-middle">@admin.Status</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</dl>

View File

@ -7,7 +7,7 @@
var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite); var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite);
} }
<h2>Provider Admins</h2> <h2>Administrators</h2>
<div class="row"> <div class="row">
<div class="col-8"> <div class="col-8">
<div class="table-responsive"> <div class="table-responsive">
@ -15,12 +15,13 @@
<thead> <thead>
<tr> <tr>
<th style="width: 190px;">Email</th> <th style="width: 190px;">Email</th>
<th style="width: 160px;">Role</th>
<th style="width: 40px;">Status</th> <th style="width: 40px;">Status</th>
<th style="width: 30px;"></th> <th style="width: 30px;"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@if(!Model.ProviderAdmins.Any()) @if(!Model.ProviderUsers.Any())
{ {
<tr> <tr>
<td colspan="6">No results to list.</td> <td colspan="6">No results to list.</td>
@ -28,29 +29,39 @@
} }
else else
{ {
@foreach(var admin in Model.ProviderAdmins) @foreach(var user in Model.ProviderUsers)
{ {
<tr> <tr>
<td class="align-middle"> <td class="align-middle">
@admin.Email @user.Email
</td> </td>
<td class="align-middle"> <td class="align-middle">
@admin.Status @if(@user.Type == 0)
{
<span>Provider Admin</span>
}
else
{
<span>Service User</span>
}
</td>
<td class="align-middle">
@user.Status
</td> </td>
<td> <td>
@if(admin.Status.Equals(ProviderUserStatusType.Confirmed) @if(user.Status.Equals(ProviderUserStatusType.Confirmed)
&& @Model.Provider.Status.Equals(ProviderStatusType.Pending) && @Model.Provider.Status.Equals(ProviderStatusType.Pending)
&& canResendEmailInvite) && canResendEmailInvite)
{ {
@if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString()) @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @user.UserId.Value.ToString())
{ {
<button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button> <button class="btn btn-outline-success btn-sm disabled" disabled>Invite Resent!</button>
} }
else else
{ {
<a class="btn btn-outline-secondary btn-sm" <a class="btn btn-outline-secondary btn-sm"
data-id="@admin.Id" asp-controller="Providers" data-id="@user.Id" asp-controller="Providers"
asp-action="ResendInvite" asp-route-ownerId="@admin.UserId" asp-action="ResendInvite" asp-route-ownerId="@user.UserId"
asp-route-providerId="@Model.Provider.Id"> asp-route-providerId="@Model.Provider.Id">
Resend Setup Invite Resend Setup Invite
</a> </a>

View File

@ -124,6 +124,10 @@
<input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'> <input type="checkbox" class="form-check-input" asp-for="UseSso" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseSso"></label> <label class="form-check-label" asp-for="UseSso"></label>
</div> </div>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseOrganizationDomains" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseOrganizationDomains"></label>
</div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'> <input type="checkbox" class="form-check-input" asp-for="UseKeyConnector" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseKeyConnector"></label> <label class="form-check-label" asp-for="UseKeyConnector"></label>

View File

@ -69,6 +69,7 @@
document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups; document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups;
document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies; document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies;
document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso; 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.UseScim))').checked = plan.hasScim;
document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory; document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory;
document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents; document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents;

View File

@ -7,7 +7,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Utilities;

View File

@ -1,8 +1,8 @@
using Bit.Admin.Billing.Models; using Bit.Admin.Billing.Models;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core.Billing.Migration.Models; using Bit.Core.Billing.Providers.Migration.Models;
using Bit.Core.Billing.Migration.Services; using Bit.Core.Billing.Providers.Migration.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Entities; using Bit.Core.Billing.Providers.Entities;
namespace Bit.Admin.Billing.Models; namespace Bit.Admin.Billing.Models;

View File

@ -1,5 +1,5 @@
@using System.Text.Json @using System.Text.Json
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult @model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult
@{ @{
ViewData["Title"] = "Results"; ViewData["Title"] = "Results";
} }

View File

@ -1,4 +1,4 @@
@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[] @model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[]
@{ @{
ViewData["Title"] = "Results"; ViewData["Title"] = "Results";
} }

View File

@ -4,7 +4,6 @@ using Bit.Admin.Enums;
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -89,7 +88,7 @@ public class UsersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); 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)); return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
} }
@ -106,7 +105,7 @@ public class UsersController : Controller
var billingInfo = await _paymentService.GetBillingAsync(user); var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(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); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
@ -167,7 +166,6 @@ public class UsersController : Controller
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
[RequirePermission(Permission.User_NewDeviceException_Edit)] [RequirePermission(Permission.User_NewDeviceException_Edit)]
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id) public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
{ {
var user = await _userRepository.GetByIdAsync(id); var user = await _userRepository.GetByIdAsync(id);
@ -179,12 +177,4 @@ public class UsersController : Controller
await _userService.ToggleNewDeviceVerificationException(user.Id); await _userService.ToggleNewDeviceVerificationException(user.Id);
return RedirectToAction("Edit", new { id }); return RedirectToAction("Edit", new { id });
} }
// TODO: Feature flag to be removed in PM-14207
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
{
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
: null;
}
} }

View File

@ -17,7 +17,7 @@ public class ChargeBraintreeModel : IValidatableObject
{ {
if (Id != null) 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)) !Guid.TryParse(Id.Substring(1, 32), out var guid))
{ {
yield return new ValidationResult("Customer Id is not a valid format."); yield return new ValidationResult("Customer Id is not a valid format.");

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Migration; using Bit.Core.Billing.Providers.Migration;
#if !OSS #if !OSS
using Bit.Commercial.Core.Utilities; using Bit.Commercial.Core.Utilities;

View File

@ -2,7 +2,7 @@
using Bit.Core; using Bit.Core;
using Bit.Core.Jobs; using Bit.Core.Jobs;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services; using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
using Quartz; using Quartz;
namespace Bit.Admin.Tools.Jobs; namespace Bit.Admin.Tools.Jobs;
@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob
} }
using (var scope = _serviceProvider.CreateScope()) using (var scope = _serviceProvider.CreateScope())
{ {
var sendService = scope.ServiceProvider.GetRequiredService<ISendService>(); var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService<INonAnonymousSendCommand>();
foreach (var send in sends) foreach (var send in sends)
{ {
await sendService.DeleteSendAsync(send); await nonAnonymousSendCommand.DeleteSendAsync(send);
} }
} }
} }

View File

@ -9,18 +9,18 @@
"version": "0.0.0", "version": "0.0.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"toastr": "2.1.4" "toastr": "2.1.4"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
}, },
@ -456,13 +456,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
@ -749,9 +749,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -782,9 +782,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -802,10 +802,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@ -822,9 +822,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -976,9 +976,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.128", "version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1010,9 +1010,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1084,9 +1084,9 @@
} }
}, },
"node_modules/expose-loader": { "node_modules/expose-loader": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz",
"integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1107,13 +1107,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -1249,9 +1242,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1755,16 +1748,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1878,9 +1861,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.85.0", "version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1899,9 +1882,9 @@
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "16.0.4", "version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1940,9 +1923,9 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1960,9 +1943,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2079,9 +2062,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2089,14 +2072,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -2165,9 +2148,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.20.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2202,16 +2185,6 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2220,9 +2193,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2234,14 +2207,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.97.1", "version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@ -2258,9 +2232,9 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },
@ -2361,59 +2335,6 @@
"node": ">=10.13.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -8,18 +8,18 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"toastr": "2.1.4" "toastr": "2.1.4"
}, },
"devDependencies": { "devDependencies": {
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.0", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
} }

View File

@ -9,7 +9,7 @@ namespace Bit.Api.AdminConsole.Authorization;
public static class HttpContextExtensions public static class HttpContextExtensions
{ {
public const string NoOrgIdError = public const string NoOrgIdError =
"A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute."; "A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute.";
/// <summary> /// <summary>
/// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request. /// 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
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="httpContext"></param> /// <param name="httpContext"></param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="InvalidOperationException"></exception> /// <exception cref="InvalidOperationException"></exception>
public static Guid GetOrganizationId(this HttpContext httpContext) public static Guid GetOrganizationId(this HttpContext httpContext)
{ {
httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam); var routeValues = httpContext.GetRouteData().Values;
if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId))
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);
} }
} }

View File

@ -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<bool> AuthorizeAsync(
CurrentContextOrganization? organizationClaims,
Func<Task<bool>> isProviderUserForOrg)
=> organizationClaims switch
{
{ Type: OrganizationUserType.Owner } => true,
{ Type: OrganizationUserType.Admin } => true,
{ Permissions.ManageResetPassword: true } => true,
_ => await isProviderUserForOrg()
};
}

View File

@ -2,13 +2,11 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller
[AllowAnonymous] [AllowAnonymous]
[HttpPost("domain/sso/verified")] [HttpPost("domain/sso/verified")]
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync( public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
[FromBody] OrganizationDomainSsoDetailsRequestModel model) [FromBody] OrganizationDomainSsoDetailsRequestModel model)
{ {

View File

@ -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.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
@ -63,6 +64,7 @@ public class OrganizationUsersController : Controller
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -89,7 +91,8 @@ public class OrganizationUsersController : Controller
IFeatureService featureService, IFeatureService featureService,
IPricingClient pricingClient, IPricingClient pricingClient,
IConfirmOrganizationUserCommand confirmOrganizationUserCommand, IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
IRestoreOrganizationUserCommand restoreOrganizationUserCommand) IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
IInitPendingOrganizationCommand initPendingOrganizationCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -116,6 +119,7 @@ public class OrganizationUsersController : Controller
_pricingClient = pricingClient; _pricingClient = pricingClient;
_confirmOrganizationUserCommand = confirmOrganizationUserCommand; _confirmOrganizationUserCommand = confirmOrganizationUserCommand;
_restoreOrganizationUserCommand = restoreOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand;
_initPendingOrganizationCommand = initPendingOrganizationCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -159,6 +163,12 @@ public class OrganizationUsersController : Controller
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> 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( var authorized = (await _authorizationService.AuthorizeAsync(
User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded;
if (!authorized) if (!authorized)
@ -188,6 +198,37 @@ public class OrganizationUsersController : Controller
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses); return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
} }
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> 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<OrganizationUserUserDetailsResponseModel> GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser,
bool TwoFactorEnabled, bool ClaimedByOrganization)> results)
{
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(results
.Select(result => new OrganizationUserUserDetailsResponseModel(result))
.ToList());
}
[HttpGet("{id}/groups")] [HttpGet("{id}/groups")]
public async Task<IEnumerable<string>> GetGroups(string orgId, string id) public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
{ {
@ -313,7 +354,7 @@ public class OrganizationUsersController : Controller
throw new UnauthorizedAccessException(); 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 _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
} }
@ -575,7 +616,6 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("{id}/delete-account")] [HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")] [HttpPost("{id}/delete-account")]
public async Task DeleteAccount(Guid orgId, Guid id) public async Task DeleteAccount(Guid orgId, Guid id)
@ -594,7 +634,6 @@ public class OrganizationUsersController : Controller
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("delete-account")] [HttpDelete("delete-account")]
[HttpPost("delete-account")] [HttpPost("delete-account")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
@ -719,11 +758,6 @@ public class OrganizationUsersController : Controller
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds) private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
}
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds); var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
return usersOrganizationClaimedStatus; return usersOrganizationClaimedStatus;
} }

View File

@ -25,7 +25,7 @@ using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; 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."); throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
&& (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."); throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
} }

View File

@ -2,7 +2,6 @@
using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@ -79,7 +78,7 @@ public class PoliciesController : Controller
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); 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); return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
} }

View File

@ -2,7 +2,7 @@
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;

View File

@ -84,22 +84,22 @@ public class ProvidersController : Controller
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var taxInfo = model.TaxInfo != null var taxInfo = new TaxInfo
? new TaxInfo {
{ BillingAddressCountry = model.TaxInfo.Country,
BillingAddressCountry = model.TaxInfo.Country, BillingAddressPostalCode = model.TaxInfo.PostalCode,
BillingAddressPostalCode = model.TaxInfo.PostalCode, TaxIdNumber = model.TaxInfo.TaxId,
TaxIdNumber = model.TaxInfo.TaxId, BillingAddressLine1 = model.TaxInfo.Line1,
BillingAddressLine1 = model.TaxInfo.Line1, BillingAddressLine2 = model.TaxInfo.Line2,
BillingAddressLine2 = model.TaxInfo.Line2, BillingAddressCity = model.TaxInfo.City,
BillingAddressCity = model.TaxInfo.City, BillingAddressState = model.TaxInfo.State
BillingAddressState = model.TaxInfo.State };
}
: null; var tokenizedPaymentSource = model.PaymentSource?.ToDomain();
var response = var response =
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key, await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,
taxInfo); taxInfo, tokenizedPaymentSource);
return new ProviderResponseModel(response); return new ProviderResponseModel(response);
} }

View File

@ -2,10 +2,10 @@
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;

View File

@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject
public string InitiationPath { get; set; } public string InitiationPath { get; set; }
public bool SkipTrial { get; set; }
public virtual OrganizationSignup ToOrganizationSignup(User user) public virtual OrganizationSignup ToOrganizationSignup(User user)
{ {
var orgSignup = new OrganizationSignup var orgSignup = new OrganizationSignup
@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
BillingAddressCountry = BillingAddressCountry, BillingAddressCountry = BillingAddressCountry,
}, },
InitiationPath = InitiationPath, InitiationPath = InitiationPath,
SkipTrial = SkipTrial
}; };
Keys?.ToOrganizationSignup(orgSignup); Keys?.ToOrganizationSignup(orgSignup);

View File

@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Integrations;
#nullable enable #nullable enable

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Models.Request; using Bit.Api.Models.Request;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -23,7 +24,9 @@ public class ProviderSetupRequestModel
public string Token { get; set; } public string Token { get; set; }
[Required] [Required]
public string Key { get; set; } public string Key { get; set; }
[Required]
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; } public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
public virtual Provider ToProvider(Provider provider) public virtual Provider ToProvider(Provider provider)
{ {

View File

@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
} }
@ -111,6 +112,7 @@ public class OrganizationResponseModel : ResponseModel
public bool LimitItemDeletion { get; set; } public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
} }

View File

@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel 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, public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails") bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails")
: base(organizationUser, obj) : base(organizationUser, obj)

View File

@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType; ProviderType = organization.ProviderType;
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && IsAdminInitiated = organization.IsAdminInitiated ?? false;
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization); .UsersCanSponsor(organization);
ProductTierType = organization.PlanType.GetProductTier(); ProductTierType = organization.PlanType.GetProductTier();
@ -72,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId); UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
if (organization.SsoConfig != null) if (organization.SsoConfig != null)
@ -135,7 +137,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary> /// <summary>
/// Obsolete. /// Obsolete.
///
/// See <see cref="UserIsClaimedByOrganization"/> /// See <see cref="UserIsClaimedByOrganization"/>
/// </summary> /// </summary>
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")] [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; set => UserIsClaimedByOrganization = value;
} }
/// <summary> /// <summary>
/// Indicates if the organization claims the user. /// Indicates if the user is claimed by the organization.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// 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. /// The organization must be enabled and able to have verified domains.
/// </remarks> /// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsClaimedByOrganization { get; set; } public bool UserIsClaimedByOrganization { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
public bool IsAdminInitiated { get; set; }
} }

View File

@ -50,6 +50,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
LimitItemDeletion = organization.LimitItemDeletion; LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights; UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
} }
} }

View File

@ -76,7 +76,7 @@ public class MembersController : Controller
{ {
return new NotFoundResult(); return new NotFoundResult();
} }
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
collections); collections);
return new JsonResult(response); return new JsonResult(response);
} }
@ -185,7 +185,7 @@ public class MembersController : Controller
{ {
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
response = new MemberResponseModel(existingUserDetails, response = new MemberResponseModel(existingUserDetails,
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations); await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
} }
else else
{ {

View File

@ -4,8 +4,6 @@
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS> <ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -45,6 +46,7 @@ public class AccountsController : Controller
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator; private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
@ -68,6 +70,7 @@ public class AccountsController : Controller
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
IRotateUserKeyCommand rotateUserKeyCommand, IRotateUserKeyCommand rotateUserKeyCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService, IFeatureService featureService,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator, IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator, IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
@ -87,6 +90,7 @@ public class AccountsController : Controller
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_rotateUserKeyCommand = rotateUserKeyCommand; _rotateUserKeyCommand = rotateUserKeyCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService; _featureService = featureService;
_cipherValidator = cipherValidator; _cipherValidator = cipherValidator;
_folderValidator = folderValidator; _folderValidator = folderValidator;
@ -389,7 +393,7 @@ public class AccountsController : Controller
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
ProviderUserStatusType.Confirmed); ProviderUserStatusType.Confirmed);
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
@ -423,7 +427,7 @@ public class AccountsController : Controller
await _userService.SaveUserAsync(model.ToUser(user)); 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 hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
@ -442,7 +446,7 @@ public class AccountsController : Controller
} }
await _userService.SaveUserAsync(model.ToUser(user), true); 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 userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
@ -514,9 +518,8 @@ public class AccountsController : Controller
} }
else else
{ {
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. // Check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{ {
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details."); 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] [AllowAnonymous]
[HttpPost("resend-new-device-otp")] [HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request) public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)

View File

@ -90,6 +90,13 @@ public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessRespons
public class EmergencyAccessTakeoverResponseModel : ResponseModel public class EmergencyAccessTakeoverResponseModel : ResponseModel
{ {
/// <summary>
/// Creates a new instance of the <see cref="EmergencyAccessTakeoverResponseModel"/> class.
/// </summary>
/// <param name="emergencyAccess">Consumed for the Encrypted Key value</param>
/// <param name="grantor">consumed for the KDF configuration</param>
/// <param name="obj">name of the object</param>
/// <exception cref="ArgumentNullException">emergencyAccess cannot be null</exception>
public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj) public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj)
{ {
if (emergencyAccess == null) if (emergencyAccess == null)

View File

@ -1,7 +1,7 @@
#nullable enable #nullable enable
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;

View File

@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
[Route("accounts")] [Route("accounts")]
[Authorize("Application")] [Authorize("Application")]
public class AccountsController( public class AccountsController(
IUserService userService) : Controller IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
{ {
[HttpPost("premium")] [HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremiumAsync( public async Task<PaymentResponseModel> PostPremiumAsync(
@ -56,7 +58,7 @@ public class AccountsController(
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); 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 userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);

View File

@ -1,5 +1,5 @@
using Bit.Core.AdminConsole.Entities; 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.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;

View File

@ -1,12 +1,16 @@
#nullable enable #nullable enable
using System.Diagnostics;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Api.Billing.Queries.Organizations;
using Bit.Core; using Bit.Core;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -24,6 +28,7 @@ public class OrganizationBillingController(
IFeatureService featureService, IFeatureService featureService,
IOrganizationBillingService organizationBillingService, IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationWarningsQuery organizationWarningsQuery,
IPaymentService paymentService, IPaymentService paymentService,
IPricingClient pricingClient, IPricingClient pricingClient,
ISubscriberService subscriberService, ISubscriberService subscriberService,
@ -288,13 +293,20 @@ public class OrganizationBillingController(
sale.Organization.PlanType = plan.Type; sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name; sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true; sale.SubscriptionSetup.SkipTrial = true;
await organizationBillingService.Finalize(sale);
var org = await organizationRepository.GetByIdAsync(organizationId); if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
if (organizationSignup.PaymentMethodType != null)
{ {
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); return Error.BadRequest("A payment method is required to restart the subscription.");
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); }
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation); 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(); return TypedResults.Ok();
@ -335,4 +347,28 @@ public class OrganizationBillingController(
return TypedResults.Ok(providerId); return TypedResults.Ok(providerId);
} }
[HttpGet("warnings")]
public async Task<IResult> 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);
}
} }

View File

@ -1,4 +1,5 @@
using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations; using Bit.Api.Models.Response.Organizations;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
@ -8,6 +9,7 @@ using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.OrganizationSponsorships; using Bit.Core.Models.Api.Request.OrganizationSponsorships;
using Bit.Core.Models.Api.Response.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.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -105,13 +107,16 @@ public class OrganizationSponsorshipsController : Controller
model.FriendlyName, model.FriendlyName,
model.IsAdminInitiated.GetValueOrDefault(), model.IsAdminInitiated.GetValueOrDefault(),
model.Notes); model.Notes);
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); if (sponsorship.OfferedToEmail != null)
{
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
}
} }
[Authorize("Application")] [Authorize("Application")]
[HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)
{ {
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId, var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
PolicyType.FreeFamiliesSponsorshipPolicy); PolicyType.FreeFamiliesSponsorshipPolicy);
@ -124,11 +129,14 @@ public class OrganizationSponsorshipsController : Controller
var sponsoringOrgUser = await _organizationUserRepository var sponsoringOrgUser = await _organizationUserRepository
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
await _organizationRepository.GetByIdAsync(sponsoringOrgId), var filteredSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));
sponsoringOrgUser, if (filteredSponsorship != null)
await _organizationSponsorshipRepository {
.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)); await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
sponsoringOrgUser, filteredSponsorship);
}
} }
[Authorize("Application")] [Authorize("Application")]
@ -214,6 +222,20 @@ public class OrganizationSponsorshipsController : Controller
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); 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")] [Authorize("Application")]
[HttpDelete("sponsored/{sponsoredOrgId}")] [HttpDelete("sponsored/{sponsoredOrgId}")]
[HttpPost("sponsored/{sponsoredOrgId}/remove")] [HttpPost("sponsored/{sponsoredOrgId}/remove")]
@ -246,5 +268,30 @@ public class OrganizationSponsorshipsController : Controller
return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate); return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate);
} }
[Authorize("Application")]
[HttpGet("{sponsoringOrgId}/sponsored")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> 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<OrganizationSponsorshipInvitesResponseModel>(
sponsorships
.Where(s => s.IsAdminInitiated)
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
);
}
private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value);
} }

View File

@ -109,28 +109,6 @@ public class OrganizationsController(
return license; 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")] [HttpPost("{id:guid}/upgrade")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model) public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)

View File

@ -1,11 +1,14 @@
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Pricing; 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.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Models.BitStripe; using Bit.Core.Models.BitStripe;
using Bit.Core.Services; using Bit.Core.Services;
@ -147,13 +150,33 @@ public class ProviderBillingController(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan => var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
{ {
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); 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( return new ConfiguredProviderPlan(
providerPlan.Id, providerPlan.Id,
providerPlan.ProviderId, providerPlan.ProviderId,
plan, plan,
unitAmount,
providerPlan.SeatMinimum ?? 0, providerPlan.SeatMinimum ?? 0,
providerPlan.PurchasedSeats ?? 0, providerPlan.PurchasedSeats ?? 0,
providerPlan.AllocatedSeats ?? 0); providerPlan.AllocatedSeats ?? 0);

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Services;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;

View File

@ -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<IResult> 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<IResult>(
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
badRequest => Error.BadRequest(badRequest.TranslationKey),
unhandled => Error.ServerError(unhandled.TranslationKey));
}
}

View File

@ -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; }
}
}

View File

@ -1,5 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Requests; namespace Bit.Api.Billing.Models.Requests;

View File

@ -12,7 +12,8 @@ public record OrganizationMetadataResponse(
bool IsSubscriptionCanceled, bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate, DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate, DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate) DateTime? SubPeriodEndDate,
int OrganizationOccupiedSeats)
{ {
public static OrganizationMetadataResponse From(OrganizationMetadata metadata) public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
=> new( => new(
@ -25,5 +26,6 @@ public record OrganizationMetadataResponse(
metadata.IsSubscriptionCanceled, metadata.IsSubscriptionCanceled,
metadata.InvoiceDueDate, metadata.InvoiceDueDate,
metadata.InvoiceCreatedDate, metadata.InvoiceCreatedDate,
metadata.SubPeriodEndDate); metadata.SubPeriodEndDate,
metadata.OrganizationOccupiedSeats);
} }

View File

@ -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; }
}
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;

View File

@ -2,6 +2,8 @@
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Providers.Models;
using Bit.Core.Billing.Tax.Models;
using Stripe; using Stripe;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;
@ -34,7 +36,7 @@ public record ProviderSubscriptionResponse(
.Select(providerPlan => .Select(providerPlan =>
{ {
var plan = providerPlan.Plan; 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; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
return new ProviderPlanResponse( return new ProviderPlanResponse(
plan.Name, plan.Name,

View File

@ -1,4 +1,4 @@
using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;

View File

@ -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<OrganizationWarningsResponse> Run(
Organization organization);
}
public class OrganizationWarningsQuery(
ICurrentContext currentContext,
IProviderRepository providerRepository,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : IOrganizationWarningsQuery
{
public async Task<OrganizationWarningsResponse> 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<FreeTrialWarning?> 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<InactiveSubscriptionWarning?> 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<ResellerRenewalWarning?> 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;
}
}

View File

@ -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<IOrganizationWarningsQuery, OrganizationWarningsQuery>();
}
}

View File

@ -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<ActionResult<ICollection<string>>> GetPhishingDomainsAsync()
{
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
{
return NotFound();
}
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync();
return Ok(domains);
}
[HttpGet("checksum")]
public async Task<ActionResult<string>> GetChecksumAsync()
{
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
{
return NotFound();
}
var checksum = await phishingDomainRepository.GetCurrentChecksumAsync();
return Ok(checksum);
}
}

View File

@ -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.Context;
using Bit.Core.Exceptions; 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.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IAuthorizationService _authorizationService;
public SelfHostedOrganizationSponsorshipsController( public SelfHostedOrganizationSponsorshipsController(
ICreateSponsorshipCommand offerSponsorshipCommand, ICreateSponsorshipCommand offerSponsorshipCommand,
@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService IFeatureService featureService,
IAuthorizationService authorizationService
) )
{ {
_offerSponsorshipCommand = offerSponsorshipCommand; _offerSponsorshipCommand = offerSponsorshipCommand;
@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService; _featureService = featureService;
_authorizationService = authorizationService;
} }
[HttpPost("{sponsoringOrgId}/families-for-enterprise")] [HttpPost("{sponsoringOrgId}/families-for-enterprise")]
@ -84,4 +91,41 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); 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<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> 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<OrganizationSponsorshipInvitesResponseModel>(
sponsorships
.Where(s => s.IsAdminInitiated)
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
);
}
} }

View File

@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response;
public class MemberCipherDetailsResponseModel public class MemberCipherDetailsResponseModel
{ {
public Guid? UserGuid { get; set; }
public string UserName { get; set; } public string UserName { get; set; }
public string Email { get; set; } public string Email { get; set; }
public bool UsesKeyConnector { get; set; } public bool UsesKeyConnector { get; set; }
/// <summary> /// <summary>
/// A distinct list of the cipher ids associated with /// A distinct list of the cipher ids associated with
/// the organization member /// the organization member
/// </summary> /// </summary>
public IEnumerable<string> CipherIds { get; set; } public IEnumerable<string> CipherIds { get; set; }
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
{ {
this.UserGuid = memberAccessCipherDetails.UserGuid;
this.UserName = memberAccessCipherDetails.UserName; this.UserName = memberAccessCipherDetails.UserName;
this.Email = memberAccessCipherDetails.Email; this.Email = memberAccessCipherDetails.Email;
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;

Some files were not shown because too many files have changed in this diff Show More