mirror of
https://github.com/bitwarden/server
synced 2026-01-14 06:23:46 +00:00
Merge branch 'main' into tools/pm-21918/send-authentication-commands
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"swashbuckle.aspnetcore.cli": {
|
||||
"version": "7.3.2",
|
||||
"version": "9.0.2",
|
||||
"commands": ["swagger"]
|
||||
},
|
||||
"dotnet-ef": {
|
||||
|
||||
2
.github/workflows/_move_edd_db_scripts.yml
vendored
2
.github/workflows/_move_edd_db_scripts.yml
vendored
@@ -153,7 +153,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Import GPG keys
|
||||
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
|
||||
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
|
||||
81
.github/workflows/build.yml
vendored
81
.github/workflows/build.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
@@ -117,10 +117,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
@@ -166,10 +166,10 @@ jobs:
|
||||
|
||||
########## Set up Docker ##########
|
||||
- name: Set up QEMU emulators
|
||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
|
||||
########## ACRs ##########
|
||||
- name: Log in to Azure
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
|
||||
- name: Build Docker image
|
||||
id: build-artifacts
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||
|
||||
- name: Sign image with Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
@@ -269,18 +269,18 @@ jobs:
|
||||
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
||||
uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0
|
||||
with:
|
||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||
fail-build: false
|
||||
output-format: sarif
|
||||
|
||||
# - name: Upload Grype results to GitHub
|
||||
# uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
# with:
|
||||
# sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
# sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
# ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
- name: Upload Grype results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
@@ -299,7 +299,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -376,62 +376,23 @@ jobs:
|
||||
path: docker-stub-EU.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Public API Swagger
|
||||
- name: Build Swagger files
|
||||
run: |
|
||||
cd ./src/Api
|
||||
echo "Restore tools"
|
||||
dotnet tool restore
|
||||
echo "Publish"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
dotnet swagger tofile --output ../../swagger.json --host https://api.bitwarden.com \
|
||||
./obj/build-output/publish/Api.dll public
|
||||
cd ../..
|
||||
env:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
swaggerGen: "True"
|
||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
cd ./dev
|
||||
pwsh ./generate_openapi_files.ps1
|
||||
|
||||
- name: Upload Public API Swagger artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: swagger.json
|
||||
path: swagger.json
|
||||
path: api.public.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Internal API Swagger
|
||||
run: |
|
||||
cd ./src/Api
|
||||
echo "Restore API tools"
|
||||
dotnet tool restore
|
||||
echo "Publish API"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \
|
||||
./obj/build-output/publish/Api.dll internal
|
||||
|
||||
cd ../Identity
|
||||
|
||||
echo "Restore Identity tools"
|
||||
dotnet tool restore
|
||||
echo "Publish Identity"
|
||||
dotnet publish -c "Release" -o obj/build-output/publish
|
||||
|
||||
dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \
|
||||
./obj/build-output/publish/Identity.dll v1
|
||||
cd ../..
|
||||
env:
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
swaggerGen: "True"
|
||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Internal API Swagger artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: internal.json
|
||||
path: internal.json
|
||||
path: api.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Identity Swagger artifact
|
||||
@@ -464,7 +425,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
|
||||
with:
|
||||
artifacts: "docker-stub-US.zip,
|
||||
docker-stub-EU.zip,
|
||||
|
||||
4
.github/workflows/repository-management.yml
vendored
4
.github/workflows/repository-management.yml
vendored
@@ -82,7 +82,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
|
||||
2
.github/workflows/scan.yml
vendored
2
.github/workflows/scan.yml
vendored
@@ -38,8 +38,6 @@ jobs:
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
with:
|
||||
upload-sarif: false
|
||||
|
||||
quality:
|
||||
name: Sonar
|
||||
|
||||
8
.github/workflows/test-database.yml
vendored
8
.github/workflows/test-database.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
|
||||
- name: Docker Compose down
|
||||
if: always()
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
@@ -58,4 +58,4 @@ jobs:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -129,7 +129,7 @@ publish/
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
@@ -226,3 +226,8 @@ src/Notifications/Notifications.zip
|
||||
bitwarden_license/src/Portal/Portal.zip
|
||||
bitwarden_license/src/Sso/Sso.zip
|
||||
**/src/**/flags.json
|
||||
|
||||
# Generated swagger specs
|
||||
/identity.json
|
||||
/api.json
|
||||
/api.public.json
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.8.0</Version>
|
||||
<Version>2025.8.1</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -133,6 +133,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -337,6 +339,10 @@ Global
|
||||
{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
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -391,6 +397,7 @@ Global
|
||||
{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}
|
||||
{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@@ -137,20 +136,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (customer.HasRecognizedTaxLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = customer.Address.Country == "US" ||
|
||||
customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
|
||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
||||
|
||||
@@ -120,10 +120,7 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException("Both address and postal code are required to set up your provider.");
|
||||
}
|
||||
|
||||
var requireProviderPaymentMethodDuringSetup =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||
|
||||
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not
|
||||
if (tokenizedPaymentSource is not
|
||||
{
|
||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||
Token: not null and not ""
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using Stripe.Tax;
|
||||
|
||||
namespace Bit.Commercial.Core.Billing.Providers.Queries;
|
||||
|
||||
using static StripeConstants;
|
||||
using SuspensionWarning = ProviderWarnings.SuspensionWarning;
|
||||
using TaxIdWarning = ProviderWarnings.TaxIdWarning;
|
||||
|
||||
public class GetProviderWarningsQuery(
|
||||
ICurrentContext currentContext,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IGetProviderWarningsQuery
|
||||
{
|
||||
public async Task<ProviderWarnings?> Run(Provider provider)
|
||||
{
|
||||
var warnings = new ProviderWarnings();
|
||||
|
||||
var subscription =
|
||||
await subscriberService.GetSubscription(provider,
|
||||
new SubscriptionGetOptions { Expand = ["customer.tax_ids"] });
|
||||
|
||||
if (subscription == null)
|
||||
{
|
||||
return warnings;
|
||||
}
|
||||
|
||||
warnings.Suspension = GetSuspensionWarning(provider, subscription);
|
||||
|
||||
warnings.TaxId = await GetTaxIdWarningAsync(provider, subscription.Customer);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private SuspensionWarning? GetSuspensionWarning(
|
||||
Provider provider,
|
||||
Subscription subscription)
|
||||
{
|
||||
if (provider.Enabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return subscription.Status switch
|
||||
{
|
||||
SubscriptionStatus.Unpaid => currentContext.ProviderProviderAdmin(provider.Id)
|
||||
? new SuspensionWarning { Resolution = "add_payment_method", SubscriptionCancelsAt = subscription.CancelAt }
|
||||
: new SuspensionWarning { Resolution = "contact_administrator" },
|
||||
_ => new SuspensionWarning { Resolution = "contact_support" }
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<TaxIdWarning?> GetTaxIdWarningAsync(
|
||||
Provider provider,
|
||||
Customer customer)
|
||||
{
|
||||
if (!currentContext.ProviderProviderAdmin(provider.Id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Potentially DRY this out with the GetOrganizationWarningsQuery
|
||||
|
||||
// Get active and scheduled registrations
|
||||
var registrations = (await Task.WhenAll(
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
|
||||
.SelectMany(registrations => registrations.Data);
|
||||
|
||||
// Find the matching registration for the customer
|
||||
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address.Country);
|
||||
|
||||
// If we're not registered in their country, we don't need a warning
|
||||
if (registration == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var taxId = customer.TaxIds.FirstOrDefault();
|
||||
|
||||
return taxId switch
|
||||
{
|
||||
// Customer's tax ID is missing
|
||||
null => new TaxIdWarning { Type = "tax_id_missing" },
|
||||
// Not sure if this case is valid, but Stripe says this property is nullable
|
||||
not null when taxId.Verification == null => null,
|
||||
// Customer's tax ID is pending verification
|
||||
not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = "tax_id_pending_verification" },
|
||||
// Customer's tax ID failed verification
|
||||
not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = "tax_id_failed_verification" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Globalization;
|
||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@@ -41,7 +40,6 @@ namespace Bit.Commercial.Core.Billing.Providers.Services;
|
||||
public class ProviderBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IEventService eventService,
|
||||
IFeatureService featureService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<ProviderBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -284,9 +282,7 @@ public class ProviderBillingService(
|
||||
]
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" })
|
||||
if (providerCustomer.Address is not { Country: "US" })
|
||||
{
|
||||
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
@@ -483,8 +479,10 @@ public class ProviderBillingService(
|
||||
public async Task<Customer> SetupCustomer(
|
||||
Provider provider,
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource = null)
|
||||
TokenizedPaymentSource tokenizedPaymentSource)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tokenizedPaymentSource);
|
||||
|
||||
if (taxInfo is not
|
||||
{
|
||||
BillingAddressCountry: not null and not "",
|
||||
@@ -527,9 +525,7 @@ public class ProviderBillingService(
|
||||
}
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US")
|
||||
if (taxInfo.BillingAddressCountry is not "US")
|
||||
{
|
||||
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
@@ -569,56 +565,50 @@ public class ProviderBillingService(
|
||||
options.Coupon = provider.DiscountId;
|
||||
}
|
||||
|
||||
var requireProviderPaymentMethodDuringSetup =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||
|
||||
var braintreeCustomerId = "";
|
||||
|
||||
if (requireProviderPaymentMethodDuringSetup)
|
||||
if (tokenizedPaymentSource is not
|
||||
{
|
||||
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
|
||||
Token: not null and not ""
|
||||
})
|
||||
{
|
||||
if (tokenizedPaymentSource is not
|
||||
logger.LogError("Cannot create customer for provider ({ProviderID}) with invalid payment method", provider.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var (type, token) = tokenizedPaymentSource;
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
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 setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
||||
.FirstOrDefault();
|
||||
|
||||
var (type, token) = tokenizedPaymentSource;
|
||||
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (type)
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
if (setupIntent == null)
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
|
||||
.FirstOrDefault();
|
||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -640,25 +630,22 @@ public class ProviderBillingService(
|
||||
|
||||
async Task Revert()
|
||||
{
|
||||
if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null)
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (tokenizedPaymentSource.Type)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,9 +688,6 @@ public class ProviderBillingService(
|
||||
});
|
||||
}
|
||||
|
||||
var requireProviderPaymentMethodDuringSetup =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup);
|
||||
|
||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||
|
||||
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||
@@ -714,10 +698,9 @@ public class ProviderBillingService(
|
||||
: null;
|
||||
|
||||
var usePaymentMethod =
|
||||
requireProviderPaymentMethodDuringSetup &&
|
||||
(!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||
customer.Metadata.ContainsKey(BraintreeCustomerIdKey) ||
|
||||
setupIntent.IsUnverifiedBankAccount());
|
||||
!string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||
|
||||
(customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true) ||
|
||||
(setupIntent?.IsUnverifiedBankAccount() == true);
|
||||
|
||||
int? trialPeriodDays = provider.Type switch
|
||||
{
|
||||
@@ -742,21 +725,8 @@ public class ProviderBillingService(
|
||||
TrialPeriodDays = trialPeriodDays
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (customer.HasRecognizedTaxLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = customer.Address.Country == "US" ||
|
||||
customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Billing.Providers.Queries;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -17,5 +19,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();
|
||||
services.AddTransient<IProviderBillingService, ProviderBillingService>();
|
||||
services.AddTransient<IBusinessUnitConverter, BusinessUnitConverter>();
|
||||
services.AddTransient<IGetProviderWarningsQuery, GetProviderWarningsQuery>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
@@ -45,6 +45,19 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secrets = await dbContext.Secret
|
||||
.Where(c => ids.Contains(c.Id) && c.DeletedDate != null)
|
||||
.Include(c => c.Projects)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
@@ -66,10 +79,14 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var query = dbContext.Secret
|
||||
.Include(c => c.Projects)
|
||||
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
|
||||
|
||||
@@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Stripe;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Context;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -23,10 +23,10 @@ using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -10,9 +10,9 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Infrastructure;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Providers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@@ -332,9 +331,6 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
Id = "subscription_id"
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true);
|
||||
|
||||
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
|
||||
|
||||
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Bit.Commercial.Core.AdminConsole.Services;
|
||||
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@@ -120,8 +119,6 @@ public class ProviderServiceTests
|
||||
|
||||
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 };
|
||||
|
||||
@@ -1194,7 +1191,7 @@ public class ProviderServiceTests
|
||||
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||
new()
|
||||
{
|
||||
Items = new List<Stripe.SubscriptionItemOptions>
|
||||
Items = new List<SubscriptionItemOptions>
|
||||
{
|
||||
new() { Id = subscriptionItem.Id, Price = expectedPlanId },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
using Bit.Commercial.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Stripe;
|
||||
using Stripe.Tax;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing.Providers.Queries;
|
||||
|
||||
using static StripeConstants;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetProviderWarningsQueryTests
|
||||
{
|
||||
private static readonly string[] _requiredExpansions = ["customer.tax_ids"];
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoSubscription_NoWarnings(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.ReturnsNull();
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension: null,
|
||||
TaxId: null
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_ProviderEnabled_NoSuspensionWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.Suspension);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_SuspensionWarning_AddPaymentMethod(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
var cancelAt = DateTime.UtcNow.AddDays(7);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
CancelAt = cancelAt,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "add_payment_method"
|
||||
});
|
||||
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_SuspensionWarning_ContactAdministrator(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "contact_administrator"
|
||||
});
|
||||
Assert.Null(response.Suspension.SubscriptionCancelsAt);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_SuspensionWarning_ContactSupport(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Canceled,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "contact_support"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NotProviderAdmin_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoTaxRegistrationForCountry_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "CA" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdMissingWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_TaxIdVerificationIsNull_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId { Verification = null }]
|
||||
},
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdPendingVerificationWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Pending
|
||||
}
|
||||
}]
|
||||
},
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_pending_verification"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_Has_TaxIdFailedVerificationWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Unverified
|
||||
}
|
||||
}]
|
||||
},
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_failed_verification"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_TaxIdVerified_NoTaxIdWarning(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId>
|
||||
{
|
||||
Data = [new TaxId
|
||||
{
|
||||
Verification = new TaxIdVerification
|
||||
{
|
||||
Status = TaxIdVerificationStatus.Verified
|
||||
}
|
||||
}]
|
||||
},
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.Null(response!.TaxId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_MultipleRegistrations_MatchesCorrectCountry(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = true;
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Active,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "DE" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [
|
||||
new Registration { Country = "US" },
|
||||
new Registration { Country = "DE" },
|
||||
new Registration { Country = "FR" }
|
||||
]
|
||||
});
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))
|
||||
.Returns(new StripeList<Registration> { Data = [] });
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_CombinesBothWarningTypes(
|
||||
Provider provider,
|
||||
SutProvider<GetProviderWarningsQuery> sutProvider)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
var cancelAt = DateTime.UtcNow.AddDays(5);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
|
||||
options.Expand.SequenceEqual(_requiredExpansions)
|
||||
))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Status = SubscriptionStatus.Unpaid,
|
||||
CancelAt = cancelAt,
|
||||
Customer = new Customer
|
||||
{
|
||||
TaxIds = new StripeList<TaxId> { Data = [] },
|
||||
Address = new Address { Country = "US" }
|
||||
}
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
|
||||
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
|
||||
.Returns(new StripeList<Registration>
|
||||
{
|
||||
Data = [new Registration { Country = "US" }]
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Run(provider);
|
||||
|
||||
Assert.True(response is
|
||||
{
|
||||
Suspension.Resolution: "add_payment_method",
|
||||
TaxId.Type: "tax_id_missing"
|
||||
});
|
||||
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Net;
|
||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@@ -352,9 +351,6 @@ public class ProviderBillingServiceTests
|
||||
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 &&
|
||||
@@ -901,11 +897,12 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_MissingCountry_ContactSupport(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource)
|
||||
{
|
||||
taxInfo.BillingAddressCountry = null;
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
@@ -916,60 +913,27 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_MissingPostalCode_ContactSupport(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource)
|
||||
{
|
||||
taxInfo.BillingAddressCountry = null;
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_NoPaymentMethod_Success(
|
||||
public async Task SetupCustomer_NullPaymentSource_ThrowsArgumentNullException(
|
||||
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 }
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
Assert.Equivalent(expected, actual);
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() =>
|
||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, null));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -989,8 +953,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true);
|
||||
|
||||
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
|
||||
|
||||
@@ -1018,8 +980,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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([
|
||||
@@ -1075,8 +1035,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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");
|
||||
@@ -1130,8 +1088,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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([
|
||||
@@ -1187,8 +1143,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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");
|
||||
@@ -1241,8 +1195,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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 &&
|
||||
@@ -1293,11 +1245,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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 &&
|
||||
@@ -1327,7 +1274,8 @@ public class ProviderBillingServiceTests
|
||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
@@ -1340,7 +1288,7 @@ public class ProviderBillingServiceTests
|
||||
.Returns((string)null);
|
||||
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
|
||||
Assert.IsType<BadRequestException>(actual);
|
||||
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
|
||||
@@ -1616,8 +1564,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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 =>
|
||||
@@ -1694,8 +1640,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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";
|
||||
|
||||
@@ -1797,8 +1741,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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 =>
|
||||
@@ -1877,11 +1819,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
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 =>
|
||||
19
dev/generate_openapi_files.ps1
Normal file
19
dev/generate_openapi_files.ps1
Normal file
@@ -0,0 +1,19 @@
|
||||
Set-Location "$PSScriptRoot/.."
|
||||
|
||||
$env:ASPNETCORE_ENVIRONMENT = "Development"
|
||||
$env:swaggerGen = "True"
|
||||
$env:DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX = "2"
|
||||
$env:GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING = "placeholder"
|
||||
|
||||
dotnet tool restore
|
||||
|
||||
# Identity
|
||||
Set-Location "./src/Identity"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1"
|
||||
|
||||
# Api internal & public
|
||||
Set-Location "../../src/Api"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
|
||||
@@ -22,6 +22,7 @@ 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.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -53,6 +54,7 @@ public class ProvidersController : Controller
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
@@ -73,7 +75,8 @@ public class ProvidersController : Controller
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IAccessControlService accessControlService)
|
||||
IAccessControlService accessControlService,
|
||||
ISubscriberService subscriberService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;
|
||||
@@ -93,6 +96,7 @@ public class ProvidersController : Controller
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
_accessControlService = accessControlService;
|
||||
_subscriberService = subscriberService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Provider_List_View)]
|
||||
@@ -299,6 +303,23 @@ public class ProvidersController : Controller
|
||||
|
||||
model.ToProvider(provider);
|
||||
|
||||
// validate the stripe ids to prevent saving a bad one
|
||||
if (provider.IsBillable())
|
||||
{
|
||||
if (!await _subscriberService.IsValidGatewayCustomerIdAsync(provider))
|
||||
{
|
||||
var oldModel = await GetEditModel(id);
|
||||
ModelState.AddModelError(nameof(model.GatewayCustomerId), $"Invalid Gateway Customer Id: {model.GatewayCustomerId}");
|
||||
return View(oldModel);
|
||||
}
|
||||
if (!await _subscriberService.IsValidGatewaySubscriptionIdAsync(provider))
|
||||
{
|
||||
var oldModel = await GetEditModel(id);
|
||||
ModelState.AddModelError(nameof(model.GatewaySubscriptionId), $"Invalid Gateway Subscription Id: {model.GatewaySubscriptionId}");
|
||||
return View(oldModel);
|
||||
}
|
||||
}
|
||||
|
||||
provider.Enabled = _accessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox)
|
||||
? model.Enabled : originalProviderStatus;
|
||||
|
||||
@@ -382,10 +403,8 @@ public class ProvidersController : Controller
|
||||
}
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
var payByInvoice =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
||||
(await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice();
|
||||
var payByInvoice = _featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
||||
((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false);
|
||||
|
||||
return new ProviderEditModel(
|
||||
provider, users, providerOrganizations,
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
@@ -87,14 +88,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||
existingProvider.Enabled = Enabled;
|
||||
switch (Type)
|
||||
if (Type.IsStripeSupported())
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
break;
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
}
|
||||
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -25,6 +28,8 @@ public class EventsController : Controller
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IEventRepository _eventRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public EventsController(
|
||||
IUserService userService,
|
||||
@@ -32,7 +37,9 @@ public class EventsController : Controller
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IEventRepository eventRepository,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
ISecretRepository secretRepository,
|
||||
IProjectRepository projectRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_cipherRepository = cipherRepository;
|
||||
@@ -40,6 +47,8 @@ public class EventsController : Controller
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_eventRepository = eventRepository;
|
||||
_currentContext = currentContext;
|
||||
_secretRepository = secretRepository;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@@ -104,6 +113,77 @@ public class EventsController : Controller
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[HttpGet("~/organization/{orgId}/secrets/{id}/events")]
|
||||
public async Task<ListResponseModel<EventResponseModel>> GetSecrets(
|
||||
Guid id, Guid orgId,
|
||||
[FromQuery] DateTime? start = null,
|
||||
[FromQuery] DateTime? end = null,
|
||||
[FromQuery] string continuationToken = null)
|
||||
{
|
||||
if (id == Guid.Empty || orgId == Guid.Empty)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(id);
|
||||
var orgIdForVerification = secret?.OrganizationId ?? orgId;
|
||||
var secretOrg = _currentContext.GetOrganization(orgIdForVerification);
|
||||
|
||||
if (secretOrg == null || !await _currentContext.AccessEventLogs(secretOrg.Id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
bool canViewLogs = false;
|
||||
|
||||
if (secret == null)
|
||||
{
|
||||
secret = new Core.SecretsManager.Entities.Secret { Id = id, OrganizationId = orgId };
|
||||
canViewLogs = secretOrg.Type is Core.Enums.OrganizationUserType.Admin or Core.Enums.OrganizationUserType.Owner;
|
||||
}
|
||||
else
|
||||
{
|
||||
canViewLogs = await CanViewSecretsLogs(secret);
|
||||
}
|
||||
|
||||
if (!canViewLogs)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
|
||||
var result = await _eventRepository.GetManyBySecretAsync(secret, fromDate, toDate, new PageOptions { ContinuationToken = continuationToken });
|
||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[HttpGet("~/organization/{orgId}/projects/{id}/events")]
|
||||
public async Task<ListResponseModel<EventResponseModel>> GetProjects(
|
||||
Guid id,
|
||||
Guid orgId,
|
||||
[FromQuery] DateTime? start = null,
|
||||
[FromQuery] DateTime? end = null,
|
||||
[FromQuery] string continuationToken = null)
|
||||
{
|
||||
if (id == Guid.Empty || orgId == Guid.Empty)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var project = await GetProject(id, orgId);
|
||||
await ValidateOrganization(project);
|
||||
|
||||
var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
|
||||
var result = await _eventRepository.GetManyByProjectAsync(
|
||||
project,
|
||||
fromDate,
|
||||
toDate,
|
||||
new PageOptions { ContinuationToken = continuationToken });
|
||||
|
||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[HttpGet("~/organizations/{orgId}/users/{id}/events")]
|
||||
public async Task<ListResponseModel<EventResponseModel>> GetOrganizationUser(string orgId, string id,
|
||||
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)
|
||||
@@ -157,4 +237,48 @@ public class EventsController : Controller
|
||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
private async Task ValidateOrganization(Project project)
|
||||
{
|
||||
var org = _currentContext.GetOrganization(project.OrganizationId);
|
||||
|
||||
if (org == null || !await _currentContext.AccessEventLogs(org.Id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
private async Task<Project> GetProject(Guid projectGuid, Guid orgGuid)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(projectGuid);
|
||||
if (project != null)
|
||||
{
|
||||
return project;
|
||||
}
|
||||
|
||||
var fallbackProject = new Project
|
||||
{
|
||||
Id = projectGuid,
|
||||
OrganizationId = orgGuid
|
||||
};
|
||||
|
||||
return fallbackProject;
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
private async Task<bool> CanViewSecretsLogs(Secret secret)
|
||||
{
|
||||
if (!_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
var isAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);
|
||||
var access = await _secretRepository.AccessToSecretAsync(secret.Id, userId, accessClient);
|
||||
return access.Read;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
@@ -235,8 +236,7 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() ||
|
||||
model.BillingEmail != organization.BillingEmail);
|
||||
var updateBilling = ShouldUpdateBilling(model, organization);
|
||||
|
||||
var hasRequiredPermissions = updateBilling
|
||||
? await _currentContext.EditSubscription(orgIdGuid)
|
||||
@@ -582,4 +582,11 @@ public class OrganizationsController : Controller
|
||||
|
||||
return organization.PlanType;
|
||||
}
|
||||
|
||||
private bool ShouldUpdateBilling(OrganizationUpdateRequestModel model, Organization organization)
|
||||
{
|
||||
var organizationNameChanged = model.Name != organization.Name;
|
||||
var billingEmailChanged = model.BillingEmail != organization.BillingEmail;
|
||||
return !_globalSettings.SelfHosted && (organizationNameChanged || billingEmailChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public class EventResponseModel : ResponseModel
|
||||
SystemUser = ev.SystemUser;
|
||||
DomainName = ev.DomainName;
|
||||
SecretId = ev.SecretId;
|
||||
ProjectId = ev.ProjectId;
|
||||
ServiceAccountId = ev.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -55,5 +56,6 @@ public class EventResponseModel : ResponseModel
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -57,25 +56,13 @@ public class OrganizationController : Controller
|
||||
throw new BadRequestException("You cannot import this much data at once.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor))
|
||||
{
|
||||
await _importOrganizationUsersAndGroupsCommand.ImportAsync(
|
||||
await _importOrganizationUsersAndGroupsCommand.ImportAsync(
|
||||
_currentContext.OrganizationId.Value,
|
||||
model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)),
|
||||
model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),
|
||||
model.Members.Where(u => u.Deleted).Select(u => u.ExternalId),
|
||||
model.OverwriteExisting.GetValueOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
await _organizationService.ImportAsync(
|
||||
_currentContext.OrganizationId.Value,
|
||||
model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)),
|
||||
model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),
|
||||
model.Members.Where(u => u.Deleted).Select(u => u.ExternalId),
|
||||
model.OverwriteExisting.GetValueOrDefault(),
|
||||
Core.Enums.EventSystemUser.PublicApi);
|
||||
}
|
||||
model.OverwriteExisting.GetValueOrDefault()
|
||||
);
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class EventResponseModel : IResponseModel
|
||||
IpAddress = ev.IpAddress;
|
||||
InstallationId = ev.InstallationId;
|
||||
SecretId = ev.SecretId;
|
||||
ProjectId = ev.ProjectId;
|
||||
ServiceAccountId = ev.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -97,6 +98,11 @@ public class EventResponseModel : IResponseModel
|
||||
/// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>
|
||||
public Guid? SecretId { get; set; }
|
||||
/// <summary>
|
||||
/// The unique identifier of the related project that the event describes.
|
||||
/// </summary>
|
||||
/// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>
|
||||
public Guid? ProjectId { get; set; }
|
||||
/// <summary>
|
||||
/// The unique identifier of the related service account that the event describes.
|
||||
/// </summary>
|
||||
/// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,7 +6,6 @@ using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Billing.Organizations.Services;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
@@ -28,7 +27,6 @@ public class OrganizationBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||
IPaymentService paymentService,
|
||||
IPricingClient pricingClient,
|
||||
ISubscriberService subscriberService,
|
||||
@@ -359,31 +357,6 @@ public class OrganizationBillingController(
|
||||
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 warnings = await getOrganizationWarningsQuery.Run(organization);
|
||||
|
||||
return TypedResults.Ok(warnings);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("change-frequency")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> ChangePlanSubscriptionFrequencyAsync(
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
@@ -27,7 +26,6 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ProviderBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IPricingClient pricingClient,
|
||||
IProviderBillingService providerBillingService,
|
||||
@@ -139,27 +137,15 @@ public class ProviderBillingController(
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
|
||||
var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe);
|
||||
|
||||
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
||||
{
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);
|
||||
var price = await stripeAdapter.PriceGetAsync(priceId);
|
||||
|
||||
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;
|
||||
}
|
||||
var unitAmount = price.UnitAmountDecimal.HasValue
|
||||
? price.UnitAmountDecimal.Value / 100M
|
||||
: plan.PasswordManager.ProviderPortalSeatPrice;
|
||||
|
||||
return new ConfiguredProviderPlan(
|
||||
providerPlan.Id,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Api.Billing.Models.Requirements;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -21,6 +22,7 @@ public class OrganizationBillingVNextController(
|
||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||
IGetBillingAddressQuery getBillingAddressQuery,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
||||
@@ -104,4 +106,14 @@ public class OrganizationBillingVNextController(
|
||||
var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
[HttpGet("warnings")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetWarningsAsync(
|
||||
[BindNever] Organization organization)
|
||||
{
|
||||
var warnings = await getOrganizationWarningsQuery.Run(organization);
|
||||
return TypedResults.Ok(warnings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Providers.Queries;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
@@ -19,6 +20,7 @@ public class ProviderBillingVNextController(
|
||||
IGetBillingAddressQuery getBillingAddressQuery,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IGetProviderWarningsQuery getProviderWarningsQuery,
|
||||
IProviderService providerService,
|
||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
||||
@@ -104,4 +106,13 @@ public class ProviderBillingVNextController(
|
||||
var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpGet("warnings")]
|
||||
[InjectProvider(ProviderUserType.ServiceUser)]
|
||||
public async Task<IResult> GetWarningsAsync(
|
||||
[BindNever] Provider provider)
|
||||
{
|
||||
var warnings = await getProviderWarningsQuery.Run(provider);
|
||||
return TypedResults.Ok(warnings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CollectionRequestModel model)
|
||||
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CreateCollectionRequestModel model)
|
||||
{
|
||||
var collection = model.ToCollection(orgId);
|
||||
|
||||
@@ -174,7 +174,7 @@ public class CollectionsController : Controller
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
|
||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
|
||||
|
||||
@@ -7,7 +7,7 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Models.Request;
|
||||
|
||||
public class CollectionRequestModel
|
||||
public class CreateCollectionRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
@@ -40,7 +40,7 @@ public class CollectionBulkDeleteRequestModel
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
public class CollectionWithIdRequestModel : CollectionRequestModel
|
||||
public class CollectionWithIdRequestModel : CreateCollectionRequestModel
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
@@ -50,3 +50,21 @@ public class CollectionWithIdRequestModel : CollectionRequestModel
|
||||
return base.ToCollection(existingCollection);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateCollectionRequestModel : CreateCollectionRequestModel
|
||||
{
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public new string Name { get; set; }
|
||||
|
||||
public override Collection ToCollection(Collection existingCollection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(existingCollection.DefaultUserCollectionEmail) && !string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
existingCollection.Name = Name;
|
||||
}
|
||||
existingCollection.ExternalId = ExternalId;
|
||||
return existingCollection;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public class CollectionDetailsResponseModel : CollectionResponseModel
|
||||
ReadOnly = collectionDetails.ReadOnly;
|
||||
HidePasswords = collectionDetails.HidePasswords;
|
||||
Manage = collectionDetails.Manage;
|
||||
DefaultUserCollectionEmail = collectionDetails.DefaultUserCollectionEmail;
|
||||
}
|
||||
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
@@ -29,6 +30,7 @@ public class ProjectsController : Controller
|
||||
private readonly IUpdateProjectCommand _updateProjectCommand;
|
||||
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public ProjectsController(
|
||||
ICurrentContext currentContext,
|
||||
@@ -38,7 +40,8 @@ public class ProjectsController : Controller
|
||||
ICreateProjectCommand createProjectCommand,
|
||||
IUpdateProjectCommand updateProjectCommand,
|
||||
IDeleteProjectCommand deleteProjectCommand,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IEventService eventService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_userService = userService;
|
||||
@@ -48,6 +51,7 @@ public class ProjectsController : Controller
|
||||
_updateProjectCommand = updateProjectCommand;
|
||||
_deleteProjectCommand = deleteProjectCommand;
|
||||
_authorizationService = authorizationService;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
[HttpGet("organizations/{organizationId}/projects")]
|
||||
@@ -89,6 +93,11 @@ public class ProjectsController : Controller
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.IdentityClientType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
await LogProjectEventAsync(project, EventType.Project_Created);
|
||||
}
|
||||
|
||||
// Creating a project means you have read & write permission.
|
||||
return new ProjectResponseModel(result, true, true);
|
||||
}
|
||||
@@ -106,6 +115,10 @@ public class ProjectsController : Controller
|
||||
}
|
||||
|
||||
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id));
|
||||
if (result != null)
|
||||
{
|
||||
await LogProjectEventAsync(project, EventType.Project_Edited);
|
||||
}
|
||||
|
||||
// Updating a project means you have read & write permission.
|
||||
return new ProjectResponseModel(result, true, true);
|
||||
@@ -136,6 +149,8 @@ public class ProjectsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await LogProjectEventAsync(project, EventType.Project_Retrieved);
|
||||
|
||||
return new ProjectResponseModel(project, access.Read, access.Write);
|
||||
}
|
||||
|
||||
@@ -175,9 +190,32 @@ public class ProjectsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
await _deleteProjectCommand.DeleteProjects(projectsToDelete);
|
||||
if (projectsToDelete.Count > 0)
|
||||
{
|
||||
await _deleteProjectCommand.DeleteProjects(projectsToDelete);
|
||||
await LogProjectsEventAsync(projectsToDelete, EventType.Project_Deleted);
|
||||
}
|
||||
|
||||
var responses = results.Select(r => new BulkDeleteResponseModel(r.Project.Id, r.Error));
|
||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||
}
|
||||
|
||||
|
||||
private async Task LogProjectsEventAsync(IEnumerable<Project> projects, EventType eventType)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
|
||||
switch (_currentContext.IdentityClientType)
|
||||
{
|
||||
case IdentityClientType.ServiceAccount:
|
||||
await _eventService.LogServiceAccountProjectsEventAsync(userId, projects, eventType);
|
||||
break;
|
||||
case IdentityClientType.User:
|
||||
await _eventService.LogUserProjectsEventAsync(userId, projects, eventType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Task LogProjectEventAsync(Project project, EventType eventType) =>
|
||||
LogProjectsEventAsync(new[] { project }, eventType);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -15,17 +19,23 @@ public class TrashController : Controller
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IEmptyTrashCommand _emptyTrashCommand;
|
||||
private readonly IRestoreTrashCommand _restoreTrashCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public TrashController(
|
||||
ICurrentContext currentContext,
|
||||
ISecretRepository secretRepository,
|
||||
IEmptyTrashCommand emptyTrashCommand,
|
||||
IRestoreTrashCommand restoreTrashCommand)
|
||||
IRestoreTrashCommand restoreTrashCommand,
|
||||
IUserService userService,
|
||||
IEventService eventService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_secretRepository = secretRepository;
|
||||
_emptyTrashCommand = emptyTrashCommand;
|
||||
_restoreTrashCommand = restoreTrashCommand;
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
[HttpGet("secrets/{organizationId}/trash")]
|
||||
@@ -58,7 +68,9 @@ public class TrashController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var deletedSecrets = await _secretRepository.GetManyTrashedSecretsByIds(ids);
|
||||
await _emptyTrashCommand.EmptyTrash(organizationId, ids);
|
||||
await LogSecretsTrashEventAsync(deletedSecrets, EventType.Secret_Permanently_Deleted);
|
||||
}
|
||||
|
||||
[HttpPost("secrets/{organizationId}/trash/restore")]
|
||||
@@ -75,5 +87,27 @@ public class TrashController : Controller
|
||||
}
|
||||
|
||||
await _restoreTrashCommand.RestoreTrash(organizationId, ids);
|
||||
await LogSecretsTrashEventAsync(ids, EventType.Secret_Restored);
|
||||
}
|
||||
|
||||
private async Task LogSecretsTrashEventAsync(IEnumerable<Guid> secretIds, EventType eventType)
|
||||
{
|
||||
var secrets = await _secretRepository.GetManyByIds(secretIds);
|
||||
await LogSecretsTrashEventAsync(secrets, eventType);
|
||||
}
|
||||
|
||||
private async Task LogSecretsTrashEventAsync(IEnumerable<Secret> secrets, EventType eventType)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
|
||||
switch (_currentContext.IdentityClientType)
|
||||
{
|
||||
case IdentityClientType.ServiceAccount:
|
||||
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType);
|
||||
break;
|
||||
case IdentityClientType.User:
|
||||
await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Bit.Core.Settings;
|
||||
using AspNetCoreRateLimit;
|
||||
using Stripe;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using System.Globalization;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
@@ -210,7 +210,7 @@ public class Startup
|
||||
config.Conventions.Add(new PublicApiControllersModelConvention());
|
||||
});
|
||||
|
||||
services.AddSwagger(globalSettings);
|
||||
services.AddSwagger(globalSettings, Environment);
|
||||
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Bit.Api.Utilities;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)
|
||||
{
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
@@ -83,6 +83,14 @@ public static class ServiceCollectionExtensions
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
config.SchemaFilter<EnumSchemaFilter>();
|
||||
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||
|
||||
// These two filters require debug symbols/git, so only add them in development mode
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||
}
|
||||
|
||||
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
||||
config.IncludeXmlComments(apiFilePath, true);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@@ -18,7 +18,6 @@ using Event = Stripe.Event;
|
||||
namespace Bit.Billing.Services.Implementations;
|
||||
|
||||
public class UpcomingInvoiceHandler(
|
||||
IFeatureService featureService,
|
||||
ILogger<StripeEventProcessor> logger,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -48,8 +47,6 @@ public class UpcomingInvoiceHandler(
|
||||
|
||||
var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId.Value);
|
||||
@@ -59,7 +56,7 @@ public class UpcomingInvoiceHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||
await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id);
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
@@ -138,7 +135,7 @@ public class UpcomingInvoiceHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge);
|
||||
await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id);
|
||||
|
||||
await SendUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice);
|
||||
}
|
||||
@@ -164,45 +161,30 @@ public class UpcomingInvoiceHandler(
|
||||
private async Task AlignOrganizationTaxConcernsAsync(
|
||||
Organization organization,
|
||||
Subscription subscription,
|
||||
string eventId,
|
||||
bool setNonUSBusinessUseToReverseCharge)
|
||||
string eventId)
|
||||
{
|
||||
var nonUSBusinessUse =
|
||||
organization.PlanType.GetProductTier() != ProductTierType.Families &&
|
||||
subscription.Customer.Address.Country != "US";
|
||||
|
||||
bool setAutomaticTaxToEnabled;
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
{
|
||||
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||
organization.Id,
|
||||
eventId);
|
||||
}
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||
organization.Id,
|
||||
eventId);
|
||||
}
|
||||
|
||||
setAutomaticTaxToEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
setAutomaticTaxToEnabled =
|
||||
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||
(subscription.Customer.Address.Country == "US" ||
|
||||
(nonUSBusinessUse && subscription.Customer.TaxIds.Any()));
|
||||
}
|
||||
|
||||
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||
if (!subscription.AutomaticTax.Enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -226,41 +208,27 @@ public class UpcomingInvoiceHandler(
|
||||
private async Task AlignProviderTaxConcernsAsync(
|
||||
Provider provider,
|
||||
Subscription subscription,
|
||||
string eventId,
|
||||
bool setNonUSBusinessUseToReverseCharge)
|
||||
string eventId)
|
||||
{
|
||||
bool setAutomaticTaxToEnabled;
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
if (subscription.Customer.Address.Country != "US" &&
|
||||
subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
{
|
||||
if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||
provider.Id,
|
||||
eventId);
|
||||
}
|
||||
await stripeFacade.UpdateCustomer(subscription.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
|
||||
provider.Id,
|
||||
eventId);
|
||||
}
|
||||
|
||||
setAutomaticTaxToEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
setAutomaticTaxToEnabled =
|
||||
subscription.Customer.HasRecognizedTaxLocation() &&
|
||||
(subscription.Customer.Address.Country == "US" ||
|
||||
subscription.Customer.TaxIds.Any());
|
||||
}
|
||||
|
||||
if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled)
|
||||
if (!subscription.AutomaticTax.Enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ public class Event : ITableObject<Guid>, IEvent
|
||||
SystemUser = e.SystemUser;
|
||||
DomainName = e.DomainName;
|
||||
SecretId = e.SecretId;
|
||||
ProjectId = e.ProjectId;
|
||||
ServiceAccountId = e.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ public class Event : ITableObject<Guid>, IEvent
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string? DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
|
||||
@@ -30,6 +30,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
||||
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
|
||||
/// </summary>
|
||||
[MaxLength(50)]
|
||||
[Obsolete("This property has been deprecated. Use the 'Name' property instead.")]
|
||||
public string? BusinessName { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string? BusinessAddress1 { get; set; }
|
||||
@@ -147,6 +148,8 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
||||
/// <summary>
|
||||
/// Returns the business name of the organization, HTML decoded ready for display.
|
||||
/// </summary>
|
||||
///
|
||||
[Obsolete("This method has been deprecated. Use the 'DisplayName()' method instead.")]
|
||||
public string? DisplayBusinessName()
|
||||
{
|
||||
return WebUtility.HtmlDecode(BusinessName);
|
||||
|
||||
@@ -93,4 +93,11 @@ public enum EventType : int
|
||||
Secret_Created = 2101,
|
||||
Secret_Edited = 2102,
|
||||
Secret_Deleted = 2103,
|
||||
Secret_Permanently_Deleted = 2104,
|
||||
Secret_Restored = 2105,
|
||||
|
||||
Project_Retrieved = 2200,
|
||||
Project_Created = 2201,
|
||||
Project_Edited = 2202,
|
||||
Project_Deleted = 2203,
|
||||
}
|
||||
|
||||
@@ -37,5 +37,6 @@ public class EventMessage : IEvent
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class AzureEvent : ITableEntity
|
||||
public int? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
public EventTableEntity ToEventTableEntity()
|
||||
@@ -65,7 +66,8 @@ public class AzureEvent : ITableEntity
|
||||
SystemUser = SystemUser.HasValue ? (EventSystemUser)SystemUser.Value : null,
|
||||
DomainName = DomainName,
|
||||
SecretId = SecretId,
|
||||
ServiceAccountId = ServiceAccountId
|
||||
ServiceAccountId = ServiceAccountId,
|
||||
ProjectId = ProjectId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -95,6 +97,7 @@ public class EventTableEntity : IEvent
|
||||
SystemUser = e.SystemUser;
|
||||
DomainName = e.DomainName;
|
||||
SecretId = e.SecretId;
|
||||
ProjectId = e.ProjectId;
|
||||
ServiceAccountId = e.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -122,6 +125,7 @@ public class EventTableEntity : IEvent
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
public AzureEvent ToAzureEvent()
|
||||
@@ -152,6 +156,7 @@ public class EventTableEntity : IEvent
|
||||
SystemUser = SystemUser.HasValue ? (int)SystemUser.Value : null,
|
||||
DomainName = DomainName,
|
||||
SecretId = SecretId,
|
||||
ProjectId = ProjectId,
|
||||
ServiceAccountId = ServiceAccountId
|
||||
};
|
||||
}
|
||||
@@ -218,6 +223,15 @@ public class EventTableEntity : IEvent
|
||||
});
|
||||
}
|
||||
|
||||
if (e.ProjectId.HasValue)
|
||||
{
|
||||
entities.Add(new EventTableEntity(e)
|
||||
{
|
||||
PartitionKey = pKey,
|
||||
RowKey = $"ProjectId={e.ProjectId}__Date={dateKey}__Uniquifier={uniquifier}"
|
||||
});
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,5 +26,6 @@ public interface IEvent
|
||||
EventSystemUser? SystemUser { get; set; }
|
||||
string DomainName { get; set; }
|
||||
Guid? SecretId { get; set; }
|
||||
Guid? ProjectId { get; set; }
|
||||
Guid? ServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
#nullable enable
|
||||
@@ -11,6 +12,13 @@ public interface IEventRepository
|
||||
PageOptions pageOptions);
|
||||
Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate,
|
||||
PageOptions pageOptions);
|
||||
|
||||
Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret, DateTime startDate, DateTime endDate,
|
||||
PageOptions pageOptions);
|
||||
|
||||
Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project, DateTime startDate, DateTime endDate,
|
||||
PageOptions pageOptions);
|
||||
|
||||
Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions);
|
||||
Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId, DateTime startDate, DateTime endDate,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Azure.Data.Tables;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@@ -34,6 +35,20 @@ public class EventRepository : IEventRepository
|
||||
return await GetManyAsync($"OrganizationId={organizationId}", "Date={0}", startDate, endDate, pageOptions);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"OrganizationId={secret.OrganizationId}",
|
||||
$"SecretId={secret.Id}__Date={{0}}", startDate, endDate, pageOptions); ;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"OrganizationId={project.OrganizationId}",
|
||||
$"ProjectId={project.Id}__Date={{0}}", startDate, endDate, pageOptions);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
|
||||
@@ -35,4 +35,6 @@ public interface IEventService
|
||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
||||
Task LogUserSecretsEventAsync(Guid userId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
|
||||
Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
|
||||
Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type, DateTime? date = null);
|
||||
Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type, DateTime? date = null);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -13,7 +12,6 @@ namespace Bit.Core.Services;
|
||||
|
||||
public interface IOrganizationService
|
||||
{
|
||||
Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null);
|
||||
Task ReinstateSubscriptionAsync(Guid organizationId);
|
||||
Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);
|
||||
Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats);
|
||||
@@ -30,9 +28,6 @@ public interface IOrganizationService
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
||||
Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting, EventSystemUser eventSystemUser);
|
||||
Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);
|
||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||
Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);
|
||||
|
||||
@@ -464,6 +464,58 @@ public class EventService : IEventService
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type, DateTime? date = null)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var eventMessages = new List<IEvent>();
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
if (!CanUseEvents(orgAbilities, project.OrganizationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = project.OrganizationId,
|
||||
Type = type,
|
||||
ProjectId = project.Id,
|
||||
UserId = userId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type, DateTime? date = null)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var eventMessages = new List<IEvent>();
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
if (!CanUseEvents(orgAbilities, project.OrganizationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = project.OrganizationId,
|
||||
Type = type,
|
||||
ProjectId = project.Id,
|
||||
ServiceAccountId = serviceAccountId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
|
||||
{
|
||||
if (_currentContext == null || !orgId.HasValue)
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@@ -27,7 +26,6 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -65,6 +63,7 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -90,7 +89,8 @@ public class OrganizationService : IOrganizationService
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
|
||||
IStripeAdapter stripeAdapter
|
||||
)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
@@ -117,24 +117,7 @@ public class OrganizationService : IOrganizationService
|
||||
_pricingClient = pricingClient;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
||||
}
|
||||
|
||||
public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null)
|
||||
{
|
||||
var organization = await GetOrgById(organizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var eop = endOfPeriod.GetValueOrDefault(true);
|
||||
if (!endOfPeriod.HasValue && organization.ExpirationDate.HasValue &&
|
||||
organization.ExpirationDate.Value < DateTime.UtcNow)
|
||||
{
|
||||
eop = false;
|
||||
}
|
||||
|
||||
await _paymentService.CancelSubscriptionAsync(organization, eop);
|
||||
_stripeAdapter = stripeAdapter;
|
||||
}
|
||||
|
||||
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
||||
@@ -355,8 +338,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
var bankService = new BankAccountService();
|
||||
var customerService = new CustomerService();
|
||||
var customer = await customerService.GetAsync(organization.GatewayCustomerId,
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId,
|
||||
new CustomerGetOptions { Expand = new List<string> { "sources" } });
|
||||
if (customer == null)
|
||||
{
|
||||
@@ -417,12 +399,25 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||
{
|
||||
var customerService = new CustomerService();
|
||||
await customerService.UpdateAsync(organization.GatewayCustomerId,
|
||||
var newDisplayName = organization.DisplayName();
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
Email = organization.BillingEmail,
|
||||
Description = organization.DisplayBusinessName()
|
||||
Description = organization.DisplayBusinessName(),
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
// This overwrites the existing custom fields for this organization
|
||||
CustomFields = [
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = organization.SubscriberType(),
|
||||
Value = newDisplayName.Length <= 30
|
||||
? newDisplayName
|
||||
: newDisplayName[..30]
|
||||
}]
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -926,214 +921,6 @@ public class OrganizationService : IOrganizationService
|
||||
: EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||
}
|
||||
|
||||
public async Task ImportAsync(Guid organizationId,
|
||||
IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers,
|
||||
IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting,
|
||||
EventSystemUser eventSystemUser
|
||||
)
|
||||
{
|
||||
var organization = await GetOrgById(organizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!organization.UseDirectory)
|
||||
{
|
||||
throw new BadRequestException("Organization cannot use directory syncing.");
|
||||
}
|
||||
|
||||
var newUsersSet = new HashSet<string>(newUsers?.Select(u => u.ExternalId) ?? new List<string>());
|
||||
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
var existingExternalUsers = existingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
|
||||
var existingExternalUsersIdDict = existingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id);
|
||||
|
||||
// Users
|
||||
|
||||
var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>();
|
||||
|
||||
// Remove Users
|
||||
if (removeUserExternalIds?.Any() ?? false)
|
||||
{
|
||||
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
|
||||
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
|
||||
.Except(newUsersSet)
|
||||
.Where(u => existingUsersDict.TryGetValue(u, out var existingUser) &&
|
||||
existingUser.Type != OrganizationUserType.Owner)
|
||||
.Select(u => existingUsersDict[u]);
|
||||
|
||||
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
|
||||
events.AddRange(removeUsersSet.Select(u => (
|
||||
u,
|
||||
EventType.OrganizationUser_Removed,
|
||||
(DateTime?)DateTime.UtcNow
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
if (overwriteExisting)
|
||||
{
|
||||
// Remove existing external users that are not in new user set
|
||||
var usersToDelete = existingExternalUsers.Where(u =>
|
||||
u.Type != OrganizationUserType.Owner &&
|
||||
!newUsersSet.Contains(u.ExternalId) &&
|
||||
existingExternalUsersIdDict.ContainsKey(u.ExternalId));
|
||||
await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id));
|
||||
events.AddRange(usersToDelete.Select(u => (
|
||||
u,
|
||||
EventType.OrganizationUser_Removed,
|
||||
(DateTime?)DateTime.UtcNow
|
||||
))
|
||||
);
|
||||
foreach (var deletedUser in usersToDelete)
|
||||
{
|
||||
existingExternalUsersIdDict.Remove(deletedUser.ExternalId);
|
||||
}
|
||||
}
|
||||
|
||||
if (newUsers?.Any() ?? false)
|
||||
{
|
||||
// Marry existing users
|
||||
var existingUsersEmailsDict = existingUsers
|
||||
.Where(u => string.IsNullOrWhiteSpace(u.ExternalId))
|
||||
.ToDictionary(u => u.Email);
|
||||
var newUsersEmailsDict = newUsers.ToDictionary(u => u.Email);
|
||||
var usersToAttach = existingUsersEmailsDict.Keys.Intersect(newUsersEmailsDict.Keys).ToList();
|
||||
var usersToUpsert = new List<OrganizationUser>();
|
||||
foreach (var user in usersToAttach)
|
||||
{
|
||||
var orgUserDetails = existingUsersEmailsDict[user];
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserDetails.Id);
|
||||
if (orgUser != null)
|
||||
{
|
||||
orgUser.ExternalId = newUsersEmailsDict[user].ExternalId;
|
||||
usersToUpsert.Add(orgUser);
|
||||
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationUserRepository.UpsertManyAsync(usersToUpsert);
|
||||
|
||||
// Add new users
|
||||
var existingUsersSet = new HashSet<string>(existingExternalUsersIdDict.Keys);
|
||||
var usersToAdd = newUsersSet.Except(existingUsersSet).ToList();
|
||||
|
||||
var seatsAvailable = int.MaxValue;
|
||||
var enoughSeatsAvailable = true;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var seatCounts =
|
||||
await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
seatsAvailable = organization.Seats.Value - seatCounts.Total;
|
||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||
}
|
||||
|
||||
var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);
|
||||
|
||||
var userInvites = new List<(OrganizationUserInvite, string)>();
|
||||
foreach (var user in newUsers)
|
||||
{
|
||||
if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invite = new OrganizationUserInvite
|
||||
{
|
||||
Emails = new List<string> { user.Email },
|
||||
Type = OrganizationUserType.User,
|
||||
Collections = new List<CollectionAccessSelection>(),
|
||||
AccessSecretsManager = hasStandaloneSecretsManager
|
||||
};
|
||||
userInvites.Add((invite, user.ExternalId));
|
||||
}
|
||||
catch (BadRequestException)
|
||||
{
|
||||
// Thrown when the user is already invited to the organization
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser,
|
||||
userInvites);
|
||||
foreach (var invitedUser in invitedUsers)
|
||||
{
|
||||
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Groups
|
||||
if (groups?.Any() ?? false)
|
||||
{
|
||||
if (!organization.UseGroups)
|
||||
{
|
||||
throw new BadRequestException("Organization cannot use groups.");
|
||||
}
|
||||
|
||||
var groupsDict = groups.ToDictionary(g => g.Group.ExternalId);
|
||||
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
|
||||
var existingExternalGroups = existingGroups
|
||||
.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
|
||||
var existingExternalGroupsDict = existingExternalGroups.ToDictionary(g => g.ExternalId);
|
||||
|
||||
var newGroups = groups
|
||||
.Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId))
|
||||
.Select(g => g.Group).ToList();
|
||||
|
||||
var savedGroups = new List<Group>();
|
||||
foreach (var group in newGroups)
|
||||
{
|
||||
group.CreationDate = group.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
savedGroups.Add(await _groupRepository.CreateAsync(group));
|
||||
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
|
||||
existingExternalUsersIdDict);
|
||||
}
|
||||
|
||||
await _eventService.LogGroupEventsAsync(
|
||||
savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser,
|
||||
(DateTime?)DateTime.UtcNow)));
|
||||
|
||||
var updateGroups = existingExternalGroups
|
||||
.Where(g => groupsDict.ContainsKey(g.ExternalId))
|
||||
.ToList();
|
||||
|
||||
if (updateGroups.Any())
|
||||
{
|
||||
var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId);
|
||||
var existingGroupUsers = groupUsers
|
||||
.GroupBy(gu => gu.GroupId)
|
||||
.ToDictionary(g => g.Key, g => new HashSet<Guid>(g.Select(gr => gr.OrganizationUserId)));
|
||||
|
||||
foreach (var group in updateGroups)
|
||||
{
|
||||
var updatedGroup = groupsDict[group.ExternalId].Group;
|
||||
if (group.Name != updatedGroup.Name)
|
||||
{
|
||||
group.RevisionDate = DateTime.UtcNow;
|
||||
group.Name = updatedGroup.Name;
|
||||
|
||||
await _groupRepository.ReplaceAsync(group);
|
||||
}
|
||||
|
||||
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
|
||||
existingExternalUsersIdDict,
|
||||
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
|
||||
}
|
||||
|
||||
await _eventService.LogGroupEventsAsync(
|
||||
updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser,
|
||||
(DateTime?)DateTime.UtcNow)));
|
||||
}
|
||||
}
|
||||
|
||||
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d)));
|
||||
}
|
||||
|
||||
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
||||
{
|
||||
@@ -1150,18 +937,6 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
|
||||
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
|
||||
{
|
||||
var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys);
|
||||
var users = new HashSet<Guid>(availableUsers.Select(u => existingUsersIdDict[u]));
|
||||
if (existingUsers != null && existingUsers.Count == users.Count && users.SetEquals(existingUsers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, users);
|
||||
}
|
||||
|
||||
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
||||
{
|
||||
|
||||
@@ -127,4 +127,16 @@ public class NoopEventService : IEventService
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Core.Test")]
|
||||
[assembly: InternalsVisibleTo("Identity.IntegrationTest")]
|
||||
|
||||
@@ -113,6 +113,21 @@ public static class StripeConstants
|
||||
public const string SpanishNIF = "es_cif";
|
||||
}
|
||||
|
||||
public static class TaxIdVerificationStatus
|
||||
{
|
||||
public const string Pending = "pending";
|
||||
public const string Unavailable = "unavailable";
|
||||
public const string Unverified = "unverified";
|
||||
public const string Verified = "verified";
|
||||
}
|
||||
|
||||
public static class TaxRegistrationStatus
|
||||
{
|
||||
public const string Active = "active";
|
||||
public const string Expired = "expired";
|
||||
public const string Scheduled = "scheduled";
|
||||
}
|
||||
|
||||
public static class ValidateTaxLocationTiming
|
||||
{
|
||||
public const string Deferred = "deferred";
|
||||
|
||||
@@ -36,6 +36,10 @@ public static class BillingExtensions
|
||||
Status: ProviderStatusType.Billable
|
||||
};
|
||||
|
||||
// Reseller types do not have Stripe entities
|
||||
public static bool IsStripeSupported(this ProviderType providerType) =>
|
||||
providerType is ProviderType.Msp or ProviderType.BusinessUnit;
|
||||
|
||||
public static bool SupportsConsolidatedBilling(this ProviderType providerType)
|
||||
=> providerType is ProviderType.Msp or ProviderType.BusinessUnit;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
@@ -14,7 +13,7 @@ public static class LicenseExtensions
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue)
|
||||
if (org.ExpirationDate.HasValue)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Enums;
|
||||
@@ -121,6 +120,6 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
||||
|
||||
private static bool IsTrialing(Organization org, SubscriptionInfo subscriptionInfo) =>
|
||||
subscriptionInfo?.Subscription is null
|
||||
? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue
|
||||
? !org.ExpirationDate.HasValue
|
||||
: subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public class OrganizationLicense : ILicense
|
||||
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue)
|
||||
if (org.ExpirationDate.HasValue)
|
||||
{
|
||||
Expires = Refresh = org.ExpirationDate.Value;
|
||||
Trial = false;
|
||||
|
||||
@@ -5,6 +5,7 @@ public record OrganizationWarnings
|
||||
public FreeTrialWarning? FreeTrial { get; set; }
|
||||
public InactiveSubscriptionWarning? InactiveSubscription { get; set; }
|
||||
public ResellerRenewalWarning? ResellerRenewal { get; set; }
|
||||
public TaxIdWarning? TaxId { get; set; }
|
||||
|
||||
public record FreeTrialWarning
|
||||
{
|
||||
@@ -39,4 +40,9 @@ public record OrganizationWarnings
|
||||
public required DateTime SuspensionDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public record TaxIdWarning
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
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.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using FreeTrialWarning = Bit.Core.Billing.Organizations.Models.OrganizationWarnings.FreeTrialWarning;
|
||||
using InactiveSubscriptionWarning =
|
||||
Bit.Core.Billing.Organizations.Models.OrganizationWarnings.InactiveSubscriptionWarning;
|
||||
using ResellerRenewalWarning =
|
||||
Bit.Core.Billing.Organizations.Models.OrganizationWarnings.ResellerRenewalWarning;
|
||||
using Stripe.Tax;
|
||||
|
||||
namespace Bit.Core.Billing.Organizations.Queries;
|
||||
|
||||
using static StripeConstants;
|
||||
using FreeTrialWarning = OrganizationWarnings.FreeTrialWarning;
|
||||
using InactiveSubscriptionWarning = OrganizationWarnings.InactiveSubscriptionWarning;
|
||||
using ResellerRenewalWarning = OrganizationWarnings.ResellerRenewalWarning;
|
||||
using TaxIdWarning = OrganizationWarnings.TaxIdWarning;
|
||||
|
||||
public interface IGetOrganizationWarningsQuery
|
||||
{
|
||||
@@ -38,29 +37,31 @@ public class GetOrganizationWarningsQuery(
|
||||
public async Task<OrganizationWarnings> Run(
|
||||
Organization organization)
|
||||
{
|
||||
var response = new OrganizationWarnings();
|
||||
var warnings = new OrganizationWarnings();
|
||||
|
||||
var subscription =
|
||||
await subscriberService.GetSubscription(organization,
|
||||
new SubscriptionGetOptions { Expand = ["customer", "latest_invoice", "test_clock"] });
|
||||
new SubscriptionGetOptions { Expand = ["customer.tax_ids", "latest_invoice", "test_clock"] });
|
||||
|
||||
if (subscription == null)
|
||||
{
|
||||
return response;
|
||||
return warnings;
|
||||
}
|
||||
|
||||
response.FreeTrial = await GetFreeTrialWarning(organization, subscription);
|
||||
warnings.FreeTrial = await GetFreeTrialWarningAsync(organization, subscription);
|
||||
|
||||
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
response.InactiveSubscription = await GetInactiveSubscriptionWarning(organization, provider, subscription);
|
||||
warnings.InactiveSubscription = await GetInactiveSubscriptionWarningAsync(organization, provider, subscription);
|
||||
|
||||
response.ResellerRenewal = await GetResellerRenewalWarning(provider, subscription);
|
||||
warnings.ResellerRenewal = await GetResellerRenewalWarningAsync(provider, subscription);
|
||||
|
||||
return response;
|
||||
warnings.TaxId = await GetTaxIdWarningAsync(organization, subscription.Customer, provider);
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private async Task<FreeTrialWarning?> GetFreeTrialWarning(
|
||||
private async Task<FreeTrialWarning?> GetFreeTrialWarningAsync(
|
||||
Organization organization,
|
||||
Subscription subscription)
|
||||
{
|
||||
@@ -81,7 +82,7 @@ public class GetOrganizationWarningsQuery(
|
||||
|
||||
var customer = subscription.Customer;
|
||||
|
||||
var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization);
|
||||
var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(organization);
|
||||
|
||||
var hasPaymentMethod =
|
||||
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||
@@ -101,66 +102,51 @@ public class GetOrganizationWarningsQuery(
|
||||
return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays };
|
||||
}
|
||||
|
||||
private async Task<InactiveSubscriptionWarning?> GetInactiveSubscriptionWarning(
|
||||
private async Task<InactiveSubscriptionWarning?> GetInactiveSubscriptionWarningAsync(
|
||||
Organization organization,
|
||||
Provider? provider,
|
||||
Subscription subscription)
|
||||
{
|
||||
// If the organization is enabled or the subscription is active, don't return a warning.
|
||||
if (organization.Enabled || subscription is not
|
||||
{
|
||||
Status: SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled
|
||||
})
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the organization is managed by a provider, return a warning asking them to contact the provider.
|
||||
if (provider != null)
|
||||
{
|
||||
return new InactiveSubscriptionWarning { Resolution = "contact_provider" };
|
||||
}
|
||||
|
||||
var isOrganizationOwner = await currentContext.OrganizationOwner(organization.Id);
|
||||
|
||||
switch (organization.Enabled)
|
||||
/* If the organization is not managed by a provider and this user is the owner, return a warning based
|
||||
on the subscription status. */
|
||||
if (isOrganizationOwner)
|
||||
{
|
||||
// Member of an enabled, trialing organization.
|
||||
case true when subscription.Status is SubscriptionStatus.Trialing:
|
||||
return subscription.Status switch
|
||||
{
|
||||
SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning
|
||||
{
|
||||
var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization);
|
||||
|
||||
var hasPaymentMethod =
|
||||
!string.IsNullOrEmpty(subscription.Customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||
!string.IsNullOrEmpty(subscription.Customer.DefaultSourceId) ||
|
||||
hasUnverifiedBankAccount ||
|
||||
subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
|
||||
|
||||
// If this member is the owner and there's no payment method on file, ask them to add one.
|
||||
return isOrganizationOwner && !hasPaymentMethod
|
||||
? new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" }
|
||||
: null;
|
||||
}
|
||||
// Member of disabled and unpaid or canceled organization.
|
||||
case false when subscription.Status is SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled:
|
||||
Resolution = "add_payment_method"
|
||||
},
|
||||
SubscriptionStatus.Canceled => new InactiveSubscriptionWarning
|
||||
{
|
||||
// If the organization is managed by a provider, return a warning asking them to contact the provider.
|
||||
if (provider != null)
|
||||
{
|
||||
return new InactiveSubscriptionWarning { Resolution = "contact_provider" };
|
||||
}
|
||||
|
||||
/* If the organization is not managed by a provider and this user is the owner, return an action warning based
|
||||
on the subscription status. */
|
||||
if (isOrganizationOwner)
|
||||
{
|
||||
return subscription.Status switch
|
||||
{
|
||||
SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning
|
||||
{
|
||||
Resolution = "add_payment_method"
|
||||
},
|
||||
SubscriptionStatus.Canceled => new InactiveSubscriptionWarning
|
||||
{
|
||||
Resolution = "resubscribe"
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, this member is not the owner, and we need to ask them to contact the owner.
|
||||
return new InactiveSubscriptionWarning { Resolution = "contact_owner" };
|
||||
}
|
||||
default: return null;
|
||||
Resolution = "resubscribe"
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, return a warning asking them to contact the owner.
|
||||
return new InactiveSubscriptionWarning { Resolution = "contact_owner" };
|
||||
}
|
||||
|
||||
private async Task<ResellerRenewalWarning?> GetResellerRenewalWarning(
|
||||
private async Task<ResellerRenewalWarning?> GetResellerRenewalWarningAsync(
|
||||
Provider? provider,
|
||||
Subscription subscription)
|
||||
{
|
||||
@@ -241,7 +227,62 @@ public class GetOrganizationWarningsQuery(
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<bool> HasUnverifiedBankAccount(
|
||||
private async Task<TaxIdWarning?> GetTaxIdWarningAsync(
|
||||
Organization organization,
|
||||
Customer customer,
|
||||
Provider? provider)
|
||||
{
|
||||
var productTier = organization.PlanType.GetProductTier();
|
||||
|
||||
// Only business tier customers can have tax IDs
|
||||
if (productTier is not ProductTierType.Teams and not ProductTierType.Enterprise)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only an organization owner can update a tax ID
|
||||
if (!await currentContext.OrganizationOwner(organization.Id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (provider != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get active and scheduled registrations
|
||||
var registrations = (await Task.WhenAll(
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),
|
||||
stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))
|
||||
.SelectMany(registrations => registrations.Data);
|
||||
|
||||
// Find the matching registration for the customer
|
||||
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);
|
||||
|
||||
// If we're not registered in their country, we don't need a warning
|
||||
if (registration == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var taxId = customer.TaxIds.FirstOrDefault();
|
||||
|
||||
return taxId switch
|
||||
{
|
||||
// Customer's tax ID is missing
|
||||
null => new TaxIdWarning { Type = "tax_id_missing" },
|
||||
// Not sure if this case is valid, but Stripe says this property is nullable
|
||||
not null when taxId.Verification == null => null,
|
||||
// Customer's tax ID is pending verification
|
||||
not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = "tax_id_pending_verification" },
|
||||
// Customer's tax ID failed verification
|
||||
not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = "tax_id_failed_verification" },
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> HasUnverifiedBankAccountAsync(
|
||||
Organization organization)
|
||||
{
|
||||
var setupIntentId = await setupIntentCache.Get(organization.Id);
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace Bit.Core.Billing.Organizations.Services;
|
||||
|
||||
public class OrganizationBillingService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IFeatureService featureService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<OrganizationBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -273,11 +272,9 @@ public class OrganizationBillingService(
|
||||
ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge &&
|
||||
planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families &&
|
||||
|
||||
if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families &&
|
||||
customerSetup.TaxInformation.Country != "US")
|
||||
{
|
||||
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
@@ -491,24 +488,10 @@ public class OrganizationBillingService(
|
||||
};
|
||||
}
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge && customer.HasBillingLocation())
|
||||
if (customer.HasBillingLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (customer.HasRecognizedTaxLocation())
|
||||
{
|
||||
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled =
|
||||
subscriptionSetup.PlanType.GetProductTier() == ProductTierType.Families ||
|
||||
customer.Address.Country == "US" ||
|
||||
customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
|
||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
}
|
||||
|
||||
@@ -519,9 +502,7 @@ public class OrganizationBillingService(
|
||||
var customer = await subscriberService.GetCustomerOrThrow(organization,
|
||||
new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (!setNonUSBusinessUseToReverseCharge || subscriptionSetup.PlanType.GetProductTier() is
|
||||
if (subscriptionSetup.PlanType.GetProductTier() is
|
||||
not (ProductTierType.Teams or
|
||||
ProductTierType.TeamsStarter or
|
||||
ProductTierType.Enterprise))
|
||||
|
||||
@@ -7,11 +7,13 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Migration.Models;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -253,7 +255,10 @@ public class ProviderMigrator(
|
||||
|
||||
var taxInfo = await paymentService.GetTaxInfoAsync(sampleOrganization);
|
||||
|
||||
var customer = await providerBillingService.SetupCustomer(provider, taxInfo);
|
||||
// Create dummy payment source for legacy migration - this migrator is deprecated and will be removed
|
||||
var dummyPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "migration_dummy_token");
|
||||
|
||||
var customer = await providerBillingService.SetupCustomer(provider, taxInfo, dummyPaymentSource);
|
||||
|
||||
await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
|
||||
18
src/Core/Billing/Providers/Models/ProviderWarnings.cs
Normal file
18
src/Core/Billing/Providers/Models/ProviderWarnings.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Bit.Core.Billing.Providers.Models;
|
||||
|
||||
public class ProviderWarnings
|
||||
{
|
||||
public SuspensionWarning? Suspension { get; set; }
|
||||
public TaxIdWarning? TaxId { get; set; }
|
||||
|
||||
public record SuspensionWarning
|
||||
{
|
||||
public required string Resolution { get; set; }
|
||||
public DateTime? SubscriptionCancelsAt { get; set; }
|
||||
}
|
||||
|
||||
public record TaxIdWarning
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Providers.Queries;
|
||||
|
||||
public interface IGetProviderWarningsQuery
|
||||
{
|
||||
Task<ProviderWarnings?> Run(Provider provider);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public interface IProviderBillingService
|
||||
Task<Customer> SetupCustomer(
|
||||
Provider provider,
|
||||
TaxInfo taxInfo,
|
||||
TokenizedPaymentSource tokenizedPaymentSource = null);
|
||||
TokenizedPaymentSource tokenizedPaymentSource);
|
||||
|
||||
/// <summary>
|
||||
/// For use during the provider setup process, this method starts a Stripe <see cref="Stripe.Subscription"/> for the given <paramref name="provider"/>.
|
||||
|
||||
@@ -157,4 +157,22 @@ public interface ISubscriberService
|
||||
Task VerifyBankAccount(
|
||||
ISubscriber subscriber,
|
||||
string descriptorCode);
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> exists in the gateway.
|
||||
/// If the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> is <see langword="null"/> or empty, returns <see langword="true"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber whose gateway customer ID should be validated.</param>
|
||||
/// <returns><see langword="true"/> if the gateway customer ID is valid or empty; <see langword="false"/> if the customer doesn't exist in the gateway.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
||||
Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewaySubscriptionId"/> exists in the gateway.
|
||||
/// If the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewaySubscriptionId"/> is <see langword="null"/> or empty, returns <see langword="true"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber whose gateway subscription ID should be validated.</param>
|
||||
/// <returns><see langword="true"/> if the gateway subscription ID is valid or empty; <see langword="false"/> if the subscription doesn't exist in the gateway.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
||||
Task<bool> IsValidGatewaySubscriptionIdAsync(ISubscriber subscriber);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -33,7 +33,6 @@ using static StripeConstants;
|
||||
|
||||
public class SubscriberService(
|
||||
IBraintreeGateway braintreeGateway,
|
||||
IFeatureService featureService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<SubscriberService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -802,28 +801,27 @@ public class SubscriberService(
|
||||
_ => false
|
||||
};
|
||||
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge && isBusinessUseSubscriber)
|
||||
|
||||
if (isBusinessUseSubscriber)
|
||||
{
|
||||
switch (customer)
|
||||
{
|
||||
case
|
||||
{
|
||||
Address.Country: not "US",
|
||||
TaxExempt: not StripeConstants.TaxExempt.Reverse
|
||||
TaxExempt: not TaxExempt.Reverse
|
||||
}:
|
||||
await stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
|
||||
break;
|
||||
case
|
||||
{
|
||||
Address.Country: "US",
|
||||
TaxExempt: StripeConstants.TaxExempt.Reverse
|
||||
TaxExempt: TaxExempt.Reverse
|
||||
}:
|
||||
await stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.None });
|
||||
new CustomerUpdateOptions { TaxExempt = TaxExempt.None });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -909,6 +907,44 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))
|
||||
{
|
||||
// subscribers are allowed to have no customer id as a business rule
|
||||
return true;
|
||||
}
|
||||
try
|
||||
{
|
||||
await stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId);
|
||||
return true;
|
||||
}
|
||||
catch (StripeException e) when (e.StripeError.Code == "resource_missing")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsValidGatewaySubscriptionIdAsync(ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
||||
{
|
||||
// subscribers are allowed to have no subscription id as a business rule
|
||||
return true;
|
||||
}
|
||||
try
|
||||
{
|
||||
await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
|
||||
return true;
|
||||
}
|
||||
catch (StripeException e) when (e.StripeError.Code == "resource_missing")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#region Shared Utilities
|
||||
|
||||
private async Task AddBraintreeCustomerIdAsync(
|
||||
|
||||
@@ -113,7 +113,6 @@ public static class FeatureFlagKeys
|
||||
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
|
||||
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
|
||||
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
|
||||
public const string ImportAsyncRefactor = "pm-22583-refactor-import-async";
|
||||
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||
public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal";
|
||||
|
||||
@@ -154,13 +153,10 @@ public static class FeatureFlagKeys
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
||||
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
||||
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
|
||||
public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0";
|
||||
public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge";
|
||||
public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe";
|
||||
public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout";
|
||||
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
|
||||
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="5.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.2.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<tr>
|
||||
<td
|
||||
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;">
|
||||
Keep you and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a
|
||||
data breach.
|
||||
Keep yourself and your organization's data safe by changing passwords that are weak, reused, or have been exposed
|
||||
in a data breach.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#>SecurityTasksHtmlLayout}}
|
||||
Keep you and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a data
|
||||
breach.
|
||||
Keep yourself and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a
|
||||
data breach.
|
||||
|
||||
Launch the Bitwarden extension to review your at-risk passwords.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface ISecretRepository
|
||||
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||
Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
|
||||
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
|
||||
Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids);
|
||||
Task<Secret> GetByIdAsync(Guid id);
|
||||
Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);
|
||||
Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);
|
||||
|
||||
@@ -105,4 +105,6 @@ public class NoopSecretRepository : ISecretRepository
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids) => Task.FromResult<IEnumerable<Secret>>([]);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ public interface IStripeAdapter
|
||||
Task<Stripe.PaymentMethod> PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null);
|
||||
Task<Stripe.TaxId> TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options);
|
||||
Task<Stripe.TaxId> TaxIdDeleteAsync(string customerId, string taxIdId, Stripe.TaxIdDeleteOptions options = null);
|
||||
Task<Stripe.StripeList<Stripe.Tax.Registration>> TaxRegistrationsListAsync(Stripe.Tax.RegistrationListOptions options = null);
|
||||
Task<Stripe.StripeList<Stripe.Charge>> ChargeListAsync(Stripe.ChargeListOptions options);
|
||||
Task<Stripe.Refund> RefundCreateAsync(Stripe.RefundCreateOptions options);
|
||||
Task<Stripe.Card> CardDeleteAsync(string customerId, string cardId, Stripe.CardDeleteOptions options = null);
|
||||
|
||||
@@ -24,6 +24,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Core.Auth.Enums;
|
||||
using HandlebarsDotNet;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@@ -31,10 +32,12 @@ public class HandlebarsMailService : IMailService
|
||||
{
|
||||
private const string Namespace = "Bit.Core.MailTemplates.Handlebars";
|
||||
private const string _utcTimeZoneDisplay = "UTC";
|
||||
private const string FailedTwoFactorAttemptCacheKeyFormat = "FailedTwoFactorAttemptEmail_{0}";
|
||||
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IMailDeliveryService _mailDeliveryService;
|
||||
private readonly IMailEnqueuingService _mailEnqueuingService;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();
|
||||
|
||||
private bool _registeredHelpersAndPartials = false;
|
||||
@@ -42,11 +45,13 @@ public class HandlebarsMailService : IMailService
|
||||
public HandlebarsMailService(
|
||||
GlobalSettings globalSettings,
|
||||
IMailDeliveryService mailDeliveryService,
|
||||
IMailEnqueuingService mailEnqueuingService)
|
||||
IMailEnqueuingService mailEnqueuingService,
|
||||
IDistributedCache distributedCache)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_mailDeliveryService = mailDeliveryService;
|
||||
_mailEnqueuingService = mailEnqueuingService;
|
||||
_distributedCache = distributedCache;
|
||||
}
|
||||
|
||||
public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token)
|
||||
@@ -196,6 +201,16 @@ public class HandlebarsMailService : IMailService
|
||||
|
||||
public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)
|
||||
{
|
||||
// Check if we've sent this email within the last hour
|
||||
var cacheKey = string.Format(FailedTwoFactorAttemptCacheKeyFormat, email);
|
||||
var cachedValue = await _distributedCache.GetAsync(cacheKey);
|
||||
|
||||
if (cachedValue != null)
|
||||
{
|
||||
// Email was already sent within the last hour, skip sending
|
||||
return;
|
||||
}
|
||||
|
||||
var message = CreateDefaultMessage("Failed two-step login attempt detected", email);
|
||||
var model = new FailedAuthAttemptModel()
|
||||
{
|
||||
@@ -211,6 +226,13 @@ public class HandlebarsMailService : IMailService
|
||||
await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempt", model);
|
||||
message.Category = "FailedTwoFactorAttempt";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
|
||||
// Set cache entry with 1 hour expiration to prevent sending again
|
||||
var cacheOptions = new DistributedCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
||||
};
|
||||
await _distributedCache.SetAsync(cacheKey, [1], cacheOptions);
|
||||
}
|
||||
|
||||
public async Task SendMasterPasswordHintEmailAsync(string email, string hint)
|
||||
|
||||
@@ -22,6 +22,7 @@ public class StripeAdapter : IStripeAdapter
|
||||
private readonly Stripe.SetupIntentService _setupIntentService;
|
||||
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
||||
private readonly CustomerBalanceTransactionService _customerBalanceTransactionService;
|
||||
private readonly Stripe.Tax.RegistrationService _taxRegistrationService;
|
||||
|
||||
public StripeAdapter()
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public class StripeAdapter : IStripeAdapter
|
||||
_setupIntentService = new SetupIntentService();
|
||||
_testClockService = new Stripe.TestHelpers.TestClockService();
|
||||
_customerBalanceTransactionService = new CustomerBalanceTransactionService();
|
||||
_taxRegistrationService = new Stripe.Tax.RegistrationService();
|
||||
}
|
||||
|
||||
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
||||
@@ -208,6 +210,11 @@ public class StripeAdapter : IStripeAdapter
|
||||
return _taxIdService.DeleteAsync(customerId, taxIdId);
|
||||
}
|
||||
|
||||
public Task<Stripe.StripeList<Stripe.Tax.Registration>> TaxRegistrationsListAsync(Stripe.Tax.RegistrationListOptions options = null)
|
||||
{
|
||||
return _taxRegistrationService.ListAsync(options);
|
||||
}
|
||||
|
||||
public Task<Stripe.StripeList<Stripe.Charge>> ChargeListAsync(Stripe.ChargeListOptions options)
|
||||
{
|
||||
return _chargeService.ListAsync(options);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
@@ -136,69 +135,17 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
if (subscriptionUpdate is CompleteSubscriptionUpdate)
|
||||
{
|
||||
var setNonUSBusinessUseToReverseCharge =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge);
|
||||
|
||||
if (setNonUSBusinessUseToReverseCharge)
|
||||
{
|
||||
if (sub.Customer is
|
||||
{
|
||||
Address.Country: not "US",
|
||||
TaxExempt: not StripeConstants.TaxExempt.Reverse
|
||||
})
|
||||
if (sub.Customer is
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else if (sub.Customer.HasRecognizedTaxLocation())
|
||||
Address.Country: not "US",
|
||||
TaxExempt: not StripeConstants.TaxExempt.Reverse
|
||||
})
|
||||
{
|
||||
switch (subscriber)
|
||||
{
|
||||
case User:
|
||||
{
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
break;
|
||||
}
|
||||
case Organization:
|
||||
{
|
||||
if (sub.Customer.Address.Country == "US")
|
||||
{
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else
|
||||
{
|
||||
var familyPriceIds = (await Task.WhenAll(
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
|
||||
_pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
|
||||
.Select(plan => plan.PasswordManager.StripePlanId);
|
||||
|
||||
var updateIsForPersonalUse = updatedItemOptions
|
||||
.Select(option => option.Price)
|
||||
.Intersect(familyPriceIds)
|
||||
.Any();
|
||||
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = updateIsForPersonalUse || sub.Customer.TaxIds.Any()
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Provider:
|
||||
{
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
{
|
||||
Enabled = sub.Customer.Address.Country == "US" ||
|
||||
sub.Customer.TaxIds.Any()
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId,
|
||||
new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse });
|
||||
}
|
||||
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
if (!subscriptionUpdate.UpdateNeeded(sub))
|
||||
|
||||
@@ -21,7 +21,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using MimeKit;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
namespace Bit.Events;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.TrialInitiation.Registration;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -11,16 +9,13 @@ namespace Bit.Identity.Billing.Controller;
|
||||
[Route("accounts")]
|
||||
[ExceptionHandlerFilter]
|
||||
public class AccountsController(
|
||||
ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand,
|
||||
IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller
|
||||
ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand) : Microsoft.AspNetCore.Mvc.Controller
|
||||
{
|
||||
[HttpPost("trial/send-verification-email")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)
|
||||
{
|
||||
var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0);
|
||||
|
||||
var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7;
|
||||
var trialLength = model.TrialLength ?? 7;
|
||||
|
||||
var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(
|
||||
model.Email,
|
||||
|
||||
@@ -8,9 +8,9 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Identity.Models;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
using System.Diagnostics;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Repositories;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user