1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-30 23:11:22 +01:00

Merge branch 'main' into ac/ac-1682/ef-migrations

This commit is contained in:
Rui Tome 2024-03-22 11:53:01 +00:00
commit 6c21d4e96a
No known key found for this signature in database
GPG Key ID: 526239D96A8EC066
142 changed files with 17858 additions and 918 deletions

View File

@ -49,6 +49,7 @@
"Azure.Messaging.ServiceBus", "Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs", "Azure.Storage.Blobs",
"Azure.Storage.Queues", "Azure.Storage.Queues",
"DuoUniversal",
"Fido2.AspNet", "Fido2.AspNet",
"Duende.IdentityServer", "Duende.IdentityServer",
"Microsoft.Azure.Cosmos", "Microsoft.Azure.Cosmos",

View File

@ -540,36 +540,11 @@ jobs:
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: | if: |
github.ref == 'refs/heads/main' (github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc' || github.ref == 'refs/heads/hotfix-rc')
env: && contains(needs.*.result, 'failure')
LINT_STATUS: ${{ needs.lint.result }} run: exit 1
TESTING_STATUS: ${{ needs.testing.result }}
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
UPLOAD_STATUS: ${{ needs.upload.result }}
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
TRIGGER_K8S_DEPLOY_STATUS: ${{ needs.trigger-k8s-deploy.result }}
run: |
if [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$TESTING_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_ARTIFACTS_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_DOCKER_STATUS" = "failure" ]; then
exit 1
elif [ "$UPLOAD_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_MSSQLMIGRATORUTILITY_STATUS" = "failure" ]; then
exit 1
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
exit 1
elif [ "$TRIGGER_K8S_DEPLOY_STATUS" = "failure" ]; then
exit 1
fi
- name: Log in to Azure - CI subscription - name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7

53
.github/workflows/cleanup-rc-branch.yml vendored Normal file
View File

@ -0,0 +1,53 @@
---
name: Cleanup RC Branch
on:
push:
tags:
- v**
jobs:
delete-rc:
name: Delete RC Branch
runs-on: ubuntu-22.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Check if a RC branch exists
id: branch-check
run: |
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
fi
- name: Delete RC branch
env:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
fi

View File

@ -69,20 +69,15 @@ jobs:
name: Check for failures name: Check for failures
if: always() if: always()
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: needs: [purge]
- purge
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: | if: |
github.ref == 'refs/heads/main' (github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc' || github.ref == 'refs/heads/hotfix-rc')
env: && contains(needs.*.result, 'failure')
PURGE_STATUS: ${{ needs.purge.result }} run: exit 1
run: |
if [ "$PURGE_STATUS" = "failure" ]; then
exit 1
fi
- name: Log in to Azure - CI subscription - name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7

View File

@ -7,25 +7,33 @@ on:
- "main" - "main"
- "rc" - "rc"
- "hotfix-rc" - "hotfix-rc"
pull_request: pull_request_target:
types: [opened, synchronize]
permissions: read-all permissions: read-all
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast: sast:
name: SAST scan name: SAST scan
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: check-run
permissions: permissions:
security-events: write security-events: write
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx - name: Scan with Checkmarx
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23 uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
env: env:
INCREMENTAL: "${{ github.event_name == 'pull_request' && '--sast-incremental' || '' }}" INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with: with:
project_name: ${{ github.repository }} project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }} cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
@ -35,17 +43,21 @@ jobs:
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }} additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub - name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2 uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
with: with:
sarif_file: cx_result.sarif sarif_file: cx_result.sarif
quality: quality:
name: Quality scan name: Quality scan
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: check-run
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud - name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1 uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
@ -56,5 +68,4 @@ jobs:
args: > args: >
-Dsonar.organization=${{ github.repository_owner }} -Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }} -Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.test.exclusions=test/**
-Dsonar.tests=test/ -Dsonar.tests=test/

View File

@ -57,9 +57,9 @@ jobs:
run: sleep 15s run: sleep 15s
- name: Migrate SQL Server - name: Migrate SQL Server
working-directory: "dev" run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
run: "./migrate.ps1" env:
shell: pwsh CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
- name: Migrate MySQL - name: Migrate MySQL
working-directory: "util/MySqlMigrations" working-directory: "util/MySqlMigrations"
@ -147,9 +147,9 @@ jobs:
shell: pwsh shell: pwsh
- name: Migrate - name: Migrate
working-directory: "dev" run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
run: "./migrate.ps1" env:
shell: pwsh CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
- name: Diff .sqlproj to migrations - name: Diff .sqlproj to migrations
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True

View File

@ -1,13 +1,12 @@
--- ---
name: Bump version name: Version Bump
run-name: Bump version to ${{ inputs.version_number }}
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
version_number: version_number_override:
description: "New version (example: '2024.1.0')" description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: true required: false
type: string type: string
cut_rc_branch: cut_rc_branch:
description: "Cut RC branch?" description: "Cut RC branch?"
@ -16,22 +15,16 @@ on:
jobs: jobs:
bump_version: bump_version:
name: Bump name: Bump Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps: steps:
- name: Log in to Azure - CI subscription - name: Validate version input
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7 if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} version: ${{ inputs.version_number_override }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Check out branch - name: Check out branch
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
@ -48,6 +41,20 @@ jobs:
exit 1 exit 1
fi fi
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Import GPG key - name: Import GPG key
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0 uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
with: with:
@ -56,22 +63,35 @@ jobs:
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: true git_commit_gpgsign: true
- name: Set up Git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create version branch - name: Create version branch
id: create-branch id: create-branch
run: | run: |
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }} NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Install xmllint - name: Install xmllint
run: sudo apt install -y libxml2-utils run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify input version - name: Get current version
env: id: current-version
NEW_VERSION: ${{ inputs.version_number }}
run: | run: |
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props) CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed. # Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed." echo "Version has not changed."
@ -87,16 +107,37 @@ jobs:
exit 1 exit 1
fi fi
- name: Bump version props - name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump version props - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main uses: bitwarden/gh-actions/version-bump@main
with: with:
version: ${{ inputs.version_number }}
file_path: "Directory.Build.props" file_path: "Directory.Build.props"
version: ${{ inputs.version_number_override }}
- name: Set up Git - name: Bump version props - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
run: | run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com" if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
git config --local user.name "bitwarden-devops-bot" echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed - name: Check if version changed
id: version-changed id: version-changed
@ -110,7 +151,7 @@ jobs:
- name: Commit files - name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes - name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
@ -124,7 +165,7 @@ jobs:
env: env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }} PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ inputs.version_number }}" TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: | run: |
PR_URL=$(gh pr create --title "$TITLE" \ PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \ --base "main" \
@ -140,38 +181,43 @@ jobs:
- [X] Other - [X] Other
## Objective ## Objective
Automated version bump to ${{ inputs.version_number }}") Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR - name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve run: gh pr review $PR_NUMBER --approve
- name: Merge PR - name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }} PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cut_rc: cut_rc:
name: Cut RC branch name: Cut RC branch
needs: bump_version
if: ${{ inputs.cut_rc_branch == true }} if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out branch - name: Check out branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
ref: main ref: main
- name: Install xmllint - name: Install xmllint
run: sudo apt install -y libxml2-utils run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated - name: Verify version has been updated
env: env:
NEW_VERSION: ${{ inputs.version_number }} NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: | run: |
# Wait for version to change. # Wait for version to change.
while : ; do while : ; do

View File

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

View File

@ -17,6 +17,7 @@ using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Services; namespace Bit.Commercial.Core.AdminConsole.Services;
@ -257,7 +258,7 @@ public class ProviderService : IProviderService
await _providerUserRepository.ReplaceAsync(providerUser); await _providerUserRepository.ReplaceAsync(providerUser);
events.Add((providerUser, EventType.ProviderUser_Confirmed, null)); events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email); await _mailService.SendProviderConfirmedEmailAsync(provider.DisplayName(), user.Email);
result.Add(Tuple.Create(providerUser, "")); result.Add(Tuple.Create(providerUser, ""));
} }
catch (BadRequestException e) catch (BadRequestException e)
@ -331,7 +332,7 @@ public class ProviderService : IProviderService
var email = user == null ? providerUser.Email : user.Email; var email = user == null ? providerUser.Email : user.Email;
if (!string.IsNullOrWhiteSpace(email)) if (!string.IsNullOrWhiteSpace(email))
{ {
await _mailService.SendProviderUserRemoved(provider.Name, email); await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);
} }
result.Add(Tuple.Create(providerUser, "")); result.Add(Tuple.Create(providerUser, ""));
@ -374,8 +375,18 @@ public class ProviderService : IProviderService
Key = key, Key = key,
}; };
await ApplyProviderPriceRateAsync(organizationId, providerId); var provider = await _providerRepository.GetByIdAsync(providerId);
await ApplyProviderPriceRateAsync(organization, provider);
await _providerOrganizationRepository.CreateAsync(providerOrganization); await _providerOrganizationRepository.CreateAsync(providerOrganization);
organization.BillingEmail = provider.BillingEmail;
await _organizationRepository.ReplaceAsync(organization);
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Email = provider.BillingEmail
});
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added); await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
} }
@ -400,16 +411,14 @@ public class ProviderService : IProviderService
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null))); await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
} }
private async Task ApplyProviderPriceRateAsync(Guid organizationId, Guid providerId) private async Task ApplyProviderPriceRateAsync(Organization organization, Provider provider)
{ {
var provider = await _providerRepository.GetByIdAsync(providerId);
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan. // if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023) if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
{ {
return; return;
} }
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType)); var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
var extractedPlanType = PlanTypeMappings(organization); var extractedPlanType = PlanTypeMappings(organization);
if (subscriptionItem != null) if (subscriptionItem != null)
@ -586,7 +595,7 @@ public class ProviderService : IProviderService
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var token = _dataProtector.Protect( var token = _dataProtector.Protect(
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}"); $"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email); await _mailService.SendProviderInviteEmailAsync(provider.DisplayName(), providerUser, token, providerUser.Email);
} }
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds) private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)

View File

@ -26,6 +26,7 @@ using IdentityModel;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
using DIM = Duende.IdentityServer.Models; using DIM = Duende.IdentityServer.Models;
namespace Bit.Sso.Controllers; namespace Bit.Sso.Controllers;
@ -483,7 +484,7 @@ public class AccountController : Controller
if (orgUser.Status == OrganizationUserStatusType.Invited) if (orgUser.Status == OrganizationUserStatusType.Invited)
{ {
// Org User is invited - they must manually accept the invite via email and authenticate with MP // Org User is invited - they must manually accept the invite via email and authenticate with MP
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name)); throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.DisplayName()));
} }
// Accepted or Confirmed - create SSO link and return; // Accepted or Confirmed - create SSO link and return;
@ -516,7 +517,7 @@ public class AccountController : Controller
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate); await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
} }
_logger.LogInformation(e, "SSO auto provisioning failed"); _logger.LogInformation(e, "SSO auto provisioning failed");
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name)); throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
} }
} }
} }

View File

@ -458,17 +458,112 @@ public class ProviderServiceTests
{ {
organization.PlanType = PlanType.EnterpriseAnnually; organization.PlanType = PlanType.EnterpriseAnnually;
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await organizationRepository.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
provider.Type = ProviderType.Msp;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
var expectedPlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
var newCreationDate = new DateTime(2023, 11, 5);
BackdateProviderCreationDate(provider, newCreationDate);
provider.Type = ProviderType.Msp;
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Plan = "Enterprise (Annually)";
var expectedPlanType = PlanType.EnterpriseAnnually2020;
var expectedPlanId = "2020-enterprise-org-seat-annually";
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider); sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>(); var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull(); providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key); await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default); await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await sutProvider.GetDependency<IEventService>() await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(), .Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added); EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
@ -576,65 +671,6 @@ public class ProviderServiceTests
t.First().Item2 == null)); t.First().Item2 == null));
} }
[Theory, BitAutoData]
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
provider.Type = ProviderType.Msp;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
var expectedPlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
var newCreationDate = new DateTime(2023, 11, 5);
BackdateProviderCreationDate(provider, newCreationDate);
provider.Type = ProviderType.Msp;
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Plan = "Enterprise (Annually)";
var expectedPlanType = PlanType.EnterpriseAnnually2020;
var expectedPlanId = "2020-enterprise-org-seat-annually";
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) => private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
new() new()
{ {

View File

@ -1,12 +1,12 @@
#!/bin/bash #!/bin/bash
#
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294) # !!! UPDATED 2024 for MsSqlMigratorUtility !!!
#
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost. # where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
# The best workaround seems to be adding tiny delay like so: # The best workaround seems to be adding tiny delay like so:
sleep 0.1; sleep 0.1;
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
LAST_MIGRATION_FILE="/mnt/data/last_migration"
SERVER='mssql' SERVER='mssql'
DATABASE="vault_dev" DATABASE="vault_dev"
USER="SA" USER="SA"
@ -16,58 +16,33 @@ while getopts "s" arg; do
case $arg in case $arg in
s) s)
echo "Running for self-host environment" echo "Running for self-host environment"
LAST_MIGRATION_FILE="/mnt/data/last_self_host_migration"
DATABASE="vault_dev_self_host" DATABASE="vault_dev_self_host"
;; ;;
esac esac
done done
if [ ! -f "$LAST_MIGRATION_FILE" ]; then QUERY="IF OBJECT_ID('[$DATABASE].[dbo].[Migration]') IS NULL AND OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NOT NULL
echo "No migration file, nothing to migrate to a database store"
exit 1
else
LAST_MIGRATION=$(cat $LAST_MIGRATION_FILE)
rm $LAST_MIGRATION_FILE
fi
[ -z "$LAST_MIGRATION" ]
PERFORM_MIGRATION=$?
# Create database if it does not already exist
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
BEGIN BEGIN
CREATE DATABASE migrations_$DATABASE; -- Create [database].dbo.Migration with the schema expected by MsSqlMigratorUtility
END; SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
CREATE TABLE [$DATABASE].[dbo].[Migration](
[Id] [int] IDENTITY(1,1) NOT NULL,
[ScriptName] [nvarchar](255) NOT NULL,
[Applied] [datetime] NOT NULL
) ON [PRIMARY];
ALTER TABLE [$DATABASE].[dbo].[Migration] ADD CONSTRAINT [PK_Migration_Id] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
-- Copy across old data
INSERT INTO [$DATABASE].[dbo].[Migration] (ScriptName, Applied)
SELECT CONCAT('Bit.Migrator.DbScripts.', [Filename]), CreationDate
FROM [migrations_$DATABASE].[dbo].[migrations];
END
" "
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY" /opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
QUERY="IF OBJECT_ID('[dbo].[migrations_$DATABASE]') IS NULL
BEGIN
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
[Id] INT IDENTITY(1,1) PRIMARY KEY,
[Filename] NVARCHAR(MAX) NOT NULL,
[CreationDate] DATETIME2 (7) NULL,
);
END;"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
record_migration () {
echo "recording $1"
local file=$(basename $1)
echo $file
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
}
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
if (( PERFORM_MIGRATION == 0 )); then
echo "Still need to migrate $f"
else
record_migration $f
if [ "$LAST_MIGRATION" == "$f" ]; then
PERFORM_MIGRATION=0
fi
fi
done;

View File

@ -1,94 +0,0 @@
#!/bin/bash
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
# The best workaround seems to be adding tiny delay like so:
sleep 0.1;
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
SERVER='mssql'
DATABASE="vault_dev"
USER="SA"
PASSWD=$MSSQL_PASSWORD
while getopts "sp" arg; do
case $arg in
s)
echo "Running for self-host environment"
DATABASE="vault_dev_self_host"
;;
p)
echo "Running for pipeline"
MIGRATE_DIRECTORY=$MSSQL_MIGRATIONS_DIRECTORY
SERVER=$MSSQL_HOST
DATABASE=$MSSQL_DATABASE
USER=$MSSQL_USER
PASSWD=$MSSQL_PASS
esac
done
# Create databases if they do not already exist
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '$DATABASE')
BEGIN
CREATE DATABASE $DATABASE;
END;
GO
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
BEGIN
CREATE DATABASE migrations_$DATABASE;
END;
GO
"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
echo "Return code: $?"
# Create migrations table if it does not already exist
QUERY="IF OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NULL
BEGIN
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
[Id] INT IDENTITY(1,1) PRIMARY KEY,
[Filename] NVARCHAR(MAX) NOT NULL,
[CreationDate] DATETIME2 (7) NULL,
);
END;
GO
"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$QUERY"
echo "Return code: $?"
should_migrate () {
local file=$(basename $1)
local query="SELECT * FROM [migrations] WHERE [Filename] = '$file'"
local result=$(/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query")
if [[ "$result" =~ .*"$file".* ]]; then
return 1;
else
return 0;
fi
}
record_migration () {
echo "recording $1"
local file=$(basename $1)
echo $file
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
}
migrate () {
local file=$1
echo "Performing $file"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d $DATABASE -U $USER -P $PASSWD -I -i $file
}
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
BASENAME=$(basename $f)
if should_migrate $f == 1 ; then
migrate $f
record_migration $f
else
echo "Skipping $f, $BASENAME"
fi
done;

View File

@ -2,20 +2,20 @@
# Creates the vault_dev database, and runs all the migrations. # Creates the vault_dev database, and runs all the migrations.
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use # Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
# the mssql-tools container which runs under x86_64. We should monitor this # the mssql-tools container which runs under x86_64.
# in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
param( param(
[switch]$all = $false, [switch]$all,
[switch]$postgres = $false, [switch]$postgres,
[switch]$mysql = $false, [switch]$mysql,
[switch]$mssql = $false, [switch]$mssql,
[switch]$sqlite = $false, [switch]$sqlite,
[switch]$selfhost = $false, [switch]$selfhost
[switch]$pipeline = $false
) )
# Abort on any error
$ErrorActionPreference = "Stop"
if (!$all -and !$postgres -and !$mysql -and !$sqlite) { if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
$mssql = $true; $mssql = $true;
} }
@ -29,22 +29,27 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
} }
if ($all -or $mssql) { if ($all -or $mssql) {
if ($selfhost) { function Get-UserSecrets {
$migrationArgs = "-s" return dotnet user-secrets list --json --project ../src/Api | ConvertFrom-Json
} elseif ($pipeline) {
$migrationArgs = "-p"
} }
Write-Host "Starting Microsoft SQL Server Migrations" if ($selfhost) {
docker run ` $msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
-v "$(pwd)/helpers/mssql:/mnt/helpers" ` $envName = "self-host"
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
-v "$(pwd)/.data/mssql:/mnt/data" ` Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
--env-file .env ` ./migrate_migration_record.ps1 -s
--network=bitwardenserver_default ` } else {
--rm ` $msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
mcr.microsoft.com/mssql-tools ` $envName = "cloud"
/mnt/helpers/run_migrations.sh $migrationArgs
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
./migrate_migration_record.ps1
}
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
} }
$currentDir = Get-Location $currentDir = Get-Location

View File

@ -1,15 +1,13 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
# This script need only be run once # !!! UPDATED 2024 for MsSqlMigratorUtility !!!
# #
# This is a migration script for updating recording the last migration run # This is a migration script to move data from [migrations_vault_dev].[dbo].[migrations] (used by our custom
# in a file to recording migrations in a database table. It will create a # migrator script) to [vault_dev].[dbo].[Migration] (used by MsSqlMigratorUtility). It is safe to run multiple
# migrations_vault table and store all of the previously run migrations as # times because it will not perform any migration if it detects that the new table is already present.
# indicated by a last_migrations file. It will then delete this file. # This will be deleted after a few months after everyone has (presumably) migrated to the new schema.
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use # Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
# the mssql-tools container which runs under x86_64. We should monitor this # the mssql-tools container which runs under x86_64.
# in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
docker run ` docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" ` -v "$(pwd)/helpers/mssql:/mnt/helpers" `

View File

@ -1,4 +1,5 @@
using Bit.Admin.AdminConsole.Models; using System.Net;
using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
@ -119,8 +120,9 @@ public class OrganizationsController : Controller
count = 1; count = 1;
} }
var encodedName = WebUtility.HtmlEncode(name);
var skip = (page - 1) * count; var skip = (page - 1) * count;
var organizations = await _organizationRepository.SearchAsync(name, userEmail, paid, skip, count); var organizations = await _organizationRepository.SearchAsync(encodedName, userEmail, paid, skip, count);
return View(new OrganizationsModel return View(new OrganizationsModel
{ {
Items = organizations as List<Organization>, Items = organizations as List<Organization>,

View File

@ -1,4 +1,5 @@
using Bit.Admin.AdminConsole.Models; using System.Net;
using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums; using Bit.Admin.Enums;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core; using Bit.Core;
@ -188,8 +189,9 @@ public class ProvidersController : Controller
count = 1; count = 1;
} }
var encodedName = WebUtility.HtmlEncode(name);
var skip = (page - 1) * count; var skip = (page - 1) * count;
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(name, ownerEmail, skip, count); var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(encodedName, ownerEmail, skip, count);
var viewModel = new OrganizationUnassignedToProviderSearchViewModel var viewModel = new OrganizationUnassignedToProviderSearchViewModel
{ {
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name, OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
@ -199,7 +201,7 @@ public class ProvidersController : Controller
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
{ {
Id = uo.Id, Id = uo.Id,
Name = uo.Name, Name = uo.DisplayName(),
PlanType = uo.PlanType PlanType = uo.PlanType
}).ToList() }).ToList()
}; };

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Net;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
@ -36,8 +37,8 @@ public class OrganizationEditModel : OrganizationViewModel
BillingInfo = billingInfo; BillingInfo = billingInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId; BraintreeMerchantId = globalSettings.Braintree.MerchantId;
Name = org.Name; Name = org.DisplayName();
BusinessName = org.BusinessName; BusinessName = org.DisplayBusinessName();
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail; BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
PlanType = org.PlanType; PlanType = org.PlanType;
Plan = org.Plan; Plan = org.Plan;
@ -184,8 +185,8 @@ public class OrganizationEditModel : OrganizationViewModel
public Organization ToOrganization(Organization existingOrganization) public Organization ToOrganization(Organization existingOrganization)
{ {
existingOrganization.Name = Name; existingOrganization.Name = WebUtility.HtmlEncode(Name.Trim());
existingOrganization.BusinessName = BusinessName; existingOrganization.BusinessName = WebUtility.HtmlEncode(BusinessName.Trim());
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim(); existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
existingOrganization.PlanType = PlanType.Value; existingOrganization.PlanType = PlanType.Value;
existingOrganization.Plan = Plan; existingOrganization.Plan = Plan;

View File

@ -11,8 +11,8 @@ public class ProviderEditModel : ProviderViewModel
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations) public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
: base(provider, providerUsers, organizations) : base(provider, providerUsers, organizations)
{ {
Name = provider.Name; Name = provider.DisplayName();
BusinessName = provider.BusinessName; BusinessName = provider.DisplayBusinessName();
BillingEmail = provider.BillingEmail; BillingEmail = provider.BillingEmail;
BillingPhone = provider.BillingPhone; BillingPhone = provider.BillingPhone;
} }

View File

@ -4,7 +4,7 @@
@inject Bit.Admin.Services.IAccessControlService AccessControlService @inject Bit.Admin.Services.IAccessControlService AccessControlService
@model OrganizationEditModel @model OrganizationEditModel
@{ @{
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name; ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name;
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View); var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View); var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
@ -58,7 +58,7 @@
</script> </script>
} }
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Organization.Name</small></h1> <h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Name</small></h1>
@if (Model.Provider != null) @if (Model.Provider != null)
{ {

View File

@ -46,7 +46,7 @@
{ {
<tr> <tr>
<td> <td>
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.Name</a> <a asp-action="@Model.Action" asp-route-id="@org.Id">@org.DisplayName()</a>
</td> </td>
<td> <td>
@org.Plan @org.Plan

View File

@ -1,10 +1,10 @@
@inject Bit.Core.Settings.GlobalSettings GlobalSettings @inject Bit.Core.Settings.GlobalSettings GlobalSettings
@model OrganizationViewModel @model OrganizationViewModel
@{ @{
ViewData["Title"] = "Organization: " + Model.Organization.Name; ViewData["Title"] = "Organization: " + Model.Organization.DisplayName();
} }
<h1>Organization <small>@Model.Organization.Name</small></h1> <h1>Organization <small>@Model.Organization.DisplayName()</small></h1>
@if (Model.Provider != null) @if (Model.Provider != null)
{ {

View File

@ -2,8 +2,8 @@
@model Bit.Core.AdminConsole.Entities.Provider.Provider @model Bit.Core.AdminConsole.Entities.Provider.Provider
<dl class="row"> <dl class="row">
<dt class="col-sm-4 col-lg-3">Provider Name</dt> <dt class="col-sm-4 col-lg-3">Provider Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Name</dd> <dd class="col-sm-8 col-lg-9">@Model.DisplayName()</dd>
<dt class="col-sm-4 col-lg-3">Provider Type</dt> <dt class="col-sm-4 col-lg-3">Provider Type</dt>
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd> <dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
</dl> </dl>

View File

@ -45,7 +45,7 @@
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" }) @Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
@Html.CheckBoxFor(m => Model.Items[i].Selected) @Html.CheckBoxFor(m => Model.Items[i].Selected)
</td> </td>
<td>@Html.ActionLink(Model.Items[i].Name, "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td> <td>@Html.ActionLink(Model.Items[i].DisplayName(), "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td> <td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
</tr> </tr>
} }

View File

@ -3,12 +3,12 @@
@model ProviderEditModel @model ProviderEditModel
@{ @{
ViewData["Title"] = "Provider: " + Model.Provider.Name; ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit); var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
} }
<h1>Provider <small>@Model.Provider.Name</small></h1> <h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
<h2>Provider Information</h2> <h2>Provider Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)
@ -17,12 +17,12 @@
<h2>General</h2> <h2>General</h2>
<dl class="row"> <dl class="row">
<dt class="col-sm-4 col-lg-3">Name</dt> <dt class="col-sm-4 col-lg-3">Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.Name</dd> <dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayName()</dd>
</dl> </dl>
<h2>Business Information</h2> <h2>Business Information</h2>
<dl class="row"> <dl class="row">
<dt class="col-sm-4 col-lg-3">Business Name</dt> <dt class="col-sm-4 col-lg-3">Business Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.BusinessName</dd> <dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayBusinessName()</dd>
</dl> </dl>
<h2>Billing</h2> <h2>Billing</h2>
<div class="row"> <div class="row">

View File

@ -52,7 +52,7 @@
{ {
<tr> <tr>
<td> <td>
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a> <a asp-action="@Model.Action" asp-route-id="@provider.Id">@(!string.IsNullOrEmpty(provider.DisplayName()) ? provider.DisplayName() : "Pending")</a>
</td> </td>
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td> <td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
<td>@provider.Status</td> <td>@provider.Status</td>

View File

@ -45,7 +45,7 @@
{ {
<tr> <tr>
<td class="align-middle"> <td class="align-middle">
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a> <a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.DisplayName()</a>
</td> </td>
<td> <td>
@providerOrganization.Status @providerOrganization.Status

View File

@ -1,9 +1,9 @@
@model ProviderViewModel @model ProviderViewModel
@{ @{
ViewData["Title"] = "Provider: " + Model.Provider.Name; ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
} }
<h1>Provider <small>@Model.Provider.Name</small></h1> <h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
<h2>Information</h2> <h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model) @await Html.PartialAsync("_ViewInformation", Model)

View File

@ -28,7 +28,7 @@
<div class="col-sm"> <div class="col-sm">
<div class="form-group"> <div class="form-group">
<label asp-for="Name"></label> <label asp-for="Name"></label>
<input type="text" class="form-control" asp-for="Name" required> <input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
</div> </div>
</div> </div>
</div> </div>
@ -68,7 +68,7 @@
<div class="col-sm"> <div class="col-sm">
<div class="form-group"> <div class="form-group">
<label asp-for="BusinessName"></label> <label asp-for="BusinessName"></label>
<input type="text" class="form-control" asp-for="BusinessName"> <input type="text" class="form-control" asp-for="BusinessName" value="@Model.BusinessName">
</div> </div>
</div> </div>
</div> </div>

View File

@ -87,6 +87,7 @@ public static class RolePermissionMapping
Permission.Provider_List_View, Permission.Provider_List_View,
Permission.Provider_Create, Permission.Provider_Create,
Permission.Provider_View, Permission.Provider_View,
Permission.Provider_Edit,
Permission.Provider_ResendEmailInvite, Permission.Provider_ResendEmailInvite,
Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_ChargeBrainTreeCustomer,
Permission.Tools_PromoteAdmin, Permission.Tools_PromoteAdmin,

View File

@ -4,6 +4,7 @@ using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers; using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
using Bit.Core;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
@ -41,6 +42,7 @@ public class OrganizationUsersController : Controller
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IAuthorizationService _authorizationService; private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly IFeatureService _featureService;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -56,7 +58,8 @@ public class OrganizationUsersController : Controller
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
IAcceptOrgUserCommand acceptOrgUserCommand, IAcceptOrgUserCommand acceptOrgUserCommand,
IAuthorizationService authorizationService, IAuthorizationService authorizationService,
IApplicationCacheService applicationCacheService) IApplicationCacheService applicationCacheService,
IFeatureService featureService)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -72,6 +75,7 @@ public class OrganizationUsersController : Controller
_acceptOrgUserCommand = acceptOrgUserCommand; _acceptOrgUserCommand = acceptOrgUserCommand;
_authorizationService = authorizationService; _authorizationService = authorizationService;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_featureService = featureService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -305,43 +309,34 @@ public class OrganizationUsersController : Controller
[HttpPut("{id}")] [HttpPut("{id}")]
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task Put(string orgId, string id, [FromBody] OrganizationUserUpdateRequestModel model) public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{ {
var orgGuidId = new Guid(orgId); if (!await _currentContext.ManageUsers(orgId))
if (!await _currentContext.ManageUsers(orgGuidId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId) if (organizationUser == null || organizationUser.OrganizationId != orgId)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var userId = _userService.GetProperUserId(User); // If admins are not allowed access to all collections, you cannot add yourself to a group
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value, // In this case we just don't update groups
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups); var userId = _userService.GetProperUserId(User).Value;
} var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
var restrictEditingGroups = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
organizationAbility.FlexibleCollections &&
userId == organizationUser.UserId &&
!organizationAbility.AllowAdminAccessToAllCollectionItems;
[HttpPut("{id}/groups")] var groups = restrictEditingGroups
[HttpPost("{id}/groups")] ? null
public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUserUpdateGroupsRequestModel model) : model.Groups;
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId,
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId) model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), groups);
{
throw new NotFoundException();
}
var loggedInUserId = _userService.GetProperUserId(User);
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
} }
[HttpPut("{userId}/reset-password-enrollment")] [HttpPut("{userId}/reset-password-enrollment")]

View File

@ -261,19 +261,19 @@ public class OrganizationsController : Controller
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false); return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
} }
[HttpGet("{id}/risks-subscription-failure")] [HttpGet("{id}/billing-status")]
public async Task<OrganizationRisksSubscriptionFailureResponseModel> RisksSubscriptionFailure(Guid id) public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
{ {
if (!await _currentContext.EditPaymentMethods(id)) if (!await _currentContext.EditPaymentMethods(id))
{ {
return new OrganizationRisksSubscriptionFailureResponseModel(id, false); throw new NotFoundException();
} }
var organization = await _organizationRepository.GetByIdAsync(id); var organization = await _organizationRepository.GetByIdAsync(id);
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization); var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
return new OrganizationRisksSubscriptionFailureResponseModel(id, risksSubscriptionFailure); return new OrganizationBillingStatusResponseModel(organization, risksSubscriptionFailure);
} }
[HttpPost("")] [HttpPost("")]
@ -303,7 +303,7 @@ public class OrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName || var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() ||
model.BillingEmail != organization.BillingEmail); model.BillingEmail != organization.BillingEmail);
var hasRequiredPermissions = updateBilling var hasRequiredPermissions = updateBilling
@ -464,8 +464,8 @@ public class OrganizationsController : Controller
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value); await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
} }
[HttpPost("{id}/churn")] [HttpPost("{id}/cancel")]
public async Task PostChurn(Guid id, [FromBody] SubscriptionCancellationRequestModel request) public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
{ {
if (!await _currentContext.EditSubscription(id)) if (!await _currentContext.EditSubscription(id))
{ {
@ -499,19 +499,6 @@ public class OrganizationsController : Controller
}); });
} }
[HttpPost("{id}/cancel")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostCancel(string id)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.CancelSubscriptionAsync(orgIdGuid);
}
[HttpPost("{id}/reinstate")] [HttpPost("{id}/reinstate")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task PostReinstate(string id) public async Task PostReinstate(string id)

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
@ -9,9 +10,11 @@ namespace Bit.Api.AdminConsole.Models.Request.Organizations;
public class OrganizationCreateRequestModel : IValidatableObject public class OrganizationCreateRequestModel : IValidatableObject
{ {
[Required] [Required]
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; } public string BusinessName { get; set; }
[Required] [Required]
[StringLength(256)] [StringLength(256)]

View File

@ -1,16 +1,20 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request.Organizations; namespace Bit.Api.AdminConsole.Models.Request.Organizations;
public class OrganizationUpdateRequestModel public class OrganizationUpdateRequestModel
{ {
[Required] [Required]
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; } public string BusinessName { get; set; }
[EmailAddress] [EmailAddress]
[Required] [Required]

View File

@ -102,12 +102,6 @@ public class OrganizationUserUpdateRequestModel
} }
} }
public class OrganizationUserUpdateGroupsRequestModel
{
[Required]
public IEnumerable<string> GroupIds { get; set; }
}
public class OrganizationUserResetPasswordEnrollmentRequestModel public class OrganizationUserResetPasswordEnrollmentRequestModel
{ {
public string ResetPasswordKey { get; set; } public string ResetPasswordKey { get; set; }

View File

@ -1,14 +1,18 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request.Providers; namespace Bit.Api.AdminConsole.Models.Request.Providers;
public class ProviderSetupRequestModel public class ProviderSetupRequestModel
{ {
[Required] [Required]
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; } public string BusinessName { get; set; }
[Required] [Required]
[StringLength(256)] [StringLength(256)]

View File

@ -1,15 +1,19 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request.Providers; namespace Bit.Api.AdminConsole.Models.Request.Providers;
public class ProviderUpdateRequestModel public class ProviderUpdateRequestModel
{ {
[Required] [Required]
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
[StringLength(50)] [StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; } public string BusinessName { get; set; }
[EmailAddress] [EmailAddress]
[Required] [Required]

View File

@ -0,0 +1,13 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Api;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationBillingStatusResponseModel(
Organization organization,
bool risksSubscriptionFailure) : ResponseModel("organizationBillingStatus")
{
public Guid OrganizationId { get; } = organization.Id;
public string OrganizationName { get; } = organization.Name;
public bool RisksSubscriptionFailure { get; } = risksSubscriptionFailure;
}

View File

@ -1,4 +1,5 @@
using Bit.Api.Models.Response; using System.Text.Json.Serialization;
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
@ -60,7 +61,9 @@ public class OrganizationResponseModel : ResponseModel
} }
public Guid Id { get; set; } public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; } public string BusinessName { get; set; }
public string BusinessAddress1 { get; set; } public string BusinessAddress1 { get; set; }
public string BusinessAddress2 { get; set; } public string BusinessAddress2 { get; set; }

View File

@ -1,17 +0,0 @@
using Bit.Core.Models.Api;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationRisksSubscriptionFailureResponseModel : ResponseModel
{
public Guid OrganizationId { get; }
public bool RisksSubscriptionFailure { get; }
public OrganizationRisksSubscriptionFailureResponseModel(
Guid organizationId,
bool risksSubscriptionFailure) : base("organizationRisksSubscriptionFailure")
{
OrganizationId = organizationId;
RisksSubscriptionFailure = risksSubscriptionFailure;
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -103,6 +104,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
} }
public Guid Id { get; set; } public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public bool UsePolicies { get; set; } public bool UsePolicies { get; set; }
public bool UseSso { get; set; } public bool UseSso { get; set; }
@ -135,6 +137,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
public bool HasPublicAndPrivateKeys { get; set; } public bool HasPublicAndPrivateKeys { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string ProviderName { get; set; } public string ProviderName { get; set; }
public ProviderType? ProviderType { get; set; } public ProviderType? ProviderType { get; set; }
public string FamilySponsorshipFriendlyName { get; set; } public string FamilySponsorshipFriendlyName { get; set; }

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -23,6 +24,7 @@ public class ProfileProviderResponseModel : ResponseModel
} }
public Guid Id { get; set; } public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public string Key { get; set; } public string Key { get; set; }
public ProviderUserStatusType Status { get; set; } public ProviderUserStatusType Status { get; set; }

View File

@ -1,6 +1,8 @@
using Bit.Core.AdminConsole.Entities.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Response.Providers; namespace Bit.Api.AdminConsole.Models.Response.Providers;
@ -68,5 +70,6 @@ public class ProviderOrganizationOrganizationDetailsResponseModel : ProviderOrga
OrganizationName = providerOrganization.OrganizationName; OrganizationName = providerOrganization.OrganizationName;
} }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string OrganizationName { get; set; } public string OrganizationName { get; set; }
} }

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Entities.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Response.Providers; namespace Bit.Api.AdminConsole.Models.Response.Providers;
@ -25,6 +27,7 @@ public class ProviderResponseModel : ResponseModel
} }
public Guid Id { get; set; } public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public string BusinessName { get; set; } public string BusinessName { get; set; }
public string BusinessAddress1 { get; set; } public string BusinessAddress1 { get; set; }

View File

@ -32,15 +32,10 @@
</Choose> </Choose>
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" /> <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" /> <PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.Network" Version="6.0.4" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="6.0.4" />
<PackageReference Include="AspNetCore.HealthChecks.SendGrid" Version="6.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="6.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="6.0.3" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.10.0" /> <PackageReference Include="Azure.Messaging.EventGrid" Version="4.10.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -821,8 +821,8 @@ public class AccountsController : Controller
await _userService.UpdateLicenseAsync(user, license); await _userService.UpdateLicenseAsync(user, license);
} }
[HttpPost("churn-premium")] [HttpPost("cancel")]
public async Task PostChurn([FromBody] SubscriptionCancellationRequestModel request) public async Task PostCancel([FromBody] SubscriptionCancellationRequestModel request)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
@ -851,19 +851,6 @@ public class AccountsController : Controller
}); });
} }
[HttpPost("cancel-premium")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostCancel()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _userService.CancelPremiumAsync(user);
}
[HttpPost("reinstate-premium")] [HttpPost("reinstate-premium")]
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task PostReinstate() public async Task PostReinstate()

View File

@ -132,7 +132,7 @@ public class OrganizationSponsorshipsController : Controller
} }
var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch); var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch);
await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.Name, offersToSend); await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.DisplayName(), offersToSend);
return new OrganizationSponsorshipSyncResponseModel(syncResponseData); return new OrganizationSponsorshipSyncResponseModel(syncResponseData);
} }

View File

@ -58,7 +58,7 @@ public class SelfHostedSponsorshipSyncJob : BaseJob
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, $"Sponsorship sync for organization {org.Name} Failed"); _logger.LogError(ex, "Sponsorship sync for organization {OrganizationName} Failed", org.DisplayName());
} }
} }
} }

View File

@ -92,34 +92,6 @@ public static class ServiceCollectionExtensions
{ {
builder.AddSqlServer(globalSettings.SqlServer.ConnectionString); builder.AddSqlServer(globalSettings.SqlServer.ConnectionString);
} }
if (CoreHelpers.SettingHasValue(globalSettings.DistributedCache?.Redis?.ConnectionString))
{
builder.AddRedis(globalSettings.DistributedCache.Redis.ConnectionString);
}
if (CoreHelpers.SettingHasValue(globalSettings.Storage.ConnectionString))
{
builder.AddAzureQueueStorage(globalSettings.Storage.ConnectionString, name: "storage_queue")
.AddAzureQueueStorage(globalSettings.Events.ConnectionString, name: "events_queue");
}
if (CoreHelpers.SettingHasValue(globalSettings.Notifications.ConnectionString))
{
builder.AddAzureQueueStorage(globalSettings.Notifications.ConnectionString,
name: "notifications_queue");
}
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString))
{
builder.AddAzureServiceBusTopic(_ => globalSettings.ServiceBus.ConnectionString,
_ => globalSettings.ServiceBus.ApplicationCacheTopicName, name: "service_bus");
}
if (CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey))
{
builder.AddSendGrid(globalSettings.Mail.SendGridApiKey);
}
}); });
} }

View File

@ -95,7 +95,7 @@ public class FreshsalesController : Controller
foreach (var org in orgs) foreach (var org in orgs)
{ {
noteItems.Add($"Org, {org.Name}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}"); noteItems.Add($"Org, {org.DisplayName()}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}");
if (TryGetPlanName(org.PlanType, out var planName)) if (TryGetPlanName(org.PlanType, out var planName))
{ {
newTags.Add($"Org: {planName}"); newTags.Add($"Org: {planName}");

View File

@ -1,6 +1,5 @@
using System.Text; using System.Text;
using Bit.Billing.Models; using Bit.Billing.Models;
using Bit.Billing.Services;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -20,7 +19,6 @@ public class PayPalController : Controller
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly IPayPalIPNClient _payPalIPNClient;
private readonly ITransactionRepository _transactionRepository; private readonly ITransactionRepository _transactionRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
@ -30,7 +28,6 @@ public class PayPalController : Controller
IMailService mailService, IMailService mailService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IPaymentService paymentService, IPaymentService paymentService,
IPayPalIPNClient payPalIPNClient,
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
IUserRepository userRepository) IUserRepository userRepository)
{ {
@ -39,7 +36,6 @@ public class PayPalController : Controller
_mailService = mailService; _mailService = mailService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_paymentService = paymentService; _paymentService = paymentService;
_payPalIPNClient = payPalIPNClient;
_transactionRepository = transactionRepository; _transactionRepository = transactionRepository;
_userRepository = userRepository; _userRepository = userRepository;
} }
@ -91,14 +87,6 @@ public class PayPalController : Controller
return BadRequest(); return BadRequest();
} }
var verified = await _payPalIPNClient.VerifyIPN(transactionModel.TransactionId, requestContent);
if (!verified)
{
_logger.LogError("PayPal IPN ({Id}): Verification failed", transactionModel.TransactionId);
return BadRequest();
}
if (transactionModel.TransactionType != "web_accept" && if (transactionModel.TransactionType != "web_accept" &&
transactionModel.TransactionType != "merch_pmt" && transactionModel.TransactionType != "merch_pmt" &&
transactionModel.PaymentStatus != "Refunded") transactionModel.PaymentStatus != "Refunded")
@ -204,8 +192,8 @@ public class PayPalController : Controller
if (parentTransaction == null) if (parentTransaction == null)
{ {
_logger.LogError("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId); _logger.LogWarning("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
return BadRequest(); return Ok();
} }
var refundAmount = Math.Abs(transactionModel.MerchantGross); var refundAmount = Math.Abs(transactionModel.MerchantGross);

View File

@ -3,6 +3,7 @@ using Bit.Billing.Models;
using Bit.Billing.Services; using Bit.Billing.Services;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -188,7 +189,7 @@ public class StripeController : Controller
} }
var user = await _userService.GetUserByIdAsync(userId); var user = await _userService.GetUserByIdAsync(userId);
if (user.Premium) if (user?.Premium == true)
{ {
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd); await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
} }
@ -250,21 +251,21 @@ public class StripeController : Controller
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax); var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
if (pm5766AutomaticTaxIsEnabled) if (pm5766AutomaticTaxIsEnabled)
{ {
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId); var customerGetOptions = new CustomerGetOptions();
customerGetOptions.AddExpand("tax");
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions);
if (!subscription.AutomaticTax.Enabled && if (!subscription.AutomaticTax.Enabled &&
!string.IsNullOrEmpty(customer.Address?.PostalCode) && customer.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported)
!string.IsNullOrEmpty(customer.Address?.Country))
{ {
subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions new SubscriptionUpdateOptions
{ {
DefaultTaxRates = new List<string>(), DefaultTaxRates = [],
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
}); });
} }
} }
var updatedSubscription = pm5766AutomaticTaxIsEnabled var updatedSubscription = pm5766AutomaticTaxIsEnabled
? subscription ? subscription
: await VerifyCorrectTaxRateForCharge(invoice, subscription); : await VerifyCorrectTaxRateForCharge(invoice, subscription);
@ -319,7 +320,7 @@ public class StripeController : Controller
{ {
var user = await _userService.GetUserByIdAsync(userId.Value); var user = await _userService.GetUserByIdAsync(userId.Value);
if (user.Premium) if (user?.Premium == true)
{ {
await SendEmails(new List<string> { user.Email }); await SendEmails(new List<string> { user.Email });
} }
@ -571,7 +572,7 @@ public class StripeController : Controller
else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated)) else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated))
{ {
var customer = var customer =
await _stripeEventService.GetCustomer(parsedEvent, true, new List<string> { "subscriptions" }); await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
if (customer.Subscriptions == null || !customer.Subscriptions.Any()) if (customer.Subscriptions == null || !customer.Subscriptions.Any())
{ {
@ -614,7 +615,7 @@ public class StripeController : Controller
{ {
Customer = paymentMethod.CustomerId, Customer = paymentMethod.CustomerId,
Status = StripeSubscriptionStatus.Unpaid, Status = StripeSubscriptionStatus.Unpaid,
Expand = new List<string> { "data.latest_invoice" } Expand = ["data.latest_invoice"]
}; };
StripeList<Subscription> unpaidSubscriptions; StripeList<Subscription> unpaidSubscriptions;
@ -672,9 +673,9 @@ public class StripeController : Controller
} }
} }
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData) private static Tuple<Guid?, Guid?> GetIdsFromMetaData(Dictionary<string, string> metaData)
{ {
if (metaData == null || !metaData.Any()) if (metaData == null || metaData.Count == 0)
{ {
return new Tuple<Guid?, Guid?>(null, null); return new Tuple<Guid?, Guid?>(null, null);
} }
@ -682,29 +683,35 @@ public class StripeController : Controller
Guid? orgId = null; Guid? orgId = null;
Guid? userId = null; Guid? userId = null;
if (metaData.ContainsKey("organizationId")) if (metaData.TryGetValue("organizationId", out var orgIdString))
{ {
orgId = new Guid(metaData["organizationId"]); orgId = new Guid(orgIdString);
} }
else if (metaData.ContainsKey("userId")) else if (metaData.TryGetValue("userId", out var userIdString))
{ {
userId = new Guid(metaData["userId"]); userId = new Guid(userIdString);
} }
if (userId == null && orgId == null) if (userId != null && userId != Guid.Empty || orgId != null && orgId != Guid.Empty)
{ {
var orgIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "organizationid"); return new Tuple<Guid?, Guid?>(orgId, userId);
if (!string.IsNullOrWhiteSpace(orgIdKey)) }
var orgIdKey = metaData.Keys
.FirstOrDefault(k => k.Equals("organizationid", StringComparison.InvariantCultureIgnoreCase));
if (!string.IsNullOrWhiteSpace(orgIdKey))
{
orgId = new Guid(metaData[orgIdKey]);
}
else
{
var userIdKey = metaData.Keys
.FirstOrDefault(k => k.Equals("userid", StringComparison.InvariantCultureIgnoreCase));
if (!string.IsNullOrWhiteSpace(userIdKey))
{ {
orgId = new Guid(metaData[orgIdKey]); userId = new Guid(metaData[userIdKey]);
}
else
{
var userIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "userid");
if (!string.IsNullOrWhiteSpace(userIdKey))
{
userId = new Guid(metaData[userIdKey]);
}
} }
} }
@ -891,9 +898,9 @@ public class StripeController : Controller
return subscription; return subscription;
} }
subscription.DefaultTaxRates = new List<Stripe.TaxRate> { stripeTaxRate }; subscription.DefaultTaxRates = [stripeTaxRate];
var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = new List<string> { stripeTaxRate.Id } }; var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = [stripeTaxRate.Id] };
subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionOptions); subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionOptions);
return subscription; return subscription;

View File

@ -25,7 +25,10 @@ public class PayPalIPNTransactionModel
var data = queryString var data = queryString
.AllKeys .AllKeys
.ToDictionary(key => key, key => queryString[key]); .Where(key => !string.IsNullOrWhiteSpace(key))
.ToDictionary(key =>
key.Trim('\r'),
key => queryString[key]?.Trim('\r'));
TransactionId = Extract(data, "txn_id"); TransactionId = Extract(data, "txn_id");
TransactionType = Extract(data, "txn_type"); TransactionType = Extract(data, "txn_type");

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Text.Json; using System.Text.Json;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
@ -17,8 +18,14 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
public Guid Id { get; set; } public Guid Id { get; set; }
[MaxLength(50)] [MaxLength(50)]
public string Identifier { get; set; } public string Identifier { get; set; }
/// <summary>
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
/// </summary>
[MaxLength(50)] [MaxLength(50)]
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
/// </summary>
[MaxLength(50)] [MaxLength(50)]
public string BusinessName { get; set; } public string BusinessName { get; set; }
[MaxLength(50)] [MaxLength(50)]
@ -104,6 +111,22 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
} }
} }
/// <summary>
/// Returns the name of the organization, HTML decoded ready for display.
/// </summary>
public string DisplayName()
{
return WebUtility.HtmlDecode(Name);
}
/// <summary>
/// Returns the business name of the organization, HTML decoded ready for display.
/// </summary>
public string DisplayBusinessName()
{
return WebUtility.HtmlDecode(BusinessName);
}
public string BillingEmailAddress() public string BillingEmailAddress()
{ {
return BillingEmail?.ToLowerInvariant()?.Trim(); return BillingEmail?.ToLowerInvariant()?.Trim();
@ -111,12 +134,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
public string BillingName() public string BillingName()
{ {
return BusinessName; return DisplayBusinessName();
} }
public string SubscriberName() public string SubscriberName()
{ {
return Name; return DisplayName();
} }
public string BraintreeCustomerIdPrefix() public string BraintreeCustomerIdPrefix()

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Net;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Entities.Provider; namespace Bit.Core.AdminConsole.Entities.Provider;
@ -7,7 +9,13 @@ namespace Bit.Core.AdminConsole.Entities.Provider;
public class Provider : ITableObject<Guid> public class Provider : ITableObject<Guid>
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary>
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
/// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
/// </summary>
public string BusinessName { get; set; } public string BusinessName { get; set; }
public string BusinessAddress1 { get; set; } public string BusinessAddress1 { get; set; }
public string BusinessAddress2 { get; set; } public string BusinessAddress2 { get; set; }
@ -22,6 +30,9 @@ public class Provider : ITableObject<Guid>
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public GatewayType? Gateway { get; set; }
public string GatewayCustomerId { get; set; }
public string GatewaySubscriptionId { get; set; }
public void SetNewId() public void SetNewId()
{ {
@ -30,4 +41,20 @@ public class Provider : ITableObject<Guid>
Id = CoreHelpers.GenerateComb(); Id = CoreHelpers.GenerateComb();
} }
} }
/// <summary>
/// Returns the name of the provider, HTML decoded ready for display.
/// </summary>
public string DisplayName()
{
return WebUtility.HtmlDecode(Name);
}
/// <summary>
/// Returns the business name of the provider, HTML decoded ready for display.
/// </summary>
public string DisplayBusinessName()
{
return WebUtility.HtmlDecode(BusinessName);
}
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -6,6 +8,7 @@ public class OrganizationUserOrganizationDetails
{ {
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public bool UsePolicies { get; set; } public bool UsePolicies { get; set; }
public bool UseSso { get; set; } public bool UseSso { get; set; }
@ -37,6 +40,7 @@ public class OrganizationUserOrganizationDetails
public string PublicKey { get; set; } public string PublicKey { get; set; }
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string ProviderName { get; set; } public string ProviderName { get; set; }
public ProviderType? ProviderType { get; set; } public ProviderType? ProviderType { get; set; }
public string FamilySponsorshipFriendlyName { get; set; } public string FamilySponsorshipFriendlyName { get; set; }

View File

@ -1,4 +1,7 @@
using Bit.Core.Enums; using System.Net;
using System.Text.Json.Serialization;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Provider; namespace Bit.Core.AdminConsole.Models.Data.Provider;
@ -7,6 +10,10 @@ public class ProviderOrganizationOrganizationDetails
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid ProviderId { get; set; } public Guid ProviderId { get; set; }
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
/// <summary>
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
/// </summary>
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string OrganizationName { get; set; } public string OrganizationName { get; set; }
public string Key { get; set; } public string Key { get; set; }
public string Settings { get; set; } public string Settings { get; set; }
@ -16,4 +23,12 @@ public class ProviderOrganizationOrganizationDetails
public int? Seats { get; set; } public int? Seats { get; set; }
public string Plan { get; set; } public string Plan { get; set; }
public OrganizationStatusType Status { get; set; } public OrganizationStatusType Status { get; set; }
/// <summary>
/// Returns the name of the organization, HTML decoded ready for display.
/// </summary>
public string DisplayName()
{
return WebUtility.HtmlDecode(OrganizationName);
}
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Provider; namespace Bit.Core.AdminConsole.Models.Data.Provider;
@ -7,6 +9,7 @@ public class ProviderOrganizationProviderDetails
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid ProviderId { get; set; } public Guid ProviderId { get; set; }
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string ProviderName { get; set; } public string ProviderName { get; set; }
public ProviderType ProviderType { get; set; } public ProviderType ProviderType { get; set; }
} }

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Provider; namespace Bit.Core.AdminConsole.Models.Data.Provider;
@ -6,6 +8,7 @@ public class ProviderUserOrganizationDetails
{ {
public Guid OrganizationId { get; set; } public Guid OrganizationId { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public bool UsePolicies { get; set; } public bool UsePolicies { get; set; }
public bool UseSso { get; set; } public bool UseSso { get; set; }
@ -33,6 +36,7 @@ public class ProviderUserOrganizationDetails
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
public Guid? ProviderId { get; set; } public Guid? ProviderId { get; set; }
public Guid? ProviderUserId { get; set; } public Guid? ProviderUserId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string ProviderName { get; set; } public string ProviderName { get; set; }
public Core.Enums.PlanType PlanType { get; set; } public Core.Enums.PlanType PlanType { get; set; }
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; }

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Provider; namespace Bit.Core.AdminConsole.Models.Data.Provider;
@ -6,6 +8,7 @@ public class ProviderUserProviderDetails
{ {
public Guid ProviderId { get; set; } public Guid ProviderId { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public string Key { get; set; } public string Key { get; set; }
public ProviderUserStatusType Status { get; set; } public ProviderUserStatusType Status { get; set; }

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums.Provider; using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.Models.Data.Provider; namespace Bit.Core.AdminConsole.Models.Data.Provider;
@ -7,6 +9,7 @@ public class ProviderUserUserDetails
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid ProviderId { get; set; } public Guid ProviderId { get; set; }
public Guid? UserId { get; set; } public Guid? UserId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; } public string Name { get; set; }
public string Email { get; set; } public string Email { get; set; }
public ProviderUserStatusType Status { get; set; } public ProviderUserStatusType Status { get; set; }

View File

@ -537,6 +537,7 @@ public class OrganizationService : IOrganizationService
Storage = returnValue.Item1.MaxStorageGb, Storage = returnValue.Item1.MaxStorageGb,
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481 // TODO: add reference events for SmSeats and Service Accounts - see AC-1481
}); });
return returnValue; return returnValue;
} }
@ -819,7 +820,7 @@ public class OrganizationService : IOrganizationService
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{ {
Email = organization.BillingEmail, Email = organization.BillingEmail,
Description = organization.BusinessName Description = organization.DisplayBusinessName()
}); });
} }
} }
@ -1276,7 +1277,7 @@ public class OrganizationService : IOrganizationService
orgUser.Email = null; orgUser.Email = null;
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await _mailService.SendOrganizationConfirmedEmailAsync(organization.Name, user.Email); await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email);
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id); await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
succeededUsers.Add(orgUser); succeededUsers.Add(orgUser);
result.Add(Tuple.Create(orgUser, "")); result.Add(Tuple.Create(orgUser, ""));
@ -1412,18 +1413,18 @@ public class OrganizationService : IOrganizationService
} }
// If the organization is using Flexible Collections, prevent use of any deprecated permissions // If the organization is using Flexible Collections, prevent use of any deprecated permissions
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(user.OrganizationId); var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
if (organizationAbility?.FlexibleCollections == true && user.Type == OrganizationUserType.Manager) if (organization.FlexibleCollections && user.Type == OrganizationUserType.Manager)
{ {
throw new BadRequestException("The Manager role has been deprecated by collection enhancements. Use the collection Can Manage permission instead."); throw new BadRequestException("The Manager role has been deprecated by collection enhancements. Use the collection Can Manage permission instead.");
} }
if (organizationAbility?.FlexibleCollections == true && user.AccessAll) if (organization.FlexibleCollections && user.AccessAll)
{ {
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the user to collections instead."); throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the user to collections instead.");
} }
if (organizationAbility?.FlexibleCollections == true && collections?.Any() == true) if (organization.FlexibleCollections && collections?.Any() == true)
{ {
var invalidAssociations = collections.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords)); var invalidAssociations = collections.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
if (invalidAssociations.Any()) if (invalidAssociations.Any())
@ -1440,7 +1441,6 @@ public class OrganizationService : IOrganizationService
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1); var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1);
if (additionalSmSeatsRequired > 0) if (additionalSmSeatsRequired > 0)
{ {
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
var update = new SecretsManagerSubscriptionUpdate(organization, true) var update = new SecretsManagerSubscriptionUpdate(organization, true)
.AdjustSeats(additionalSmSeatsRequired); .AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update); await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);

View File

@ -124,14 +124,21 @@ public class PolicyService : IPolicyService
switch (policy.Type) switch (policy.Type)
{ {
case PolicyType.TwoFactorAuthentication: case PolicyType.TwoFactorAuthentication:
foreach (var orgUser in removableOrgUsers) // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{ {
if (!await userService.TwoFactorIsEnabledAsync(orgUser)) if (!await userService.TwoFactorIsEnabledAsync(orgUser))
{ {
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id, await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId); savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org.Name, orgUser.Email); org.DisplayName(), orgUser.Email);
} }
} }
break; break;
@ -147,7 +154,7 @@ public class PolicyService : IPolicyService
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id, await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId); savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.Name, orgUser.Email); org.DisplayName(), orgUser.Email);
} }
} }
break; break;

View File

@ -4,6 +4,7 @@ using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens; using Bit.Core.Tokens;
using Microsoft.Extensions.Logging;
using Duo = DuoUniversal; using Duo = DuoUniversal;
namespace Bit.Core.Auth.Identity; namespace Bit.Core.Auth.Identity;
@ -25,6 +26,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory; private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
private readonly ILogger<TemporaryDuoWebV4SDKService> _logger;
/// <summary> /// <summary>
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK /// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
@ -34,11 +36,13 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
public TemporaryDuoWebV4SDKService( public TemporaryDuoWebV4SDKService(
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory) IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
ILogger<TemporaryDuoWebV4SDKService> logger)
{ {
_currentContext = currentContext; _currentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_tokenDataFactory = tokenDataFactory; _tokenDataFactory = tokenDataFactory;
_logger = logger;
} }
/// <summary> /// <summary>
@ -129,8 +133,9 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
(string)provider.MetaData["Host"], (string)provider.MetaData["Host"],
redirectUri).Build(); redirectUri).Build();
if (!await client.DoHealthCheck()) if (!await client.DoHealthCheck(true))
{ {
_logger.LogError("Unable to connect to Duo. Health check failed.");
return null; return null;
} }
return client; return client;

View File

@ -103,19 +103,27 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
// established ownership in this context. // established ownership in this context.
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true); IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback); try
{
var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback);
provider.MetaData.Remove("login"); provider.MetaData.Remove("login");
// Update SignatureCounter // Update SignatureCounter
webAuthCred.Item2.SignatureCounter = res.Counter; webAuthCred.Item2.SignatureCounter = res.Counter;
var providers = user.GetTwoFactorProviders(); var providers = user.GetTwoFactorProviders();
providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2; providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2;
user.SetTwoFactorProviders(providers); user.SetTwoFactorProviders(providers);
await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false); await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false);
return res.Status == "ok";
}
catch (Fido2VerificationException)
{
return false;
}
return res.Status == "ok";
} }
private bool HasProperMetaData(TwoFactorProvider provider) private bool HasProperMetaData(TwoFactorProvider provider)

View File

@ -0,0 +1,9 @@
namespace Bit.Core.Billing.Constants;
public static class StripeCustomerAutomaticTaxStatus
{
public const string Failed = "failed";
public const string NotCollecting = "not_collecting";
public const string Supported = "supported";
public const string UnrecognizedLocation = "unrecognized_location";
}

View File

@ -0,0 +1,23 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.Billing.Entities;
public class ProviderPlan : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid ProviderId { get; set; }
public PlanType PlanType { get; set; }
public int? SeatMinimum { get; set; }
public int? PurchasedSeats { get; set; }
public int? AllocatedSeats { get; set; }
public void SetNewId()
{
if (Id == default)
{
Id = CoreHelpers.GenerateComb();
}
}
}

View File

@ -0,0 +1,9 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Repositories;
namespace Bit.Core.Billing.Repositories;
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
{
Task<ProviderPlan> GetByProviderId(Guid providerId);
}

View File

@ -108,7 +108,6 @@ public static class FeatureFlagKeys
public const string TrustedDeviceEncryption = "trusted-device-encryption"; public const string TrustedDeviceEncryption = "trusted-device-encryption";
public const string Fido2VaultCredentials = "fido2-vault-credentials"; public const string Fido2VaultCredentials = "fido2-vault-credentials";
public const string VaultOnboarding = "vault-onboarding"; public const string VaultOnboarding = "vault-onboarding";
public const string AutofillV2 = "autofill-v2";
public const string BrowserFilelessImport = "browser-fileless-import"; public const string BrowserFilelessImport = "browser-fileless-import";
/// <summary> /// <summary>
/// Deprecated - never used, do not use. Will always default to false. Will be deleted as part of Flexible Collections cleanup /// Deprecated - never used, do not use. Will always default to false. Will be deleted as part of Flexible Collections cleanup
@ -116,7 +115,6 @@ public static class FeatureFlagKeys
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use"; public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
public const string BulkCollectionAccess = "bulk-collection-access"; public const string BulkCollectionAccess = "bulk-collection-access";
public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share"; public const string ItemShare = "item-share";
public const string KeyRotationImprovements = "key-rotation-improvements"; public const string KeyRotationImprovements = "key-rotation-improvements";
public const string DuoRedirect = "duo-redirect"; public const string DuoRedirect = "duo-redirect";
@ -129,9 +127,10 @@ public static class FeatureFlagKeys
/// flexible collections /// flexible collections
/// </summary> /// </summary>
public const string FlexibleCollectionsMigration = "flexible-collections-migration"; public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string AC1607_PresentUsersWithOffboardingSurvey = "AC-1607_present-user-offboarding-survey";
public const string PM5766AutomaticTax = "PM-5766-automatic-tax"; public const string PM5766AutomaticTax = "PM-5766-automatic-tax";
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold"; public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -21,8 +21,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" /> <PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.52" /> <PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.59" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.52" /> <PackageReference Include="AWSSDK.SQS" Version="3.7.300.59" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" /> <PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" /> <PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" /> <PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
@ -32,22 +32,22 @@
<PackageReference Include="DnsClient" Version="1.7.0" /> <PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" /> <PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="Handlebars.Net" Version="2.1.4" /> <PackageReference Include="Handlebars.Net" Version="2.1.4" />
<PackageReference Include="MailKit" Version="4.3.0" /> <PackageReference Include="MailKit" Version="4.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.38.0" /> <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.38.0" />
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" /> <PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.1.0" /> <PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.25" /> <PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.25" />
<PackageReference Include="Quartz" Version="3.4.0" /> <PackageReference Include="Quartz" Version="3.8.1" />
<PackageReference Include="SendGrid" Version="9.29.2" /> <PackageReference Include="SendGrid" Version="9.29.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" /> <PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
<PackageReference Include="Sentry.Serilog" Version="3.41.3" /> <PackageReference Include="Sentry.Serilog" Version="3.41.4" />
<PackageReference Include="Duende.IdentityServer" Version="6.3.7" /> <PackageReference Include="Duende.IdentityServer" Version="6.3.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.1" /> <PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.1" />
@ -57,7 +57,7 @@
<PackageReference Include="Otp.NET" Version="1.3.0" /> <PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" /> <PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.0.0" /> <PackageReference Include="LaunchDarkly.ServerSdk" Version="8.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -20,6 +20,7 @@ public class Transaction : ITableObject<Guid>
[MaxLength(50)] [MaxLength(50)]
public string GatewayId { get; set; } public string GatewayId { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow; public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public Guid? ProviderId { get; set; }
public void SetNewId() public void SetNewId()
{ {

View File

@ -0,0 +1,75 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
Welcome to Bitwarden! You can now get started with secure credential management and extend the benefits of end-to-end encryption across your entire organization.
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 10px; -webkit-text-size-adjust: none; text-align: center; border: 2px solid #175DDC; border-radius: 2px;" valign="top">
Your Master Password is <b>the only way</b> to unlock your account and only <b>you</b> hold the key. Memorize it, or write it down and keep it in a safe place.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="h3" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
Install Bitwarden
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
Access your Bitwarden account from anywhere and any device at <a href="{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">{{{WebVaultUrlHostname}}}</a>! For added convenience, <a href="https://bitwarden.com/download/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">download and install Bitwarden</a> on any desktop, device, and browser.
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
<a href="https://bitwarden.com/download/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: auto; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: none;">
<img src="https://bitwarden.com/images/mail-download-options.png" alt="Windows, Mac, Linux, Android, Apple, Chrome, Safari, Firefox, Edge, Opera, Brave, Vivaldi, Tor" width="598" height="65" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: auto; border: none; max-width: 100%; height: auto; display: block;" />
</a>
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="h3" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
Securely Share using Bitwarden Organizations
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
Bitwarden makes it easy for teams and enterprises to securely share passwords, developer secrets, and passkeys via Organizations. Join an Organization if invited, or launch a new one anytime from the <a href="{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Web App</a> with the <b>+ New Organization</b> button.
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top" align="center">
<a href="{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email" clicktracking=off target="_blank" rel="noopener" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Login to Bitwarden
</a>
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="h3" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
Bitwarden is Here for You
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
Check out the <a href="http://www.bitwarden.com/help" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Help</a> site for documentation, join the <a href="https://community.bitwarden.com/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Bitwarden Community</a> forums and connect with other enthusiasts on <a href="https://www.reddit.com/r/Bitwarden/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Reddit</a>. If you have any questions or issues, <a href="http://www.bitwarden.com/contact" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">contact support.</a>
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold;" valign="top">
Stay safe and secure,<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
The Bitwarden Team
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,39 @@
{{#>FullTextLayout}}
Welcome to Bitwarden! You can now get started with secure credential management and extend the benefits of end-to-end encryption across your entire organization.
Your Master Password is the only way to unlock your account and only you hold the key. Memorize it, or write it down and keep it in a safe place.
Install Bitwarden
============
Access your Bitwarden account from anywhere and any device at the web vault ({{WebVaultUrl}}/?utm_source=welcome_email&utm_medium=email). For added convenience, download and install Bitwarden on any desktop, device and browser (http://www.bitwarden.com/download).
Download Options
============
http://www.bitwarden.com/download
Securely Share using Bitwarden Organizations
============
Bitwarden makes it easy for teams and enterprises to securely share passwords, developer secrets, and passkeys via Organizations. Join an Organization if invited, or launch a new one anytime from the Web App ({{WebVaultUrl}}/?utm_source=welcome_email&utm_medium=email) with the + New Organization button.
Login to Bitwarden
============
{{WebVaultUrl}}/?utm_source=welcome_email&utm_medium=email
Bitwarden is Here for You
============
Check out our Help (http://www.bitwarden.com/help) site for documentation, join the Bitwarden Community forums (https://community.bitwarden.com/) and connect with other enthusiasts on Reddit (https://www.reddit.com/r/Bitwarden/). If you have any questions or issues, contact support (http://www.bitwarden.com/contact).
Stay safe and secure,
The Bitwarden Team
{{/FullTextLayout}}

View File

@ -42,6 +42,7 @@ public class SubscriptionInfo
{ {
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i)); Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
} }
CollectionMethod = sub.CollectionMethod;
} }
public DateTime? TrialStartDate { get; set; } public DateTime? TrialStartDate { get; set; }
@ -54,6 +55,7 @@ public class SubscriptionInfo
public string Status { get; set; } public string Status { get; set; }
public bool Cancelled { get; set; } public bool Cancelled { get; set; }
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>(); public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
public string CollectionMethod { get; set; }
public class BillingSubscriptionItem public class BillingSubscriptionItem
{ {

View File

@ -15,7 +15,7 @@ public class OrganizationInvitesInfo
bool initOrganization = false bool initOrganization = false
) )
{ {
OrganizationName = org.Name; OrganizationName = org.DisplayName();
OrgSsoIdentifier = org.Identifier; OrgSsoIdentifier = org.Identifier;
IsFreeOrg = org.PlanType == PlanType.Free; IsFreeOrg = org.PlanType == PlanType.Free;

View File

@ -65,6 +65,6 @@ public class SendSponsorshipOfferCommand : ISendSponsorshipOfferCommand
throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization."); throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization.");
} }
await SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); await SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.DisplayName());
} }
} }

View File

@ -7,5 +7,6 @@ public interface ITransactionRepository : IRepository<Transaction, Guid>
{ {
Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId); Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId);
Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId); Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId);
Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId); Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);
} }

View File

@ -77,5 +77,6 @@ public interface IMailService
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails); Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier); Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
Task SendTrialInitiationEmailAsync(string email);
} }

View File

@ -145,7 +145,7 @@ public class HandlebarsMailService : IMailService
public async Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails) public async Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails)
{ {
var message = CreateDefaultMessage($"{organization.Name} Seat Count Has Increased", ownerEmails); var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Count Has Increased", ownerEmails);
var model = new OrganizationSeatsAutoscaledViewModel var model = new OrganizationSeatsAutoscaledViewModel
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,
@ -160,7 +160,7 @@ public class HandlebarsMailService : IMailService
public async Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails) public async Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails)
{ {
var message = CreateDefaultMessage($"{organization.Name} Seat Limit Reached", ownerEmails); var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel var model = new OrganizationSeatsMaxReachedViewModel
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,
@ -179,7 +179,7 @@ public class HandlebarsMailService : IMailService
var model = new OrganizationUserAcceptedViewModel var model = new OrganizationUserAcceptedViewModel
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,
OrganizationName = CoreHelpers.SanitizeForEmail(organization.Name, false), OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false),
UserIdentifier = userIdentifier, UserIdentifier = userIdentifier,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName SiteName = _globalSettings.SiteName
@ -251,6 +251,19 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message); await _mailDeliveryService.SendEmailAsync(message);
} }
public async Task SendTrialInitiationEmailAsync(string userEmail)
{
var message = CreateDefaultMessage("Welcome to Bitwarden!", userEmail);
var model = new BaseMailModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "TrialInitiation", model);
message.Category = "Welcome";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email) public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
{ {
var message = CreateDefaultMessage("[Admin] Continue Logging In", email); var message = CreateDefaultMessage("[Admin] Continue Logging In", email);
@ -920,7 +933,7 @@ public class HandlebarsMailService : IMailService
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
IEnumerable<string> ownerEmails) IEnumerable<string> ownerEmails)
{ {
var message = CreateDefaultMessage($"{organization.Name} Secrets Manager Seat Limit Reached", ownerEmails); var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Seat Limit Reached", ownerEmails);
var model = new OrganizationSeatsMaxReachedViewModel var model = new OrganizationSeatsMaxReachedViewModel
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,
@ -935,7 +948,7 @@ public class HandlebarsMailService : IMailService
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
IEnumerable<string> ownerEmails) IEnumerable<string> ownerEmails)
{ {
var message = CreateDefaultMessage($"{organization.Name} Secrets Manager Service Accounts Limit Reached", ownerEmails); var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Service Accounts Limit Reached", ownerEmails);
var model = new OrganizationServiceAccountsMaxReachedViewModel var model = new OrganizationServiceAccountsMaxReachedViewModel
{ {
OrganizationId = organization.Id, OrganizationId = organization.Id,

View File

@ -132,13 +132,13 @@ public class LicensingService : ILicensingService
{ {
_logger.LogInformation(Constants.BypassFiltersEventId, null, _logger.LogInformation(Constants.BypassFiltersEventId, null,
"Organization {0} ({1}) has an invalid license and is being disabled. Reason: {2}", "Organization {0} ({1}) has an invalid license and is being disabled. Reason: {2}",
org.Id, org.Name, reason); org.Id, org.DisplayName(), reason);
org.Enabled = false; org.Enabled = false;
org.ExpirationDate = license?.Expires ?? DateTime.UtcNow; org.ExpirationDate = license?.Expires ?? DateTime.UtcNow;
org.RevisionDate = DateTime.UtcNow; org.RevisionDate = DateTime.UtcNow;
await _organizationRepository.ReplaceAsync(org); await _organizationRepository.ReplaceAsync(org);
await _mailService.SendLicenseExpiredAsync(new List<string> { org.BillingEmail }, org.Name); await _mailService.SendLicenseExpiredAsync(new List<string> { org.BillingEmail }, org.DisplayName());
} }
public async Task ValidateUsersAsync() public async Task ValidateUsersAsync()

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -125,59 +126,61 @@ public class StripePaymentService : IPaymentService
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
, additionalSmSeats, additionalServiceAccount); , additionalSmSeats, additionalServiceAccount);
Stripe.Customer customer = null; Customer customer = null;
Stripe.Subscription subscription; Subscription subscription;
try try
{ {
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions var customerCreateOptions = new CustomerCreateOptions
{ {
Description = org.BusinessName, Description = org.DisplayBusinessName(),
Email = org.BillingEmail, Email = org.BillingEmail,
Source = stipeCustomerSourceToken, Source = stipeCustomerSourceToken,
PaymentMethod = stipeCustomerPaymentMethodId, PaymentMethod = stipeCustomerPaymentMethodId,
Metadata = stripeCustomerMetadata, Metadata = stripeCustomerMetadata,
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions InvoiceSettings = new CustomerInvoiceSettingsOptions
{ {
DefaultPaymentMethod = stipeCustomerPaymentMethodId, DefaultPaymentMethod = stipeCustomerPaymentMethodId,
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> CustomFields =
{ [
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() new CustomerInvoiceSettingsCustomFieldOptions
{ {
Name = org.SubscriberType(), Name = org.SubscriberType(),
Value = GetFirstThirtyCharacters(org.SubscriberName()), Value = GetFirstThirtyCharacters(org.SubscriberName()),
}, }
}, ],
}, },
Coupon = signupIsFromSecretsManagerTrial Coupon = signupIsFromSecretsManagerTrial
? SecretsManagerStandaloneDiscountId ? SecretsManagerStandaloneDiscountId
: provider : provider
? ProviderDiscountId ? ProviderDiscountId
: null, : null,
Address = new Stripe.AddressOptions Address = new AddressOptions
{ {
Country = taxInfo.BillingAddressCountry, Country = taxInfo?.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode, PostalCode = taxInfo?.BillingAddressPostalCode,
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead. // Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, Line1 = taxInfo?.BillingAddressLine1 ?? string.Empty,
Line2 = taxInfo.BillingAddressLine2, Line2 = taxInfo?.BillingAddressLine2,
City = taxInfo.BillingAddressCity, City = taxInfo?.BillingAddressCity,
State = taxInfo.BillingAddressState, State = taxInfo?.BillingAddressState,
}, },
TaxIdData = !taxInfo.HasTaxId ? null : new List<Stripe.CustomerTaxIdDataOptions> TaxIdData = taxInfo?.HasTaxId != true
{ ? null
new Stripe.CustomerTaxIdDataOptions :
{ [
Type = taxInfo.TaxIdType, new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }
Value = taxInfo.TaxIdNumber, ],
}, };
},
}); customerCreateOptions.AddExpand("tax");
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.AddExpand("latest_invoice.payment_intent");
subCreateOptions.Customer = customer.Id; subCreateOptions.Customer = customer.Id;
if (pm5766AutomaticTaxIsEnabled) if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
{ {
subCreateOptions.AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true }; subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
@ -185,7 +188,7 @@ public class StripePaymentService : IPaymentService
{ {
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method") if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
{ {
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions()); await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
throw new GatewayException("Payment method was declined."); throw new GatewayException("Payment method was declined.");
} }
} }
@ -252,9 +255,10 @@ public class StripePaymentService : IPaymentService
throw new BadRequestException("Organization already has a subscription."); throw new BadRequestException("Organization already has a subscription.");
} }
var customerOptions = new Stripe.CustomerGetOptions(); var customerOptions = new CustomerGetOptions();
customerOptions.AddExpand("default_source"); customerOptions.AddExpand("default_source");
customerOptions.AddExpand("invoice_settings.default_payment_method"); customerOptions.AddExpand("invoice_settings.default_payment_method");
customerOptions.AddExpand("tax");
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions); var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
if (customer == null) if (customer == null)
{ {
@ -301,14 +305,15 @@ public class StripePaymentService : IPaymentService
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions }; var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
customerUpdateOptions.AddExpand("default_source"); customerUpdateOptions.AddExpand("default_source");
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method"); customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
customerUpdateOptions.AddExpand("tax");
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions); customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
} }
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade); var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
if (pm5766AutomaticTaxIsEnabled) if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
{ {
subCreateOptions.DefaultTaxRates = new List<string>(); subCreateOptions.DefaultTaxRates = [];
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
@ -333,7 +338,7 @@ public class StripePaymentService : IPaymentService
} }
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod( private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions) Customer customer, SubscriptionCreateOptions subCreateOptions)
{ {
var stripePaymentMethod = false; var stripePaymentMethod = false;
var paymentMethodType = PaymentMethodType.Credit; var paymentMethodType = PaymentMethodType.Credit;
@ -351,12 +356,12 @@ public class StripePaymentService : IPaymentService
} }
else if (customer.DefaultSource != null) else if (customer.DefaultSource != null)
{ {
if (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.SourceCard) if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
{ {
paymentMethodType = PaymentMethodType.Card; paymentMethodType = PaymentMethodType.Card;
stripePaymentMethod = true; stripePaymentMethod = true;
} }
else if (customer.DefaultSource is Stripe.BankAccount || customer.DefaultSource is Stripe.SourceAchDebit) else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
{ {
paymentMethodType = PaymentMethodType.BankAccount; paymentMethodType = PaymentMethodType.BankAccount;
stripePaymentMethod = true; stripePaymentMethod = true;
@ -394,7 +399,7 @@ public class StripePaymentService : IPaymentService
} }
var createdStripeCustomer = false; var createdStripeCustomer = false;
Stripe.Customer customer = null; Customer customer = null;
Braintree.Customer braintreeCustomer = null; Braintree.Customer braintreeCustomer = null;
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
or PaymentMethodType.Credit; or PaymentMethodType.Credit;
@ -422,14 +427,23 @@ public class StripePaymentService : IPaymentService
try try
{ {
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId); var customerGetOptions = new CustomerGetOptions();
customerGetOptions.AddExpand("tax");
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId, customerGetOptions);
}
catch
{
_logger.LogWarning(
"Attempted to get existing customer from Stripe, but customer ID was not found. Attempting to recreate customer...");
} }
catch { }
} }
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken)) if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
{ {
var stripeCustomerMetadata = new Dictionary<string, string> { { "region", _globalSettings.BaseServiceUri.CloudRegion } }; var stripeCustomerMetadata = new Dictionary<string, string>
{
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
};
if (paymentMethodType == PaymentMethodType.PayPal) if (paymentMethodType == PaymentMethodType.PayPal)
{ {
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false); var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
@ -458,32 +472,35 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("Payment method is not supported at this time."); throw new GatewayException("Payment method is not supported at this time.");
} }
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions var customerCreateOptions = new CustomerCreateOptions
{ {
Description = user.Name, Description = user.Name,
Email = user.Email, Email = user.Email,
Metadata = stripeCustomerMetadata, Metadata = stripeCustomerMetadata,
PaymentMethod = stipeCustomerPaymentMethodId, PaymentMethod = stipeCustomerPaymentMethodId,
Source = stipeCustomerSourceToken, Source = stipeCustomerSourceToken,
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions InvoiceSettings = new CustomerInvoiceSettingsOptions
{ {
DefaultPaymentMethod = stipeCustomerPaymentMethodId, DefaultPaymentMethod = stipeCustomerPaymentMethodId,
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> CustomFields =
{ [
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() new CustomerInvoiceSettingsCustomFieldOptions()
{ {
Name = user.SubscriberType(), Name = user.SubscriberType(),
Value = GetFirstThirtyCharacters(user.SubscriberName()), Value = GetFirstThirtyCharacters(user.SubscriberName()),
}, }
}
]
}, },
Address = new Stripe.AddressOptions Address = new AddressOptions
{ {
Line1 = string.Empty, Line1 = string.Empty,
Country = taxInfo.BillingAddressCountry, Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode, PostalCode = taxInfo.BillingAddressPostalCode,
}, },
}); };
customerCreateOptions.AddExpand("tax");
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
createdStripeCustomer = true; createdStripeCustomer = true;
} }
@ -492,17 +509,17 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("Could not set up customer payment profile."); throw new GatewayException("Could not set up customer payment profile.");
} }
var subCreateOptions = new Stripe.SubscriptionCreateOptions var subCreateOptions = new SubscriptionCreateOptions
{ {
Customer = customer.Id, Customer = customer.Id,
Items = new List<Stripe.SubscriptionItemOptions>(), Items = [],
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
{ {
[user.GatewayIdField()] = user.Id.ToString() [user.GatewayIdField()] = user.Id.ToString()
} }
}; };
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions subCreateOptions.Items.Add(new SubscriptionItemOptions
{ {
Plan = PremiumPlanId, Plan = PremiumPlanId,
Quantity = 1 Quantity = 1
@ -524,25 +541,22 @@ public class StripePaymentService : IPaymentService
var taxRate = taxRates.FirstOrDefault(); var taxRate = taxRates.FirstOrDefault();
if (taxRate != null) if (taxRate != null)
{ {
subCreateOptions.DefaultTaxRates = new List<string>(1) subCreateOptions.DefaultTaxRates = [taxRate.Id];
{
taxRate.Id
};
} }
} }
if (additionalStorageGb > 0) if (additionalStorageGb > 0)
{ {
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions subCreateOptions.Items.Add(new SubscriptionItemOptions
{ {
Plan = StoragePlanId, Plan = StoragePlanId,
Quantity = additionalStorageGb Quantity = additionalStorageGb
}); });
} }
if (pm5766AutomaticTaxIsEnabled) if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
{ {
subCreateOptions.DefaultTaxRates = new List<string>(); subCreateOptions.DefaultTaxRates = [];
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
@ -558,34 +572,33 @@ public class StripePaymentService : IPaymentService
{ {
return subscription.LatestInvoice.PaymentIntent.ClientSecret; return subscription.LatestInvoice.PaymentIntent.ClientSecret;
} }
else
{ user.Premium = true;
user.Premium = true; user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
user.PremiumExpirationDate = subscription.CurrentPeriodEnd; return null;
return null;
}
} }
private async Task<Stripe.Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Stripe.Customer customer, private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer,
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType, bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
Stripe.SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer) SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
{ {
var addedCreditToStripeCustomer = false; var addedCreditToStripeCustomer = false;
Braintree.Transaction braintreeTransaction = null; Braintree.Transaction braintreeTransaction = null;
var subInvoiceMetadata = new Dictionary<string, string>(); var subInvoiceMetadata = new Dictionary<string, string>();
Stripe.Subscription subscription = null; Subscription subscription = null;
try try
{ {
if (!stripePaymentMethod) if (!stripePaymentMethod)
{ {
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
{ {
Customer = customer.Id, Customer = customer.Id,
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items) SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
}); });
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax)) if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
CustomerHasTaxLocationVerified(customer))
{ {
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true }; previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
} }
@ -632,7 +645,7 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("No payment was able to be collected."); throw new GatewayException("No payment was able to be collected.");
} }
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{ {
Balance = customer.Balance - previewInvoice.AmountDue Balance = customer.Balance - previewInvoice.AmountDue
}); });
@ -649,10 +662,10 @@ public class StripePaymentService : IPaymentService
}; };
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax); var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
if (pm5766AutomaticTaxIsEnabled) if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
{ {
upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = new List<string>(); upcomingInvoiceOptions.SubscriptionDefaultTaxRates = [];
} }
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
@ -666,17 +679,12 @@ public class StripePaymentService : IPaymentService
subCreateOptions.OffSession = true; subCreateOptions.OffSession = true;
subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.AddExpand("latest_invoice.payment_intent");
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
{
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
}
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
{ {
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method") if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
{ {
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions()); await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
throw new GatewayException("Payment method was declined."); throw new GatewayException("Payment method was declined.");
} }
} }
@ -694,7 +702,7 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("Invoice not found."); throw new GatewayException("Invoice not found.");
} }
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
{ {
Metadata = subInvoiceMetadata Metadata = subInvoiceMetadata
}); });
@ -712,7 +720,7 @@ public class StripePaymentService : IPaymentService
} }
else if (addedCreditToStripeCustomer || customer.Balance < 0) else if (addedCreditToStripeCustomer || customer.Balance < 0)
{ {
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{ {
Balance = customer.Balance Balance = customer.Balance
}); });
@ -727,7 +735,7 @@ public class StripePaymentService : IPaymentService
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
} }
if (e is Stripe.StripeException strEx && if (e is StripeException strEx &&
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false)) (strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
{ {
throw new GatewayException("Bank account is not yet verified."); throw new GatewayException("Bank account is not yet verified.");
@ -737,10 +745,10 @@ public class StripePaymentService : IPaymentService
} }
} }
private List<Stripe.InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions( private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
List<Stripe.SubscriptionItemOptions> subItemOptions) List<SubscriptionItemOptions> subItemOptions)
{ {
return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
{ {
Plan = si.Plan, Plan = si.Plan,
Price = si.Price, Price = si.Price,
@ -753,7 +761,10 @@ public class StripePaymentService : IPaymentService
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false) SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
{ {
// remember, when in doubt, throw // remember, when in doubt, throw
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId); var subGetOptions = new SubscriptionGetOptions();
// subGetOptions.AddExpand("customer");
subGetOptions.AddExpand("customer.tax");
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId, subGetOptions);
if (sub == null) if (sub == null)
{ {
throw new GatewayException("Subscription not found."); throw new GatewayException("Subscription not found.");
@ -766,7 +777,7 @@ public class StripePaymentService : IPaymentService
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub); var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold); var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions var subUpdateOptions = new SubscriptionUpdateOptions
{ {
Items = updatedItemOptions, Items = updatedItemOptions,
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
@ -777,7 +788,7 @@ public class StripePaymentService : IPaymentService
ProrationDate = prorationDate, ProrationDate = prorationDate,
}; };
var immediatelyInvoice = false; var immediatelyInvoice = false;
if (!invoiceNow && isPm5864DollarThresholdEnabled) if (!invoiceNow && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
{ {
var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
{ {
@ -789,7 +800,8 @@ public class StripePaymentService : IPaymentService
SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now
}); });
immediatelyInvoice = upcomingInvoiceWithChanges.AmountRemaining >= 50000; var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
immediatelyInvoice = isAnnualPlan && upcomingInvoiceWithChanges.AmountRemaining >= 50000;
subUpdateOptions.BillingCycleAnchor = immediatelyInvoice subUpdateOptions.BillingCycleAnchor = immediatelyInvoice
? SubscriptionBillingCycleAnchor.Now ? SubscriptionBillingCycleAnchor.Now
@ -797,9 +809,11 @@ public class StripePaymentService : IPaymentService
} }
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax); var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
if (pm5766AutomaticTaxIsEnabled) if (pm5766AutomaticTaxIsEnabled &&
sub.AutomaticTax.Enabled != true &&
CustomerHasTaxLocationVerified(sub.Customer))
{ {
subUpdateOptions.DefaultTaxRates = new List<string>(); subUpdateOptions.DefaultTaxRates = [];
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
} }
@ -824,7 +838,7 @@ public class StripePaymentService : IPaymentService
var taxRate = taxRates.FirstOrDefault(); var taxRate = taxRates.FirstOrDefault();
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id))) if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
{ {
subUpdateOptions.DefaultTaxRates = new List<string>(1) { taxRate.Id }; subUpdateOptions.DefaultTaxRates = [taxRate.Id];
} }
} }
} }
@ -834,7 +848,7 @@ public class StripePaymentService : IPaymentService
{ {
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions); var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions()); var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new InvoiceGetOptions());
if (invoice == null) if (invoice == null)
{ {
throw new BadRequestException("Unable to locate draft invoice for subscription update."); throw new BadRequestException("Unable to locate draft invoice for subscription update.");
@ -852,11 +866,11 @@ public class StripePaymentService : IPaymentService
} }
else else
{ {
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new InvoiceFinalizeOptions
{ {
AutoAdvance = false, AutoAdvance = false,
}); });
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions()); await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new InvoiceSendOptions());
paymentIntentClientSecret = null; paymentIntentClientSecret = null;
} }
} }
@ -864,7 +878,7 @@ public class StripePaymentService : IPaymentService
catch catch
{ {
// Need to revert the subscription // Need to revert the subscription
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
{ {
Items = subscriptionUpdate.RevertItemsOptions(sub), Items = subscriptionUpdate.RevertItemsOptions(sub),
// This proration behavior prevents a false "credit" from // This proration behavior prevents a false "credit" from
@ -889,7 +903,7 @@ public class StripePaymentService : IPaymentService
// Change back the subscription collection method and/or days until due // Change back the subscription collection method and/or days until due
if (collectionMethod != "send_invoice" || daysUntilDue == null) if (collectionMethod != "send_invoice" || daysUntilDue == null)
{ {
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
{ {
CollectionMethod = collectionMethod, CollectionMethod = collectionMethod,
DaysUntilDue = daysUntilDue, DaysUntilDue = daysUntilDue,
@ -950,7 +964,7 @@ public class StripePaymentService : IPaymentService
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
{ {
await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId, await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId,
new Stripe.SubscriptionCancelOptions()); new SubscriptionCancelOptions());
} }
if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
@ -983,7 +997,7 @@ public class StripePaymentService : IPaymentService
} }
else else
{ {
var charges = await _stripeAdapter.ChargeListAsync(new Stripe.ChargeListOptions var charges = await _stripeAdapter.ChargeListAsync(new ChargeListOptions
{ {
Customer = subscriber.GatewayCustomerId Customer = subscriber.GatewayCustomerId
}); });
@ -992,7 +1006,7 @@ public class StripePaymentService : IPaymentService
{ {
foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded)) foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded))
{ {
await _stripeAdapter.RefundCreateAsync(new Stripe.RefundCreateOptions { Charge = charge.Id }); await _stripeAdapter.RefundCreateAsync(new RefundCreateOptions { Charge = charge.Id });
} }
} }
} }
@ -1000,9 +1014,9 @@ public class StripePaymentService : IPaymentService
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId); await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
} }
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice) public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Invoice invoice)
{ {
var customerOptions = new Stripe.CustomerGetOptions(); var customerOptions = new CustomerGetOptions();
customerOptions.AddExpand("default_source"); customerOptions.AddExpand("default_source");
customerOptions.AddExpand("invoice_settings.default_payment_method"); customerOptions.AddExpand("invoice_settings.default_payment_method");
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions); var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
@ -1016,7 +1030,7 @@ public class StripePaymentService : IPaymentService
{ {
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card"; var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
var hasDefaultValidSource = customer.DefaultSource != null && var hasDefaultValidSource = customer.DefaultSource != null &&
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount); (customer.DefaultSource is Card || customer.DefaultSource is BankAccount);
if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource) if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource)
{ {
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id; cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
@ -1029,7 +1043,7 @@ public class StripePaymentService : IPaymentService
} }
catch catch
{ {
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
{ {
AutoAdvance = false AutoAdvance = false
}); });
@ -1045,11 +1059,11 @@ public class StripePaymentService : IPaymentService
{ {
// Finalize the invoice (from Draft) w/o auto-advance so we // Finalize the invoice (from Draft) w/o auto-advance so we
// can attempt payment manually. // can attempt payment manually.
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
{ {
AutoAdvance = false, AutoAdvance = false,
}); });
var invoicePayOptions = new Stripe.InvoicePayOptions var invoicePayOptions = new InvoicePayOptions
{ {
PaymentMethod = cardPaymentMethodId, PaymentMethod = cardPaymentMethodId,
}; };
@ -1083,7 +1097,7 @@ public class StripePaymentService : IPaymentService
} }
braintreeTransaction = transactionResult.Target; braintreeTransaction = transactionResult.Target;
invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
{ {
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
{ {
@ -1099,13 +1113,13 @@ public class StripePaymentService : IPaymentService
{ {
invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions); invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
} }
catch (Stripe.StripeException e) catch (StripeException e)
{ {
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired && if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
e.StripeError?.Code == "invoice_payment_intent_requires_action") e.StripeError?.Code == "invoice_payment_intent_requires_action")
{ {
// SCA required, get intent client secret // SCA required, get intent client secret
var invoiceGetOptions = new Stripe.InvoiceGetOptions(); var invoiceGetOptions = new InvoiceGetOptions();
invoiceGetOptions.AddExpand("payment_intent"); invoiceGetOptions.AddExpand("payment_intent");
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions); invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret; paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
@ -1130,7 +1144,7 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret; return paymentIntentClientSecret;
} }
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions()); invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new InvoiceVoidOptions());
// HACK: Workaround for customer balance credit // HACK: Workaround for customer balance credit
if (invoice.StartingBalance < 0) if (invoice.StartingBalance < 0)
@ -1143,7 +1157,7 @@ public class StripePaymentService : IPaymentService
// Assumption: Customer balance should now be $0, otherwise payment would not have failed. // Assumption: Customer balance should now be $0, otherwise payment would not have failed.
if (customer.Balance == 0) if (customer.Balance == 0)
{ {
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{ {
Balance = invoice.StartingBalance Balance = invoice.StartingBalance
}); });
@ -1151,7 +1165,7 @@ public class StripePaymentService : IPaymentService
} }
} }
if (e is Stripe.StripeException strEx && if (e is StripeException strEx &&
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false)) (strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
{ {
throw new GatewayException("Bank account is not yet verified."); throw new GatewayException("Bank account is not yet verified.");
@ -1192,14 +1206,14 @@ public class StripePaymentService : IPaymentService
{ {
var canceledSub = endOfPeriod ? var canceledSub = endOfPeriod ?
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) : new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new Stripe.SubscriptionCancelOptions()); await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new SubscriptionCancelOptions());
if (!canceledSub.CanceledAt.HasValue) if (!canceledSub.CanceledAt.HasValue)
{ {
throw new GatewayException("Unable to cancel subscription."); throw new GatewayException("Unable to cancel subscription.");
} }
} }
catch (Stripe.StripeException e) catch (StripeException e)
{ {
if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}") if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}")
{ {
@ -1233,7 +1247,7 @@ public class StripePaymentService : IPaymentService
} }
var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = false }); new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
if (updatedSub.CanceledAt.HasValue) if (updatedSub.CanceledAt.HasValue)
{ {
throw new GatewayException("Unable to reinstate subscription."); throw new GatewayException("Unable to reinstate subscription.");
@ -1264,12 +1278,11 @@ public class StripePaymentService : IPaymentService
}; };
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount; var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
Stripe.Customer customer = null; Customer customer = null;
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
var options = new Stripe.CustomerGetOptions(); var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] };
options.AddExpand("sources");
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options); customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options);
if (customer.Metadata?.Any() ?? false) if (customer.Metadata?.Any() ?? false)
{ {
@ -1369,26 +1382,27 @@ public class StripePaymentService : IPaymentService
{ {
if (customer == null) if (customer == null)
{ {
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
{ {
Description = subscriber.BillingName(), Description = subscriber.BillingName(),
Email = subscriber.BillingEmailAddress(), Email = subscriber.BillingEmailAddress(),
Metadata = stripeCustomerMetadata, Metadata = stripeCustomerMetadata,
Source = stipeCustomerSourceToken, Source = stipeCustomerSourceToken,
PaymentMethod = stipeCustomerPaymentMethodId, PaymentMethod = stipeCustomerPaymentMethodId,
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions InvoiceSettings = new CustomerInvoiceSettingsOptions
{ {
DefaultPaymentMethod = stipeCustomerPaymentMethodId, DefaultPaymentMethod = stipeCustomerPaymentMethodId,
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> CustomFields =
{ [
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() new CustomerInvoiceSettingsCustomFieldOptions()
{ {
Name = subscriber.SubscriberType(), Name = subscriber.SubscriberType(),
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()), Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
}, }
}
]
}, },
Address = taxInfo == null ? null : new Stripe.AddressOptions Address = taxInfo == null ? null : new AddressOptions
{ {
Country = taxInfo.BillingAddressCountry, Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode, PostalCode = taxInfo.BillingAddressPostalCode,
@ -1397,7 +1411,7 @@ public class StripePaymentService : IPaymentService
City = taxInfo.BillingAddressCity, City = taxInfo.BillingAddressCity,
State = taxInfo.BillingAddressState, State = taxInfo.BillingAddressState,
}, },
Expand = new List<string> { "sources" }, Expand = ["sources", "tax", "subscriptions"],
}); });
subscriber.Gateway = GatewayType.Stripe; subscriber.Gateway = GatewayType.Stripe;
@ -1413,7 +1427,7 @@ public class StripePaymentService : IPaymentService
{ {
if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_")) if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_"))
{ {
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new Stripe.BankAccountCreateOptions var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new BankAccountCreateOptions
{ {
Source = paymentToken Source = paymentToken
}); });
@ -1422,7 +1436,7 @@ public class StripePaymentService : IPaymentService
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId)) else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
{ {
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId, await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
new Stripe.PaymentMethodAttachOptions { Customer = customer.Id }); new PaymentMethodAttachOptions { Customer = customer.Id });
defaultPaymentMethodId = stipeCustomerPaymentMethodId; defaultPaymentMethodId = stipeCustomerPaymentMethodId;
} }
} }
@ -1431,44 +1445,44 @@ public class StripePaymentService : IPaymentService
{ {
foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId)) foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId))
{ {
if (source is Stripe.BankAccount) if (source is BankAccount)
{ {
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id); await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
} }
else if (source is Stripe.Card) else if (source is Card)
{ {
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id); await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
} }
} }
} }
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new Stripe.PaymentMethodListOptions var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions
{ {
Customer = customer.Id, Customer = customer.Id,
Type = "card" Type = "card"
}); });
foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId)) foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId))
{ {
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new Stripe.PaymentMethodDetachOptions()); await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new PaymentMethodDetachOptions());
} }
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{ {
Metadata = stripeCustomerMetadata, Metadata = stripeCustomerMetadata,
DefaultSource = defaultSourceId, DefaultSource = defaultSourceId,
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions InvoiceSettings = new CustomerInvoiceSettingsOptions
{ {
DefaultPaymentMethod = defaultPaymentMethodId, DefaultPaymentMethod = defaultPaymentMethodId,
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions> CustomFields =
{ [
new Stripe.CustomerInvoiceSettingsCustomFieldOptions() new CustomerInvoiceSettingsCustomFieldOptions()
{ {
Name = subscriber.SubscriberType(), Name = subscriber.SubscriberType(),
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()) Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
}, }
} ]
}, },
Address = taxInfo == null ? null : new Stripe.AddressOptions Address = taxInfo == null ? null : new AddressOptions
{ {
Country = taxInfo.BillingAddressCountry, Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode, PostalCode = taxInfo.BillingAddressPostalCode,
@ -1477,8 +1491,27 @@ public class StripePaymentService : IPaymentService
City = taxInfo.BillingAddressCity, City = taxInfo.BillingAddressCity,
State = taxInfo.BillingAddressState, State = taxInfo.BillingAddressState,
}, },
Expand = ["tax", "subscriptions"]
}); });
} }
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
customer.Subscriptions.Any(sub =>
sub.Id == subscriber.GatewaySubscriptionId &&
!sub.AutomaticTax.Enabled) &&
CustomerHasTaxLocationVerified(customer))
{
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
DefaultTaxRates = []
};
_ = await _stripeAdapter.SubscriptionUpdateAsync(
subscriber.GatewaySubscriptionId,
subscriptionUpdateOptions);
}
} }
catch catch
{ {
@ -1494,7 +1527,7 @@ public class StripePaymentService : IPaymentService
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount) public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
{ {
Stripe.Customer customer = null; Customer customer = null;
var customerExists = subscriber.Gateway == GatewayType.Stripe && var customerExists = subscriber.Gateway == GatewayType.Stripe &&
!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId); !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);
if (customerExists) if (customerExists)
@ -1503,7 +1536,7 @@ public class StripePaymentService : IPaymentService
} }
else else
{ {
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
{ {
Email = subscriber.BillingEmailAddress(), Email = subscriber.BillingEmailAddress(),
Description = subscriber.BillingName(), Description = subscriber.BillingName(),
@ -1511,7 +1544,7 @@ public class StripePaymentService : IPaymentService
subscriber.Gateway = GatewayType.Stripe; subscriber.Gateway = GatewayType.Stripe;
subscriber.GatewayCustomerId = customer.Id; subscriber.GatewayCustomerId = customer.Id;
} }
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{ {
Balance = customer.Balance - (long)(creditAmount * 100) Balance = customer.Balance - (long)(creditAmount * 100)
}); });
@ -1614,7 +1647,7 @@ public class StripePaymentService : IPaymentService
} }
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId,
new Stripe.CustomerGetOptions { Expand = new List<string> { "tax_ids" } }); new CustomerGetOptions { Expand = ["tax_ids"] });
if (customer == null) if (customer == null)
{ {
@ -1647,9 +1680,9 @@ public class StripePaymentService : IPaymentService
{ {
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{ {
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new Stripe.CustomerUpdateOptions var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions
{ {
Address = new Stripe.AddressOptions Address = new AddressOptions
{ {
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
Line2 = taxInfo.BillingAddressLine2, Line2 = taxInfo.BillingAddressLine2,
@ -1658,7 +1691,7 @@ public class StripePaymentService : IPaymentService
PostalCode = taxInfo.BillingAddressPostalCode, PostalCode = taxInfo.BillingAddressPostalCode,
Country = taxInfo.BillingAddressCountry, Country = taxInfo.BillingAddressCountry,
}, },
Expand = new List<string> { "tax_ids" } Expand = ["tax_ids"]
}); });
if (!subscriber.IsUser() && customer != null) if (!subscriber.IsUser() && customer != null)
@ -1672,7 +1705,7 @@ public class StripePaymentService : IPaymentService
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) && if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType)) !string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
{ {
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new Stripe.TaxIdCreateOptions await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions
{ {
Type = taxInfo.TaxIdType, Type = taxInfo.TaxIdType,
Value = taxInfo.TaxIdNumber, Value = taxInfo.TaxIdNumber,
@ -1684,7 +1717,7 @@ public class StripePaymentService : IPaymentService
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate) public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
{ {
var stripeTaxRateOptions = new Stripe.TaxRateCreateOptions() var stripeTaxRateOptions = new TaxRateCreateOptions()
{ {
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}", DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
Inclusive = false, Inclusive = false,
@ -1717,7 +1750,7 @@ public class StripePaymentService : IPaymentService
var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync( var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
taxRate.Id, taxRate.Id,
new Stripe.TaxRateUpdateOptions() { Active = false } new TaxRateUpdateOptions() { Active = false }
); );
if (!updatedStripeTaxRate.Active) if (!updatedStripeTaxRate.Active)
{ {
@ -1738,32 +1771,36 @@ public class StripePaymentService : IPaymentService
{ {
var subscriptionInfo = await GetSubscriptionAsync(organization); var subscriptionInfo = await GetSubscriptionAsync(organization);
if (subscriptionInfo.Subscription is not { Status: "active" or "trialing" or "past_due" } || if (subscriptionInfo.Subscription is not
subscriptionInfo.UpcomingInvoice == null) {
Status: "active" or "trialing" or "past_due",
CollectionMethod: "charge_automatically"
}
|| subscriptionInfo.UpcomingInvoice == null)
{ {
return false; return false;
} }
var customer = await GetCustomerAsync(organization.GatewayCustomerId); var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions());
var paymentSource = await GetBillingPaymentSourceAsync(customer); var paymentSource = await GetBillingPaymentSourceAsync(customer);
return paymentSource == null; return paymentSource == null;
} }
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId) private PaymentMethod GetLatestCardPaymentMethod(string customerId)
{ {
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
new Stripe.PaymentMethodListOptions { Customer = customerId, Type = "card" }); new PaymentMethodListOptions { Customer = customerId, Type = "card" });
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault(); return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
} }
private decimal GetBillingBalance(Stripe.Customer customer) private decimal GetBillingBalance(Customer customer)
{ {
return customer != null ? customer.Balance / 100M : default; return customer != null ? customer.Balance / 100M : default;
} }
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Stripe.Customer customer) private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
{ {
if (customer == null) if (customer == null)
{ {
@ -1792,7 +1829,7 @@ public class StripePaymentService : IPaymentService
} }
if (customer.DefaultSource != null && if (customer.DefaultSource != null &&
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount)) (customer.DefaultSource is Card || customer.DefaultSource is BankAccount))
{ {
return new BillingInfo.BillingSource(customer.DefaultSource); return new BillingInfo.BillingSource(customer.DefaultSource);
} }
@ -1801,27 +1838,27 @@ public class StripePaymentService : IPaymentService
return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null; return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null;
} }
private Stripe.CustomerGetOptions GetCustomerPaymentOptions() private CustomerGetOptions GetCustomerPaymentOptions()
{ {
var customerOptions = new Stripe.CustomerGetOptions(); var customerOptions = new CustomerGetOptions();
customerOptions.AddExpand("default_source"); customerOptions.AddExpand("default_source");
customerOptions.AddExpand("invoice_settings.default_payment_method"); customerOptions.AddExpand("invoice_settings.default_payment_method");
return customerOptions; return customerOptions;
} }
private async Task<Stripe.Customer> GetCustomerAsync(string gatewayCustomerId, Stripe.CustomerGetOptions options = null) private async Task<Customer> GetCustomerAsync(string gatewayCustomerId, CustomerGetOptions options = null)
{ {
if (string.IsNullOrWhiteSpace(gatewayCustomerId)) if (string.IsNullOrWhiteSpace(gatewayCustomerId))
{ {
return null; return null;
} }
Stripe.Customer customer = null; Customer customer = null;
try try
{ {
customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options); customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options);
} }
catch (Stripe.StripeException) { } catch (StripeException) { }
return customer; return customer;
} }
@ -1843,7 +1880,7 @@ public class StripePaymentService : IPaymentService
} }
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Stripe.Customer customer) private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Customer customer)
{ {
if (customer == null) if (customer == null)
{ {
@ -1865,28 +1902,32 @@ public class StripePaymentService : IPaymentService
.OrderByDescending(invoice => invoice.Created) .OrderByDescending(invoice => invoice.Created)
.Select(invoice => new BillingInfo.BillingInvoice(invoice)); .Select(invoice => new BillingInfo.BillingInvoice(invoice));
} }
catch (Stripe.StripeException exception) catch (StripeException exception)
{ {
_logger.LogError(exception, "An error occurred while listing Stripe invoices"); _logger.LogError(exception, "An error occurred while listing Stripe invoices");
throw new GatewayException("Failed to retrieve current invoices", exception); throw new GatewayException("Failed to retrieve current invoices", exception);
} }
} }
/// <summary>
/// Determines if a Stripe customer supports automatic tax
/// </summary>
/// <param name="customer"></param>
/// <returns></returns>
private static bool CustomerHasTaxLocationVerified(Customer customer) =>
customer?.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported;
// We are taking only first 30 characters of the SubscriberName because stripe provide // We are taking only first 30 characters of the SubscriberName because stripe provide
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create // for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
public static string GetFirstThirtyCharacters(string subscriberName) private static string GetFirstThirtyCharacters(string subscriberName)
{ {
if (string.IsNullOrWhiteSpace(subscriberName)) if (string.IsNullOrWhiteSpace(subscriberName))
{ {
return ""; return string.Empty;
}
else if (subscriberName.Length <= 30)
{
return subscriberName;
}
else
{
return subscriberName.Substring(0, 30);
} }
return subscriberName.Length <= 30
? subscriberName
: subscriberName[..30];
} }
} }

View File

@ -338,14 +338,13 @@ public class UserService : UserManager<User>, IUserService, IDisposable
var result = await base.CreateAsync(user, masterPassword); var result = await base.CreateAsync(user, masterPassword);
if (result == IdentityResult.Success) if (result == IdentityResult.Success)
{ {
await _mailService.SendWelcomeEmailAsync(user);
if (!string.IsNullOrEmpty(user.ReferenceData)) if (!string.IsNullOrEmpty(user.ReferenceData))
{ {
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData); var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData);
if (referenceData.TryGetValue("initiationPath", out var value)) if (referenceData.TryGetValue("initiationPath", out var value))
{ {
var initiationPath = value.ToString(); var initiationPath = value.ToString();
await SendAppropriateWelcomeEmailAsync(user, initiationPath);
if (!string.IsNullOrEmpty(initiationPath)) if (!string.IsNullOrEmpty(initiationPath))
{ {
await _referenceEventService.RaiseEventAsync( await _referenceEventService.RaiseEventAsync(
@ -797,7 +796,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
user.ForcePasswordReset = true; user.ForcePasswordReset = true;
await _userRepository.ReplaceAsync(user); await _userRepository.ReplaceAsync(user);
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.Name); await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword); await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);
await _pushService.PushLogOutAsync(user.Id); await _pushService.PushLogOutAsync(user.Id);
@ -1391,7 +1390,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await organizationService.DeleteUserAsync(p.OrganizationId, user.Id); await organizationService.DeleteUserAsync(p.OrganizationId, user.Id);
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
organization.Name, user.Email); organization.DisplayName(), user.Email);
}).ToArray(); }).ToArray();
await Task.WhenAll(removeOrgUserTasks); await Task.WhenAll(removeOrgUserTasks);
@ -1453,4 +1452,18 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return isVerified; return isVerified;
} }
private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath)
{
var isFromMarketingWebsite = initiationPath.Contains("Secrets Manager trial");
if (isFromMarketingWebsite)
{
await _mailService.SendTrialInitiationEmailAsync(user.Email);
}
else
{
await _mailService.SendWelcomeEmailAsync(user);
}
}
} }

View File

@ -262,5 +262,10 @@ public class NoopMailService : IMailService
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendTrialInitiationEmailAsync(string email)
{
return Task.FromResult(0);
}
} }

View File

@ -117,6 +117,11 @@ public class Send : ITableObject<Guid>
/// </value> /// </value>
public bool? HideEmail { get; set; } public bool? HideEmail { get; set; }
/// <summary>
/// Identifies the Cipher associated with this send.
/// </summary>
public Guid? CipherId { get; set; }
/// <summary> /// <summary>
/// Generates the send's <see cref="Id" /> /// Generates the send's <see cref="Id" />
/// </summary> /// </summary>

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Net;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using NS = Newtonsoft.Json; using NS = Newtonsoft.Json;
@ -192,3 +193,33 @@ public class PermissiveStringEnumerableConverter : JsonConverter<IEnumerable<str
writer.WriteEndArray(); writer.WriteEndArray();
} }
} }
/// <summary>
/// Encodes incoming strings using HTML encoding
/// and decodes outgoing strings using HTML decoding.
/// </summary>
public class HtmlEncodingStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var originalValue = reader.GetString();
return WebUtility.HtmlEncode(originalValue);
}
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (!string.IsNullOrEmpty(value))
{
var encodedValue = WebUtility.HtmlDecode(value);
writer.WriteStringValue(encodedValue);
}
else
{
writer.WriteNullValue();
}
}
}

View File

@ -90,7 +90,12 @@ public class ClientStore : IClientStore
private async Task<Client> CreateApiKeyClientAsync(string clientId) private async Task<Client> CreateApiKeyClientAsync(string clientId)
{ {
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(new Guid(clientId)); if (!Guid.TryParse(clientId, out var guid))
{
return null;
}
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(guid);
if (apiKey == null || apiKey.ExpireAt <= DateTime.Now) if (apiKey == null || apiKey.ExpireAt <= DateTime.Now)
{ {

View File

@ -0,0 +1,28 @@
using System.Data;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
using Microsoft.Data.SqlClient;
namespace Bit.Infrastructure.Dapper.Billing.Repositories;
public class ProviderPlanRepository(
GlobalSettings globalSettings)
: Repository<ProviderPlan, Guid>(
globalSettings.SqlServer.ConnectionString,
globalSettings.SqlServer.ReadOnlyConnectionString), IProviderPlanRepository
{
public async Task<ProviderPlan> GetByProviderId(Guid providerId)
{
var sqlConnection = new SqlConnection(ConnectionString);
var results = await sqlConnection.QueryAsync<ProviderPlan>(
"[dbo].[ProviderPlan_ReadByProviderId]",
new { ProviderId = providerId },
commandType: CommandType.StoredProcedure);
return results.FirstOrDefault();
}
}

View File

@ -1,11 +1,13 @@
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Repositories;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.AdminConsole.Repositories; using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.Dapper.Auth.Repositories;
using Bit.Infrastructure.Dapper.Billing.Repositories;
using Bit.Infrastructure.Dapper.Repositories; using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.SecretsManager.Repositories; using Bit.Infrastructure.Dapper.SecretsManager.Repositories;
using Bit.Infrastructure.Dapper.Tools.Repositories; using Bit.Infrastructure.Dapper.Tools.Repositories;
@ -48,6 +50,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<IUserRepository, UserRepository>(); services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>(); services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>(); services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
if (selfHosted) if (selfHosted)
{ {

View File

@ -44,6 +44,16 @@ public class TransactionRepository : Repository<Transaction, Guid>, ITransaction
} }
} }
public async Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId)
{
await using var sqlConnection = new SqlConnection(ConnectionString);
var results = await sqlConnection.QueryAsync<Transaction>(
$"[{Schema}].[Transaction_ReadByProviderId]",
new { ProviderId = providerId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
public async Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId) public async Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)
{ {
using (var connection = new SqlConnection(ConnectionString)) using (var connection = new SqlConnection(ConnectionString))

View File

@ -0,0 +1,21 @@
using Bit.Infrastructure.EntityFramework.Billing.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Billing.Configurations;
public class ProviderPlanEntityTypeConfiguration : IEntityTypeConfiguration<ProviderPlan>
{
public void Configure(EntityTypeBuilder<ProviderPlan> builder)
{
builder
.Property(t => t.Id)
.ValueGeneratedNever();
builder
.HasIndex(providerPlan => new { providerPlan.Id, providerPlan.PlanType })
.IsUnique();
builder.ToTable(nameof(ProviderPlan));
}
}

View File

@ -0,0 +1,17 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
namespace Bit.Infrastructure.EntityFramework.Billing.Models;
public class ProviderPlan : Core.Billing.Entities.ProviderPlan
{
public virtual Provider Provider { get; set; }
}
public class ProviderPlanMapperProfile : Profile
{
public ProviderPlanMapperProfile()
{
CreateMap<Core.Billing.Entities.ProviderPlan, ProviderPlan>().ReverseMap();
}
}

View File

@ -0,0 +1,29 @@
using AutoMapper;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using EFProviderPlan = Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan;
namespace Bit.Infrastructure.EntityFramework.Billing.Repositories;
public class ProviderPlanRepository(
IMapper mapper,
IServiceScopeFactory serviceScopeFactory)
: Repository<ProviderPlan, EFProviderPlan, Guid>(
serviceScopeFactory,
mapper,
context => context.ProviderPlans), IProviderPlanRepository
{
public async Task<ProviderPlan> GetByProviderId(Guid providerId)
{
using var serviceScope = ServiceScopeFactory.CreateScope();
var databaseContext = GetDatabaseContext(serviceScope);
var query =
from providerPlan in databaseContext.ProviderPlans
where providerPlan.ProviderId == providerId
select providerPlan;
return await query.FirstOrDefaultAsync();
}
}

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories;
@ -7,6 +8,7 @@ using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Auth.Repositories;
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories; using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Tools.Repositories; using Bit.Infrastructure.EntityFramework.Tools.Repositories;
@ -85,6 +87,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton<IUserRepository, UserRepository>(); services.AddSingleton<IUserRepository, UserRepository>();
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>(); services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>(); services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
if (selfHosted) if (selfHosted)
{ {

View File

@ -1,5 +1,6 @@
using AutoMapper; using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
namespace Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.Models;
@ -7,6 +8,7 @@ public class Transaction : Core.Entities.Transaction
{ {
public virtual Organization Organization { get; set; } public virtual Organization Organization { get; set; }
public virtual User User { get; set; } public virtual User User { get; set; }
public virtual Provider Provider { get; set; }
} }
public class TransactionMapperProfile : Profile public class TransactionMapperProfile : Profile

View File

@ -2,6 +2,7 @@
using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Auth.Models;
using Bit.Infrastructure.EntityFramework.Billing.Models;
using Bit.Infrastructure.EntityFramework.Converters; using Bit.Infrastructure.EntityFramework.Converters;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
@ -65,6 +66,7 @@ public class DatabaseContext : DbContext
public DbSet<AuthRequest> AuthRequests { get; set; } public DbSet<AuthRequest> AuthRequests { get; set; }
public DbSet<OrganizationDomain> OrganizationDomains { get; set; } public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; } public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
public DbSet<ProviderPlan> ProviderPlans { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {

View File

@ -47,4 +47,14 @@ public class TransactionRepository : Repository<Core.Entities.Transaction, Trans
return Mapper.Map<List<Core.Entities.Transaction>>(results); return Mapper.Map<List<Core.Entities.Transaction>>(results);
} }
} }
public async Task<ICollection<Core.Entities.Transaction>> GetManyByProviderIdAsync(Guid providerId)
{
using var serviceScope = ServiceScopeFactory.CreateScope();
var databaseContext = GetDatabaseContext(serviceScope);
var results = await databaseContext.Transactions
.Where(transaction => transaction.ProviderId == providerId)
.ToListAsync();
return Mapper.Map<List<Core.Entities.Transaction>>(results);
}
} }

View File

@ -7,8 +7,8 @@
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications' " /> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications' " />
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications-SelfHost' " /> <PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications-SelfHost' " />
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.2" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.2" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,30 @@
CREATE PROCEDURE [dbo].[ProviderPlan_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@ProviderId UNIQUEIDENTIFIER,
@PlanType TINYINT,
@SeatMinimum INT,
@PurchasedSeats INT,
@AllocatedSeats INT
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[ProviderPlan]
(
[Id],
[ProviderId],
[PlanType],
[SeatMinimum],
[PurchasedSeats],
[AllocatedSeats]
)
VALUES
(
@Id,
@ProviderId,
@PlanType,
@SeatMinimum,
@PurchasedSeats,
@AllocatedSeats
)
END

View File

@ -0,0 +1,12 @@
CREATE PROCEDURE [dbo].[ProviderPlan_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
DELETE
FROM
[dbo].[ProviderPlan]
WHERE
[Id] = @Id
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[ProviderPlan_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[ProviderPlanView]
WHERE
[Id] = @Id
END

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