mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Merge branch 'main' into vault/delete-only-can-manage
This commit is contained in:
commit
21e6d4e498
@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"swashbuckle.aspnetcore.cli": {
|
"swashbuckle.aspnetcore.cli": {
|
||||||
"version": "6.8.1",
|
"version": "6.9.0",
|
||||||
"commands": ["swagger"]
|
"commands": ["swagger"]
|
||||||
},
|
},
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
|
216
.github/workflows/repository-management.yml
vendored
216
.github/workflows/repository-management.yml
vendored
@ -3,12 +3,13 @@ name: Repository management
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
branch_to_cut:
|
task:
|
||||||
default: "rc"
|
default: "Version Bump"
|
||||||
description: "Branch to cut"
|
description: "Task to execute"
|
||||||
options:
|
options:
|
||||||
- "rc"
|
- "Version Bump"
|
||||||
- "hotfix-rc"
|
- "Version Bump and Cut rc"
|
||||||
|
- "Version Bump and Cut hotfix-rc"
|
||||||
required: true
|
required: true
|
||||||
type: choice
|
type: choice
|
||||||
target_ref:
|
target_ref:
|
||||||
@ -22,18 +23,51 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
setup:
|
||||||
|
name: Setup
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
branch: ${{ steps.set-branch.outputs.branch }}
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
steps:
|
||||||
|
- name: Set branch
|
||||||
|
id: set-branch
|
||||||
|
env:
|
||||||
|
TASK: ${{ inputs.task }}
|
||||||
|
run: |
|
||||||
|
if [[ "$TASK" == "Version Bump" ]]; then
|
||||||
|
BRANCH="none"
|
||||||
|
elif [[ "$TASK" == "Version Bump and Cut rc" ]]; then
|
||||||
|
BRANCH="rc"
|
||||||
|
elif [[ "$TASK" == "Version Bump and Cut hotfix-rc" ]]; then
|
||||||
|
BRANCH="hotfix-rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate GH App token
|
||||||
|
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||||
|
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||||
|
|
||||||
|
|
||||||
cut_branch:
|
cut_branch:
|
||||||
name: Cut branch
|
name: Cut branch
|
||||||
runs-on: ubuntu-22.04
|
if: ${{ needs.setup.outputs.branch != 'none' }}
|
||||||
|
needs: setup
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out target ref
|
- name: Check out target ref
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.target_ref }}
|
ref: ${{ inputs.target_ref }}
|
||||||
|
token: ${{ needs.setup.outputs.token }}
|
||||||
|
|
||||||
- name: Check if ${{ inputs.branch_to_cut }} branch exists
|
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||||
env:
|
env:
|
||||||
BRANCH_NAME: ${{ inputs.branch_to_cut }}
|
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||||
@ -42,7 +76,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Cut branch
|
- name: Cut branch
|
||||||
env:
|
env:
|
||||||
BRANCH_NAME: ${{ inputs.branch_to_cut }}
|
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
git switch --quiet --create $BRANCH_NAME
|
git switch --quiet --create $BRANCH_NAME
|
||||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||||
@ -50,8 +84,11 @@ jobs:
|
|||||||
|
|
||||||
bump_version:
|
bump_version:
|
||||||
name: Bump Version
|
name: Bump Version
|
||||||
runs-on: ubuntu-22.04
|
if: ${{ always() }}
|
||||||
needs: cut_branch
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- cut_branch
|
||||||
|
- setup
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
@ -65,6 +102,12 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
token: ${{ needs.setup.outputs.token }}
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config --local user.email "actions@github.com"
|
||||||
|
git config --local user.name "Github Actions"
|
||||||
|
|
||||||
- name: Install xmllint
|
- name: Install xmllint
|
||||||
run: |
|
run: |
|
||||||
@ -123,133 +166,69 @@ jobs:
|
|||||||
|
|
||||||
- name: Set final version output
|
- name: Set final version output
|
||||||
id: set-final-version-output
|
id: set-final-version-output
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version_number_override }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
||||||
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
|
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
|
||||||
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Configure Git
|
|
||||||
run: |
|
|
||||||
git config --local user.email "actions@github.com"
|
|
||||||
git config --local user.name "Github Actions"
|
|
||||||
|
|
||||||
- name: Create version branch
|
|
||||||
id: create-branch
|
|
||||||
run: |
|
|
||||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
|
||||||
git switch -c $NAME
|
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
run: git push
|
run: git push
|
||||||
|
|
||||||
- name: Generate GH App token
|
|
||||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
|
||||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
|
||||||
owner: ${{ github.repository_owner }}
|
|
||||||
|
|
||||||
- name: Create version PR
|
|
||||||
id: create-pr
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
|
||||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
|
||||||
run: |
|
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
|
||||||
--base "main" \
|
|
||||||
--head "$PR_BRANCH" \
|
|
||||||
--label "version update" \
|
|
||||||
--label "automated pr" \
|
|
||||||
--body "
|
|
||||||
## Type of change
|
|
||||||
- [ ] Bug fix
|
|
||||||
- [ ] New feature development
|
|
||||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
|
||||||
- [ ] Build/deploy pipeline (DevOps)
|
|
||||||
- [X] Other
|
|
||||||
## Objective
|
|
||||||
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
|
||||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Approve PR
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
|
||||||
run: gh pr review $PR_NUMBER --approve
|
|
||||||
|
|
||||||
- name: Merge PR
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
|
||||||
|
|
||||||
|
|
||||||
cherry_pick:
|
cherry_pick:
|
||||||
name: Cherry-Pick Commit(s)
|
name: Cherry-Pick Commit(s)
|
||||||
runs-on: ubuntu-22.04
|
if: ${{ needs.setup.outputs.branch != 'none' }}
|
||||||
needs: bump_version
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- bump_version
|
||||||
|
- setup
|
||||||
steps:
|
steps:
|
||||||
- name: Check out main branch
|
- name: Check out main branch
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
token: ${{ needs.setup.outputs.token }}
|
||||||
- name: Install xmllint
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libxml2-utils
|
|
||||||
|
|
||||||
- name: Verify version has been updated
|
|
||||||
env:
|
|
||||||
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
|
||||||
run: |
|
|
||||||
# Wait for version to change.
|
|
||||||
while : ; do
|
|
||||||
echo "Waiting for version to be updated..."
|
|
||||||
git pull --force
|
|
||||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
|
||||||
|
|
||||||
# If the versions don't match we continue the loop, otherwise we break out of the loop.
|
|
||||||
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Get last version commit(s)
|
|
||||||
id: get-commits
|
|
||||||
run: |
|
|
||||||
git switch main
|
|
||||||
MAIN_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
|
|
||||||
echo "main_commit=$MAIN_COMMIT" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
if [[ $(git ls-remote --heads origin rc) ]]; then
|
|
||||||
git switch rc
|
|
||||||
RC_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
|
|
||||||
echo "rc_commit=$RC_COMMIT" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
|
||||||
echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Configure Git
|
- name: Configure Git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "actions@github.com"
|
git config --local user.email "actions@github.com"
|
||||||
git config --local user.name "Github Actions"
|
git config --local user.name "Github Actions"
|
||||||
|
|
||||||
|
- name: Install xmllint
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxml2-utils
|
||||||
|
|
||||||
- name: Perform cherry-pick(s)
|
- name: Perform cherry-pick(s)
|
||||||
env:
|
env:
|
||||||
CUT_BRANCH: ${{ inputs.branch_to_cut }}
|
CUT_BRANCH: ${{ needs.setup.outputs.branch }}
|
||||||
MAIN_COMMIT: ${{ steps.get-commits.outputs.main_commit }}
|
|
||||||
RC_COMMIT: ${{ steps.get-commits.outputs.rc_commit }}
|
|
||||||
RC_VERSION: ${{ steps.get-commits.outputs.rc_version }}
|
|
||||||
run: |
|
run: |
|
||||||
|
# Function for cherry-picking
|
||||||
|
cherry_pick () {
|
||||||
|
local source_branch=$1
|
||||||
|
local destination_branch=$2
|
||||||
|
|
||||||
|
# Get project commit/version from source branch
|
||||||
|
git switch $source_branch
|
||||||
|
SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
|
||||||
|
SOURCE_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||||
|
|
||||||
|
# Get project commit/version from destination branch
|
||||||
|
git switch $destination_branch
|
||||||
|
DESTINATION_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||||
|
|
||||||
|
if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
|
||||||
|
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
|
||||||
|
git push -u origin $destination_branch
|
||||||
|
fi
|
||||||
|
|
||||||
# If we are cutting 'hotfix-rc':
|
# If we are cutting 'hotfix-rc':
|
||||||
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
|
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
|
||||||
|
|
||||||
@ -257,25 +236,16 @@ jobs:
|
|||||||
if [[ $(git ls-remote --heads origin rc) ]]; then
|
if [[ $(git ls-remote --heads origin rc) ]]; then
|
||||||
|
|
||||||
# Chery-pick from 'rc' into 'hotfix-rc'
|
# Chery-pick from 'rc' into 'hotfix-rc'
|
||||||
git switch hotfix-rc
|
cherry_pick rc hotfix-rc
|
||||||
HOTFIX_RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
|
||||||
if [[ "$HOTFIX_RC_VERSION" != "$RC_VERSION" ]]; then
|
|
||||||
git cherry-pick --strategy-option=theirs -x $RC_COMMIT
|
|
||||||
git push -u origin hotfix-rc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cherry-pick from 'main' into 'rc'
|
# Cherry-pick from 'main' into 'rc'
|
||||||
git switch rc
|
cherry_pick main rc
|
||||||
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
|
|
||||||
git push -u origin rc
|
|
||||||
|
|
||||||
# If the 'rc' branch does not exist:
|
# If the 'rc' branch does not exist:
|
||||||
else
|
else
|
||||||
|
|
||||||
# Cherry-pick from 'main' into 'hotfix-rc'
|
# Cherry-pick from 'main' into 'hotfix-rc'
|
||||||
git switch hotfix-rc
|
cherry_pick main hotfix-rc
|
||||||
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
|
|
||||||
git push -u origin hotfix-rc
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -283,9 +253,7 @@ jobs:
|
|||||||
elif [[ "$CUT_BRANCH" == "rc" ]]; then
|
elif [[ "$CUT_BRANCH" == "rc" ]]; then
|
||||||
|
|
||||||
# Cherry-pick from 'main' into 'rc'
|
# Cherry-pick from 'main' into 'rc'
|
||||||
git switch rc
|
cherry_pick main rc
|
||||||
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
|
|
||||||
git push -u origin rc
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2024.10.1</Version>
|
<Version>2024.11.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -392,7 +392,9 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
ThrowOnInvalidPlanType(organization.PlanType);
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
|
ThrowOnInvalidPlanType(provider.Type, organization.PlanType);
|
||||||
|
|
||||||
if (organization.UseSecretsManager)
|
if (organization.UseSecretsManager)
|
||||||
{
|
{
|
||||||
@ -407,8 +409,6 @@ public class ProviderService : IProviderService
|
|||||||
Key = key,
|
Key = key,
|
||||||
};
|
};
|
||||||
|
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
||||||
|
|
||||||
await ApplyProviderPriceRateAsync(organization, provider);
|
await ApplyProviderPriceRateAsync(organization, provider);
|
||||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||||
|
|
||||||
@ -547,7 +547,7 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
|
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
|
||||||
|
|
||||||
ThrowOnInvalidPlanType(organizationSignup.Plan, consolidatedBillingEnabled);
|
ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan, consolidatedBillingEnabled);
|
||||||
|
|
||||||
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
var (organization, _, defaultCollection) = consolidatedBillingEnabled
|
||||||
? await _organizationService.SignupClientAsync(organizationSignup)
|
? await _organizationService.SignupClientAsync(organizationSignup)
|
||||||
@ -687,11 +687,27 @@ public class ProviderService : IProviderService
|
|||||||
return confirmedOwnersIds.Except(providerUserIds).Any();
|
return confirmedOwnersIds.Except(providerUserIds).Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThrowOnInvalidPlanType(PlanType requestedType, bool consolidatedBillingEnabled = false)
|
private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType, bool consolidatedBillingEnabled = false)
|
||||||
{
|
{
|
||||||
if (consolidatedBillingEnabled && requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
|
if (consolidatedBillingEnabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
|
switch (providerType)
|
||||||
|
{
|
||||||
|
case ProviderType.Msp:
|
||||||
|
if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually))
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
||||||
|
@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -208,16 +209,9 @@ public class ProviderBillingService(
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(provider);
|
ArgumentNullException.ThrowIfNull(provider);
|
||||||
|
|
||||||
if (provider.Type != ProviderType.Msp)
|
if (!provider.SupportsConsolidatedBilling())
|
||||||
{
|
{
|
||||||
logger.LogError("Non-MSP provider ({ProviderID}) cannot scale their seats", provider.Id);
|
logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id);
|
||||||
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!planType.SupportsConsolidatedBilling())
|
|
||||||
{
|
|
||||||
logger.LogError("Cannot scale provider ({ProviderID}) seats for plan type {PlanType} as it does not support consolidated billing", provider.Id, planType.ToString());
|
|
||||||
|
|
||||||
throw new BillingException();
|
throw new BillingException();
|
||||||
}
|
}
|
||||||
@ -437,144 +431,159 @@ public class ProviderBillingService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateSeatMinimums(
|
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||||
Provider provider,
|
|
||||||
int enterpriseSeatMinimum,
|
|
||||||
int teamsSeatMinimum)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(provider);
|
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||||
|
|
||||||
if (enterpriseSeatMinimum < 0 || teamsSeatMinimum < 0)
|
if (plan == null)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Provider plan not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.PlanType == command.NewPlan)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||||
|
|
||||||
|
plan.PlanType = command.NewPlan;
|
||||||
|
await providerPlanRepository.ReplaceAsync(plan);
|
||||||
|
|
||||||
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw new ConflictException("Subscription not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldSubscriptionItem = subscription.Items.SingleOrDefault(x =>
|
||||||
|
x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId);
|
||||||
|
|
||||||
|
var updateOptions = new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
Items =
|
||||||
|
[
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||||
|
Quantity = oldSubscriptionItem!.Quantity
|
||||||
|
},
|
||||||
|
new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = oldSubscriptionItem.Id,
|
||||||
|
Deleted = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
|
||||||
|
|
||||||
|
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
|
||||||
|
// 1. Retrieve PlanType and PlanName for ProviderPlan
|
||||||
|
// 2. Assign PlanType & PlanName to Organization
|
||||||
|
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(plan.ProviderId);
|
||||||
|
|
||||||
|
foreach (var providerOrganization in providerOrganizations)
|
||||||
|
{
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
||||||
|
}
|
||||||
|
organization.PlanType = command.NewPlan;
|
||||||
|
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name;
|
||||||
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||||
|
{
|
||||||
|
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId);
|
Subscription subscription;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, command.Id);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
throw new ConflictException("Subscription not found.");
|
||||||
|
}
|
||||||
|
|
||||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||||
|
|
||||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
var providerPlans = await providerPlanRepository.GetByProviderId(command.Id);
|
||||||
|
|
||||||
var enterpriseProviderPlan =
|
foreach (var newPlanConfiguration in command.Configuration)
|
||||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
|
||||||
|
|
||||||
if (enterpriseProviderPlan.SeatMinimum != enterpriseSeatMinimum)
|
|
||||||
{
|
{
|
||||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager
|
var providerPlan =
|
||||||
.StripeProviderPortalSeatPlanId;
|
providerPlans.Single(providerPlan => providerPlan.PlanType == newPlanConfiguration.Plan);
|
||||||
|
|
||||||
var enterpriseSubscriptionItem = subscription.Items.First(item => item.Price.Id == enterprisePriceId);
|
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
|
||||||
|
|
||||||
if (enterpriseProviderPlan.PurchasedSeats == 0)
|
|
||||||
{
|
{
|
||||||
if (enterpriseProviderPlan.AllocatedSeats > enterpriseSeatMinimum)
|
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager
|
||||||
{
|
.StripeProviderPortalSeatPlanId;
|
||||||
enterpriseProviderPlan.PurchasedSeats =
|
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
|
||||||
enterpriseProviderPlan.AllocatedSeats - enterpriseSeatMinimum;
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
if (providerPlan.PurchasedSeats == 0)
|
||||||
|
{
|
||||||
|
if (providerPlan.AllocatedSeats > newPlanConfiguration.SeatsMinimum)
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - newPlanConfiguration.SeatsMinimum;
|
||||||
Price = enterprisePriceId,
|
|
||||||
Quantity = enterpriseProviderPlan.AllocatedSeats
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
});
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = priceId,
|
||||||
|
Quantity = providerPlan.AllocatedSeats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = priceId,
|
||||||
|
Quantity = newPlanConfiguration.SeatsMinimum
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats;
|
||||||
|
|
||||||
|
if (newPlanConfiguration.SeatsMinimum <= totalSeats)
|
||||||
{
|
{
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
providerPlan.PurchasedSeats = totalSeats - newPlanConfiguration.SeatsMinimum;
|
||||||
Price = enterprisePriceId,
|
}
|
||||||
Quantity = enterpriseSeatMinimum
|
else
|
||||||
});
|
{
|
||||||
|
providerPlan.PurchasedSeats = 0;
|
||||||
|
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = priceId,
|
||||||
|
Quantity = newPlanConfiguration.SeatsMinimum
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
providerPlan.SeatMinimum = newPlanConfiguration.SeatsMinimum;
|
||||||
|
|
||||||
|
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var totalEnterpriseSeats = enterpriseProviderPlan.SeatMinimum + enterpriseProviderPlan.PurchasedSeats;
|
|
||||||
|
|
||||||
if (enterpriseSeatMinimum <= totalEnterpriseSeats)
|
|
||||||
{
|
|
||||||
enterpriseProviderPlan.PurchasedSeats = totalEnterpriseSeats - enterpriseSeatMinimum;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
enterpriseProviderPlan.PurchasedSeats = 0;
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = enterpriseSubscriptionItem.Id,
|
|
||||||
Price = enterprisePriceId,
|
|
||||||
Quantity = enterpriseSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enterpriseProviderPlan.SeatMinimum = enterpriseSeatMinimum;
|
|
||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan);
|
|
||||||
}
|
|
||||||
|
|
||||||
var teamsProviderPlan =
|
|
||||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
|
||||||
|
|
||||||
if (teamsProviderPlan.SeatMinimum != teamsSeatMinimum)
|
|
||||||
{
|
|
||||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager
|
|
||||||
.StripeProviderPortalSeatPlanId;
|
|
||||||
|
|
||||||
var teamsSubscriptionItem = subscription.Items.First(item => item.Price.Id == teamsPriceId);
|
|
||||||
|
|
||||||
if (teamsProviderPlan.PurchasedSeats == 0)
|
|
||||||
{
|
|
||||||
if (teamsProviderPlan.AllocatedSeats > teamsSeatMinimum)
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = teamsProviderPlan.AllocatedSeats - teamsSeatMinimum;
|
|
||||||
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsProviderPlan.AllocatedSeats
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var totalTeamsSeats = teamsProviderPlan.SeatMinimum + teamsProviderPlan.PurchasedSeats;
|
|
||||||
|
|
||||||
if (teamsSeatMinimum <= totalTeamsSeats)
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = totalTeamsSeats - teamsSeatMinimum;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
teamsProviderPlan.PurchasedSeats = 0;
|
|
||||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
|
||||||
{
|
|
||||||
Id = teamsSubscriptionItem.Id,
|
|
||||||
Price = teamsPriceId,
|
|
||||||
Quantity = teamsSeatMinimum
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
teamsProviderPlan.SeatMinimum = teamsSeatMinimum;
|
|
||||||
|
|
||||||
await providerPlanRepository.ReplaceAsync(teamsProviderPlan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionItemOptionsList.Count > 0)
|
if (subscriptionItemOptionsList.Count > 0)
|
||||||
{
|
{
|
||||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId,
|
||||||
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using Bit.Core.Billing.Entities;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -1011,26 +1012,192 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UpdateSeatMinimums
|
#region ChangePlan
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NullProvider_ThrowsArgumentNullException(
|
public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException(
|
||||||
SutProvider<ProviderBillingService> sutProvider) =>
|
ChangeProviderPlanCommand command,
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdateSeatMinimums(null, 0, 0));
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
providerPlanRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((ProviderPlan)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ChangePlan(command));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Provider plan not found.", actual.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_ProviderNotFound_DoesNothing(
|
||||||
|
ChangeProviderPlanCommand command,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = command.ProviderPlanId,
|
||||||
|
PlanType = command.NewPlan,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0,
|
||||||
|
SeatMinimum = 0
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||||
|
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_SameProviderPlan_DoesNothing(
|
||||||
|
ChangeProviderPlanCommand command,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = command.ProviderPlanId,
|
||||||
|
PlanType = command.NewPlan,
|
||||||
|
PurchasedSeats = 0,
|
||||||
|
AllocatedSeats = 0,
|
||||||
|
SeatMinimum = 0
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||||
|
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ChangePlan_UpdatesSubscriptionCorrectly(
|
||||||
|
Guid providerPlanId,
|
||||||
|
Provider provider,
|
||||||
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var existingPlan = new ProviderPlan
|
||||||
|
{
|
||||||
|
Id = providerPlanId,
|
||||||
|
ProviderId = provider.Id,
|
||||||
|
PlanType = PlanType.EnterpriseAnnually,
|
||||||
|
PurchasedSeats = 2,
|
||||||
|
AllocatedSeats = 10,
|
||||||
|
SeatMinimum = 8
|
||||||
|
};
|
||||||
|
providerPlanRepository
|
||||||
|
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
|
||||||
|
.Returns(existingPlan);
|
||||||
|
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider);
|
||||||
|
|
||||||
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
|
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is(provider.Id))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Id = provider.GatewaySubscriptionId,
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data =
|
||||||
|
[
|
||||||
|
new SubscriptionItem
|
||||||
|
{
|
||||||
|
Id = "si_ent_annual",
|
||||||
|
Price = new Price
|
||||||
|
{
|
||||||
|
Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager
|
||||||
|
.StripeProviderPortalSeatPlanId
|
||||||
|
},
|
||||||
|
Quantity = 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var command =
|
||||||
|
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.ChangePlan(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await providerPlanRepository.Received(1)
|
||||||
|
.ReplaceAsync(Arg.Is<ProviderPlan>(p => p.PlanType == PlanType.EnterpriseMonthly));
|
||||||
|
|
||||||
|
await stripeAdapter.Received(1)
|
||||||
|
.SubscriptionUpdateAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||||
|
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
|
||||||
|
|
||||||
|
var newPlanCfg = StaticStore.GetPlan(command.NewPlan);
|
||||||
|
await stripeAdapter.Received(1)
|
||||||
|
.SubscriptionUpdateAsync(
|
||||||
|
Arg.Is(provider.GatewaySubscriptionId),
|
||||||
|
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||||
|
p.Items.Count(si =>
|
||||||
|
si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId &&
|
||||||
|
si.Deleted == default &&
|
||||||
|
si.Quantity == 10) == 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UpdateSeatMinimums
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider) =>
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(provider, -10, 100));
|
{
|
||||||
|
// Arrange
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.TeamsMonthly, -10),
|
||||||
|
(PlanType.EnterpriseMonthly, 50)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(command));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Provider seat minimums must be at least 0.", actual.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1058,7 +1225,9 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1066,10 +1235,21 @@ public class ProviderBillingServiceTests
|
|||||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 30, 20);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 30),
|
||||||
|
(PlanType.TeamsMonthly, 20)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
||||||
|
|
||||||
@ -1091,8 +1271,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1120,7 +1303,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1130,8 +1313,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 50);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 70),
|
||||||
|
(PlanType.TeamsMonthly, 50)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||||
|
|
||||||
@ -1153,8 +1346,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1182,7 +1378,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1192,8 +1388,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 60, 60);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 60),
|
||||||
|
(PlanType.TeamsMonthly, 60)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
||||||
|
|
||||||
@ -1209,8 +1415,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1238,7 +1447,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1248,8 +1457,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 80, 80);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 80),
|
||||||
|
(PlanType.TeamsMonthly, 80)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
||||||
|
|
||||||
@ -1271,8 +1490,11 @@ public class ProviderBillingServiceTests
|
|||||||
Provider provider,
|
Provider provider,
|
||||||
SutProvider<ProviderBillingService> sutProvider)
|
SutProvider<ProviderBillingService> sutProvider)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||||
const string teamsLineItemId = "teams_line_item_id";
|
const string teamsLineItemId = "teams_line_item_id";
|
||||||
@ -1300,7 +1522,7 @@ public class ProviderBillingServiceTests
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||||
|
|
||||||
var providerPlans = new List<ProviderPlan>
|
var providerPlans = new List<ProviderPlan>
|
||||||
{
|
{
|
||||||
@ -1310,8 +1532,18 @@ public class ProviderBillingServiceTests
|
|||||||
|
|
||||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 30);
|
var command = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(PlanType.EnterpriseMonthly, 70),
|
||||||
|
(PlanType.TeamsMonthly, 30)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -290,25 +291,39 @@ public class ProvidersController : Controller
|
|||||||
|
|
||||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||||
|
|
||||||
if (providerPlans.Count == 0)
|
switch (provider.Type)
|
||||||
{
|
{
|
||||||
var newProviderPlans = new List<ProviderPlan>
|
case ProviderType.Msp:
|
||||||
{
|
var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
provider.Id,
|
||||||
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
provider.GatewaySubscriptionId,
|
||||||
};
|
[
|
||||||
|
(Plan: PlanType.TeamsMonthly, SeatsMinimum: model.TeamsMonthlySeatMinimum),
|
||||||
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
||||||
|
]);
|
||||||
|
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||||
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
{
|
||||||
|
var existingMoePlan = providerPlans.Single();
|
||||||
|
|
||||||
foreach (var newProviderPlan in newProviderPlans)
|
// 1. Change the plan and take over any old values.
|
||||||
{
|
var changeMoePlanCommand = new ChangeProviderPlanCommand(
|
||||||
await _providerPlanRepository.CreateAsync(newProviderPlan);
|
existingMoePlan.Id,
|
||||||
}
|
model.Plan!.Value,
|
||||||
}
|
provider.GatewaySubscriptionId);
|
||||||
else
|
await _providerBillingService.ChangePlan(changeMoePlanCommand);
|
||||||
{
|
|
||||||
await _providerBillingService.UpdateSeatMinimums(
|
// 2. Update the seat minimums.
|
||||||
provider,
|
var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
model.EnterpriseMonthlySeatMinimum,
|
provider.Id,
|
||||||
model.TeamsMonthlySeatMinimum);
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value)
|
||||||
|
]);
|
||||||
|
await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction("Edit", new { id });
|
return RedirectToAction("Edit", new { id });
|
||||||
|
@ -33,6 +33,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||||
Type = provider.Type;
|
Type = provider.Type;
|
||||||
|
|
||||||
|
if (Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
var plan = providerPlans.SingleOrDefault();
|
||||||
|
EnterpriseMinimumSeats = plan?.SeatMinimum ?? 0;
|
||||||
|
Plan = plan?.PlanType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Display(Name = "Billing Email")]
|
[Display(Name = "Billing Email")]
|
||||||
@ -58,13 +65,24 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
[Display(Name = "Provider Type")]
|
[Display(Name = "Provider Type")]
|
||||||
public ProviderType Type { get; set; }
|
public ProviderType Type { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Plan")]
|
||||||
|
public PlanType? Plan { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enterprise Seats Minimum")]
|
||||||
|
public int? EnterpriseMinimumSeats { get; set; }
|
||||||
|
|
||||||
public virtual Provider ToProvider(Provider existingProvider)
|
public virtual Provider ToProvider(Provider existingProvider)
|
||||||
{
|
{
|
||||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||||
existingProvider.Gateway = Gateway;
|
switch (Type)
|
||||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
{
|
||||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
case ProviderType.Msp:
|
||||||
|
existingProvider.Gateway = Gateway;
|
||||||
|
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||||
|
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
return existingProvider;
|
return existingProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +100,23 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
|||||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
if (Plan == null)
|
||||||
|
{
|
||||||
|
var displayName = nameof(Plan).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Plan);
|
||||||
|
yield return new ValidationResult($"The {displayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats == null)
|
||||||
|
{
|
||||||
|
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {displayName} field is required.");
|
||||||
|
}
|
||||||
|
if (EnterpriseMinimumSeats < 0)
|
||||||
|
{
|
||||||
|
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||||
|
yield return new ValidationResult($"The {displayName} field cannot be less than 0.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
@using Bit.Admin.Enums;
|
@using Bit.Admin.Enums;
|
||||||
@using Bit.Core
|
@using Bit.Core
|
||||||
|
@using Bit.Core.AdminConsole.Enums.Provider
|
||||||
|
@using Bit.Core.Billing.Enums
|
||||||
@using Bit.Core.Billing.Extensions
|
@using Bit.Core.Billing.Extensions
|
||||||
|
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
|
||||||
@ -47,60 +50,97 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||||
{
|
{
|
||||||
<div class="row">
|
switch (Model.Provider.Type)
|
||||||
<div class="col-sm">
|
{
|
||||||
<div class="form-group">
|
case ProviderType.Msp:
|
||||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
{
|
||||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
<div class="row">
|
||||||
</div>
|
<div class="col-sm">
|
||||||
</div>
|
<div class="form-group">
|
||||||
<div class="col-sm">
|
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||||
<div class="form-group">
|
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
</div>
|
||||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Gateway"></label>
|
|
||||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
|
||||||
<option value="">--</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-sm">
|
||||||
</div>
|
<div class="form-group">
|
||||||
</div>
|
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||||
<div class="row">
|
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||||
<div class="col-sm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="GatewayCustomerId"></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
|
||||||
<i class="fa fa-external-link"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="GatewaySubscriptionId"></label>
|
<div class="form-group">
|
||||||
<div class="input-group">
|
<label asp-for="Gateway"></label>
|
||||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||||
<div class="input-group-append">
|
<option value="">--</option>
|
||||||
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
</select>
|
||||||
<i class="fa fa-external-link"></i>
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
</div>
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="GatewayCustomerId"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="GatewaySubscriptionId"></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
||||||
|
<i class="fa fa-external-link"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ProviderType.MultiOrganizationEnterprise:
|
||||||
|
{
|
||||||
|
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
@{
|
||||||
|
var multiOrgPlans = new List<PlanType>
|
||||||
|
{
|
||||||
|
PlanType.EnterpriseAnnually,
|
||||||
|
PlanType.EnterpriseMonthly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<label asp-for="Plan"></label>
|
||||||
|
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||||
|
<option value="">--</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||||
|
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
@await Html.PartialAsync("Organizations", Model)
|
@await Html.PartialAsync("Organizations", Model)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
|
||||||
using Bit.Api.Models.Request.Organizations;
|
using Bit.Api.Models.Request.Organizations;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
@ -545,7 +544,7 @@ public class OrganizationUsersController : Controller
|
|||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||||
[HttpDelete("{id}/delete-account")]
|
[HttpDelete("{id}/delete-account")]
|
||||||
[HttpPost("{id}/delete-account")]
|
[HttpPost("{id}/delete-account")]
|
||||||
public async Task DeleteAccount(Guid orgId, Guid id, [FromBody] SecretVerificationRequestModel model)
|
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.ManageUsers(orgId))
|
if (!await _currentContext.ManageUsers(orgId))
|
||||||
{
|
{
|
||||||
@ -558,19 +557,13 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.VerifySecretAsync(currentUser, model.Secret))
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||||
[HttpDelete("delete-account")]
|
[HttpDelete("delete-account")]
|
||||||
[HttpPost("delete-account")]
|
[HttpPost("delete-account")]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] SecureOrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.ManageUsers(orgId))
|
if (!await _currentContext.ManageUsers(orgId))
|
||||||
{
|
{
|
||||||
@ -583,12 +576,6 @@ public class OrganizationUsersController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _userService.VerifySecretAsync(currentUser, model.Secret))
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
|
var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
|
||||||
|
|
||||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
|
||||||
|
|
||||||
public class SecureOrganizationUserBulkRequestModel : SecretVerificationRequestModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<Guid> Ids { get; set; }
|
|
||||||
}
|
|
@ -35,7 +35,7 @@
|
|||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -580,6 +580,13 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _userService.DeleteAsync(user);
|
var result = await _userService.DeleteAsync(user);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
|
@ -26,7 +26,7 @@ public class OrganizationBillingController(
|
|||||||
[HttpGet("metadata")]
|
[HttpGet("metadata")]
|
||||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||||
{
|
{
|
||||||
if (!await currentContext.AccessMembersTab(organizationId))
|
if (!await currentContext.OrganizationUser(organizationId))
|
||||||
{
|
{
|
||||||
return Error.Unauthorized();
|
return Error.Unauthorized();
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,8 @@ public class ProviderBillingController(
|
|||||||
subscription,
|
subscription,
|
||||||
providerPlans,
|
providerPlans,
|
||||||
taxInformation,
|
taxInformation,
|
||||||
subscriptionSuspension);
|
subscriptionSuspension,
|
||||||
|
provider);
|
||||||
|
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ public class CreateClientOrganizationRequestBody
|
|||||||
[Required(ErrorMessage = "'ownerEmail' must be provided")]
|
[Required(ErrorMessage = "'ownerEmail' must be provided")]
|
||||||
public string OwnerEmail { get; set; }
|
public string OwnerEmail { get; set; }
|
||||||
|
|
||||||
[EnumMatches<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, ErrorMessage = "'planType' must be Teams (Monthly) or Enterprise (Monthly)")]
|
[EnumMatches<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually, ErrorMessage = "'planType' must be Teams (Monthly), Enterprise (Monthly) or Enterprise (Annually)")]
|
||||||
public PlanType PlanType { get; set; }
|
public PlanType PlanType { get; set; }
|
||||||
|
|
||||||
[Range(1, int.MaxValue, ErrorMessage = "'seats' must be greater than 0")]
|
[Range(1, int.MaxValue, ErrorMessage = "'seats' must be greater than 0")]
|
||||||
|
@ -4,10 +4,14 @@ namespace Bit.Api.Billing.Models.Responses;
|
|||||||
|
|
||||||
public record OrganizationMetadataResponse(
|
public record OrganizationMetadataResponse(
|
||||||
bool IsEligibleForSelfHost,
|
bool IsEligibleForSelfHost,
|
||||||
bool IsOnSecretsManagerStandalone)
|
bool IsManaged,
|
||||||
|
bool IsOnSecretsManagerStandalone,
|
||||||
|
bool IsSubscriptionUnpaid)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||||
=> new(
|
=> new(
|
||||||
metadata.IsEligibleForSelfHost,
|
metadata.IsEligibleForSelfHost,
|
||||||
metadata.IsOnSecretsManagerStandalone);
|
metadata.IsManaged,
|
||||||
|
metadata.IsOnSecretsManagerStandalone,
|
||||||
|
metadata.IsSubscriptionUnpaid);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
@ -14,7 +17,8 @@ public record ProviderSubscriptionResponse(
|
|||||||
decimal AccountCredit,
|
decimal AccountCredit,
|
||||||
TaxInformation TaxInformation,
|
TaxInformation TaxInformation,
|
||||||
DateTime? CancelAt,
|
DateTime? CancelAt,
|
||||||
SubscriptionSuspension Suspension)
|
SubscriptionSuspension Suspension,
|
||||||
|
ProviderType ProviderType)
|
||||||
{
|
{
|
||||||
private const string _annualCadence = "Annual";
|
private const string _annualCadence = "Annual";
|
||||||
private const string _monthlyCadence = "Monthly";
|
private const string _monthlyCadence = "Monthly";
|
||||||
@ -23,7 +27,8 @@ public record ProviderSubscriptionResponse(
|
|||||||
Subscription subscription,
|
Subscription subscription,
|
||||||
ICollection<ProviderPlan> providerPlans,
|
ICollection<ProviderPlan> providerPlans,
|
||||||
TaxInformation taxInformation,
|
TaxInformation taxInformation,
|
||||||
SubscriptionSuspension subscriptionSuspension)
|
SubscriptionSuspension subscriptionSuspension,
|
||||||
|
Provider provider)
|
||||||
{
|
{
|
||||||
var providerPlanResponses = providerPlans
|
var providerPlanResponses = providerPlans
|
||||||
.Where(providerPlan => providerPlan.IsConfigured())
|
.Where(providerPlan => providerPlan.IsConfigured())
|
||||||
@ -35,6 +40,8 @@ public record ProviderSubscriptionResponse(
|
|||||||
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
||||||
return new ProviderPlanResponse(
|
return new ProviderPlanResponse(
|
||||||
plan.Name,
|
plan.Name,
|
||||||
|
plan.Type,
|
||||||
|
plan.ProductTier,
|
||||||
configuredProviderPlan.SeatMinimum,
|
configuredProviderPlan.SeatMinimum,
|
||||||
configuredProviderPlan.PurchasedSeats,
|
configuredProviderPlan.PurchasedSeats,
|
||||||
configuredProviderPlan.AssignedSeats,
|
configuredProviderPlan.AssignedSeats,
|
||||||
@ -53,12 +60,15 @@ public record ProviderSubscriptionResponse(
|
|||||||
accountCredit,
|
accountCredit,
|
||||||
taxInformation,
|
taxInformation,
|
||||||
subscription.CancelAt,
|
subscription.CancelAt,
|
||||||
subscriptionSuspension);
|
subscriptionSuspension,
|
||||||
|
provider.Type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ProviderPlanResponse(
|
public record ProviderPlanResponse(
|
||||||
string PlanName,
|
string PlanName,
|
||||||
|
PlanType Type,
|
||||||
|
ProductTierType ProductTier,
|
||||||
int SeatMinimum,
|
int SeatMinimum,
|
||||||
int PurchasedSeats,
|
int PurchasedSeats,
|
||||||
int AssignedSeats,
|
int AssignedSeats,
|
||||||
|
@ -196,8 +196,8 @@ public class DevicesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
[HttpPost("{id}/delete")]
|
[HttpPost("{id}/deactivate")]
|
||||||
public async Task Delete(string id)
|
public async Task Deactivate(string id)
|
||||||
{
|
{
|
||||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||||
if (device == null)
|
if (device == null)
|
||||||
@ -205,7 +205,7 @@ public class DevicesController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _deviceService.DeleteAsync(device);
|
await _deviceService.DeactivateAsync(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using Bit.Api.Vault.Models.Response;
|
using Bit.Api.Vault.Models.Response;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -10,6 +12,7 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Vault.Models.Data;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -30,6 +33,8 @@ public class SyncController : Controller
|
|||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly ISendRepository _sendRepository;
|
private readonly ISendRepository _sendRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public SyncController(
|
public SyncController(
|
||||||
@ -43,6 +48,7 @@ public class SyncController : Controller
|
|||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
ISendRepository sendRepository,
|
ISendRepository sendRepository,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
@ -55,6 +61,7 @@ public class SyncController : Controller
|
|||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +84,8 @@ public class SyncController : Controller
|
|||||||
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
||||||
|
|
||||||
var folders = await _folderRepository.GetManyByUserIdAsync(user.Id);
|
var folders = await _folderRepository.GetManyByUserIdAsync(user.Id);
|
||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs);
|
var allCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs);
|
||||||
|
var ciphers = FilterSSHKeys(allCiphers);
|
||||||
var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);
|
var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
|
||||||
IEnumerable<CollectionDetails> collections = null;
|
IEnumerable<CollectionDetails> collections = null;
|
||||||
@ -101,4 +109,16 @@ public class SyncController : Controller
|
|||||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)
|
||||||
|
{
|
||||||
|
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion)
|
||||||
|
{
|
||||||
|
return ciphers;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ciphers.Where(c => c.Type != Core.Vault.Enums.CipherType.SSHKey).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
src/Api/Vault/Models/CipherSSHKeyModel.cs
Normal file
26
src/Api/Vault/Models/CipherSSHKeyModel.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Api.Vault.Models;
|
||||||
|
|
||||||
|
public class CipherSSHKeyModel
|
||||||
|
{
|
||||||
|
public CipherSSHKeyModel() { }
|
||||||
|
|
||||||
|
public CipherSSHKeyModel(CipherSSHKeyData data)
|
||||||
|
{
|
||||||
|
PrivateKey = data.PrivateKey;
|
||||||
|
PublicKey = data.PublicKey;
|
||||||
|
KeyFingerprint = data.KeyFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(5000)]
|
||||||
|
public string PrivateKey { get; set; }
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(5000)]
|
||||||
|
public string PublicKey { get; set; }
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
|
public string KeyFingerprint { get; set; }
|
||||||
|
}
|
@ -37,6 +37,7 @@ public class CipherRequestModel
|
|||||||
public CipherCardModel Card { get; set; }
|
public CipherCardModel Card { get; set; }
|
||||||
public CipherIdentityModel Identity { get; set; }
|
public CipherIdentityModel Identity { get; set; }
|
||||||
public CipherSecureNoteModel SecureNote { get; set; }
|
public CipherSecureNoteModel SecureNote { get; set; }
|
||||||
|
public CipherSSHKeyModel SSHKey { get; set; }
|
||||||
public DateTime? LastKnownRevisionDate { get; set; } = null;
|
public DateTime? LastKnownRevisionDate { get; set; } = null;
|
||||||
|
|
||||||
public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)
|
public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)
|
||||||
@ -82,6 +83,9 @@ public class CipherRequestModel
|
|||||||
case CipherType.SecureNote:
|
case CipherType.SecureNote:
|
||||||
existingCipher.Data = JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull);
|
existingCipher.Data = JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull);
|
||||||
break;
|
break;
|
||||||
|
case CipherType.SSHKey:
|
||||||
|
existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
|
throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
|
||||||
}
|
}
|
||||||
@ -230,6 +234,21 @@ public class CipherRequestModel
|
|||||||
Type = SecureNote.Type,
|
Type = SecureNote.Type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CipherSSHKeyData ToCipherSSHKeyData()
|
||||||
|
{
|
||||||
|
return new CipherSSHKeyData
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Notes = Notes,
|
||||||
|
Fields = Fields?.Select(f => f.ToCipherFieldData()),
|
||||||
|
PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),
|
||||||
|
|
||||||
|
PrivateKey = SSHKey.PrivateKey,
|
||||||
|
PublicKey = SSHKey.PublicKey,
|
||||||
|
KeyFingerprint = SSHKey.KeyFingerprint,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CipherWithIdRequestModel : CipherRequestModel
|
public class CipherWithIdRequestModel : CipherRequestModel
|
||||||
|
@ -48,6 +48,12 @@ public class CipherMiniResponseModel : ResponseModel
|
|||||||
cipherData = identityData;
|
cipherData = identityData;
|
||||||
Identity = new CipherIdentityModel(identityData);
|
Identity = new CipherIdentityModel(identityData);
|
||||||
break;
|
break;
|
||||||
|
case CipherType.SSHKey:
|
||||||
|
var sshKeyData = JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data);
|
||||||
|
Data = sshKeyData;
|
||||||
|
cipherData = sshKeyData;
|
||||||
|
SSHKey = new CipherSSHKeyModel(sshKeyData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("Unsupported " + nameof(Type) + ".");
|
throw new ArgumentException("Unsupported " + nameof(Type) + ".");
|
||||||
}
|
}
|
||||||
@ -76,6 +82,7 @@ public class CipherMiniResponseModel : ResponseModel
|
|||||||
public CipherCardModel Card { get; set; }
|
public CipherCardModel Card { get; set; }
|
||||||
public CipherIdentityModel Identity { get; set; }
|
public CipherIdentityModel Identity { get; set; }
|
||||||
public CipherSecureNoteModel SecureNote { get; set; }
|
public CipherSecureNoteModel SecureNote { get; set; }
|
||||||
|
public CipherSSHKeyModel SSHKey { get; set; }
|
||||||
public IEnumerable<CipherFieldModel> Fields { get; set; }
|
public IEnumerable<CipherFieldModel> Fields { get; set; }
|
||||||
public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }
|
public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }
|
||||||
public IEnumerable<AttachmentResponseModel> Attachments { get; set; }
|
public IEnumerable<AttachmentResponseModel> Attachments { get; set; }
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
using Bit.Core.Models.Mail;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.Models.Mail;
|
||||||
|
|
||||||
|
public class CannotDeleteManagedAccountViewModel : BaseMailModel
|
||||||
|
{
|
||||||
|
}
|
@ -11,11 +11,10 @@ namespace Bit.Core.Billing.Extensions;
|
|||||||
public static class BillingExtensions
|
public static class BillingExtensions
|
||||||
{
|
{
|
||||||
public static bool IsBillable(this Provider provider) =>
|
public static bool IsBillable(this Provider provider) =>
|
||||||
provider is
|
provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable;
|
||||||
{
|
|
||||||
Type: ProviderType.Msp,
|
public static bool SupportsConsolidatedBilling(this Provider provider)
|
||||||
Status: ProviderStatusType.Billable
|
=> provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
||||||
};
|
|
||||||
|
|
||||||
public static bool IsValidClient(this Organization organization)
|
public static bool IsValidClient(this Organization organization)
|
||||||
=> organization is
|
=> organization is
|
||||||
@ -44,5 +43,5 @@ public static class BillingExtensions
|
|||||||
};
|
};
|
||||||
|
|
||||||
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
public static bool SupportsConsolidatedBilling(this PlanType planType)
|
||||||
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly;
|
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Billing.Migration.Models;
|
using Bit.Core.Billing.Migration.Models;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -307,7 +308,14 @@ public class ProviderMigrator(
|
|||||||
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
||||||
.SeatMinimum ?? 0;
|
.SeatMinimum ?? 0;
|
||||||
|
|
||||||
await providerBillingService.UpdateSeatMinimums(provider, enterpriseSeatMinimum, teamsSeatMinimum);
|
var updateSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||||
|
provider.Id,
|
||||||
|
provider.GatewaySubscriptionId,
|
||||||
|
[
|
||||||
|
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: enterpriseSeatMinimum),
|
||||||
|
(Plan: PlanType.TeamsMonthly, SeatsMinimum: teamsSeatMinimum)
|
||||||
|
]);
|
||||||
|
await providerBillingService.UpdateSeatMinimums(updateSeatMinimumsCommand);
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
||||||
@ -325,13 +333,16 @@ public class ProviderMigrator(
|
|||||||
|
|
||||||
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
||||||
|
|
||||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
if (organizationCancellationCredit != 0)
|
||||||
new CustomerBalanceTransactionCreateOptions
|
{
|
||||||
{
|
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||||
Amount = organizationCancellationCredit,
|
new CustomerBalanceTransactionCreateOptions
|
||||||
Currency = "USD",
|
{
|
||||||
Description = "Unused, prorated time for client organization subscriptions."
|
Amount = organizationCancellationCredit,
|
||||||
});
|
Currency = "USD",
|
||||||
|
Description = "Unused, prorated time for client organization subscriptions."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
||||||
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
public record OrganizationMetadata(
|
public record OrganizationMetadata(
|
||||||
bool IsEligibleForSelfHost,
|
bool IsEligibleForSelfHost,
|
||||||
bool IsOnSecretsManagerStandalone)
|
bool IsManaged,
|
||||||
{
|
bool IsOnSecretsManagerStandalone,
|
||||||
public static OrganizationMetadata Default() => new(
|
bool IsSubscriptionUnpaid);
|
||||||
IsEligibleForSelfHost: false,
|
|
||||||
IsOnSecretsManagerStandalone: false);
|
|
||||||
}
|
|
||||||
|
@ -24,6 +24,7 @@ public record TeamsPlan : Plan
|
|||||||
Has2fa = true;
|
Has2fa = true;
|
||||||
HasApi = true;
|
HasApi = true;
|
||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
|
HasScim = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 3;
|
UpgradeSortOrder = 3;
|
||||||
DisplaySortOrder = 3;
|
DisplaySortOrder = 3;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Services.Contracts;
|
||||||
|
|
||||||
|
public record ChangeProviderPlanCommand(
|
||||||
|
Guid ProviderPlanId,
|
||||||
|
PlanType NewPlan,
|
||||||
|
string GatewaySubscriptionId);
|
@ -0,0 +1,10 @@
|
|||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Services.Contracts;
|
||||||
|
|
||||||
|
/// <param name="Id">The ID of the provider to update the seat minimums for.</param>
|
||||||
|
/// <param name="Configuration">The new seat minimums for the provider.</param>
|
||||||
|
public record UpdateProviderSeatMinimumsCommand(
|
||||||
|
Guid Id,
|
||||||
|
string GatewaySubscriptionId,
|
||||||
|
IReadOnlyCollection<(PlanType Plan, int SeatsMinimum)> Configuration);
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Billing.Entities;
|
using Bit.Core.Billing.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Services.Contracts;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
|
||||||
@ -89,8 +90,12 @@ public interface IProviderBillingService
|
|||||||
Task<Subscription> SetupSubscription(
|
Task<Subscription> SetupSubscription(
|
||||||
Provider provider);
|
Provider provider);
|
||||||
|
|
||||||
Task UpdateSeatMinimums(
|
/// <summary>
|
||||||
Provider provider,
|
/// Changes the assigned provider plan for the provider.
|
||||||
int enterpriseSeatMinimum,
|
/// </summary>
|
||||||
int teamsSeatMinimum);
|
/// <param name="command">The command to change the provider plan.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task ChangePlan(ChangeProviderPlanCommand command);
|
||||||
|
|
||||||
|
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
@ -27,7 +26,6 @@ public class OrganizationBillingService(
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<OrganizationBillingService> logger,
|
ILogger<OrganizationBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IProviderRepository providerRepository,
|
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||||
@ -64,18 +62,18 @@ public class OrganizationBillingService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions
|
var customer = await subscriberService.GetCustomer(organization,
|
||||||
{
|
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
||||||
Expand = ["discount.coupon.applies_to"]
|
|
||||||
});
|
|
||||||
|
|
||||||
var subscription = await subscriberService.GetSubscription(organization);
|
var subscription = await subscriberService.GetSubscription(organization);
|
||||||
|
|
||||||
var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription);
|
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
|
||||||
|
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||||
|
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
|
||||||
|
|
||||||
return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone);
|
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
|
||||||
|
isSubscriptionUnpaid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePaymentMethod(
|
public async Task UpdatePaymentMethod(
|
||||||
@ -339,26 +337,12 @@ public class OrganizationBillingService(
|
|||||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsEligibleForSelfHost(
|
private static bool IsEligibleForSelfHost(
|
||||||
Organization organization,
|
Organization organization)
|
||||||
Subscription? organizationSubscription)
|
|
||||||
{
|
{
|
||||||
if (organization.Status != OrganizationStatusType.Managed)
|
var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
|
||||||
{
|
|
||||||
return organization.Plan.Contains("Families") ||
|
|
||||||
organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
return eligibleSelfHostPlans.Contains(organization.PlanType);
|
||||||
|
|
||||||
var providerSubscription = await subscriberService.GetSubscriptionOrThrow(provider);
|
|
||||||
|
|
||||||
return organization.Plan.Contains("Enterprise") && IsActive(providerSubscription);
|
|
||||||
|
|
||||||
bool IsActive(Subscription? subscription) => subscription?.Status is
|
|
||||||
StripeConstants.SubscriptionStatus.Active or
|
|
||||||
StripeConstants.SubscriptionStatus.Trialing or
|
|
||||||
StripeConstants.SubscriptionStatus.PastDue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsOnSecretsManagerStandalone(
|
private static bool IsOnSecretsManagerStandalone(
|
||||||
@ -392,5 +376,16 @@ public class OrganizationBillingService(
|
|||||||
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsSubscriptionUnpaid(Subscription subscription)
|
||||||
|
{
|
||||||
|
if (subscription == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscription.Status == "unpaid";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ public static class Constants
|
|||||||
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
|
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
|
||||||
|
|
||||||
public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
|
public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
|
||||||
|
public const string SSHKeyCipherMinimumVersion = "2024.12.0";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used by IdentityServer to identify our own provider.
|
/// Used by IdentityServer to identify our own provider.
|
||||||
@ -100,13 +101,11 @@ public static class AuthenticationSchemes
|
|||||||
|
|
||||||
public static class FeatureFlagKeys
|
public static class FeatureFlagKeys
|
||||||
{
|
{
|
||||||
public const string DisplayEuEnvironment = "display-eu-environment";
|
|
||||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
public const string BrowserFilelessImport = "browser-fileless-import";
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||||
@ -124,6 +123,8 @@ public static class FeatureFlagKeys
|
|||||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||||
public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner";
|
public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner";
|
||||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||||
|
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||||
|
public const string SSHAgent = "ssh-agent";
|
||||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||||
public const string EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub";
|
public const string EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub";
|
||||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||||
@ -149,6 +150,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||||
public const string NewDeviceVerification = "new-device-verification";
|
public const string NewDeviceVerification = "new-device-verification";
|
||||||
|
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
<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.4" />
|
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
||||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.6" />
|
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||||
|
@ -38,6 +38,10 @@ public class Device : ITableObject<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? EncryptedPrivateKey { get; set; }
|
public string? EncryptedPrivateKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the device is active for the user.
|
||||||
|
/// </summary>
|
||||||
|
public bool Active { get; set; } = true;
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
{{#>FullHtmlLayout}}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; 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 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||||
|
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; box-sizing: border-box; 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; text-align: left;" valign="top" align="center">
|
||||||
|
Please contact your organization administrator for additional details.
|
||||||
|
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{/FullHtmlLayout}}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{#>BasicTextLayout}}
|
||||||
|
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
|
||||||
|
|
||||||
|
Please contact your organization administrator for additional details.
|
||||||
|
|
||||||
|
{{/BasicTextLayout}}
|
@ -7,7 +7,7 @@ public interface IDeviceService
|
|||||||
{
|
{
|
||||||
Task SaveAsync(Device device);
|
Task SaveAsync(Device device);
|
||||||
Task ClearTokenAsync(Device device);
|
Task ClearTokenAsync(Device device);
|
||||||
Task DeleteAsync(Device device);
|
Task DeactivateAsync(Device device);
|
||||||
Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||||
Guid currentUserId,
|
Guid currentUserId,
|
||||||
DeviceKeysUpdateRequestModel currentDeviceUpdate,
|
DeviceKeysUpdateRequestModel currentDeviceUpdate,
|
||||||
|
@ -18,6 +18,7 @@ public interface IMailService
|
|||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products);
|
IEnumerable<ProductType> products);
|
||||||
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
||||||
|
Task SendCannotDeleteManagedAccountEmailAsync(string email);
|
||||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||||
Task SendTwoFactorEmailAsync(string email, string token);
|
Task SendTwoFactorEmailAsync(string email, string token);
|
||||||
|
@ -14,6 +14,17 @@ public interface IStripeAdapter
|
|||||||
CustomerBalanceTransactionCreateOptions options);
|
CustomerBalanceTransactionCreateOptions options);
|
||||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a subscription object for a provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The subscription ID.</param>
|
||||||
|
/// <param name="providerId">The provider ID.</param>
|
||||||
|
/// <param name="options">Additional options.</param>
|
||||||
|
/// <returns>The subscription object.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when the subscription doesn't belong to the provider.</exception>
|
||||||
|
Task<Stripe.Subscription> ProviderSubscriptionGetAsync(string id, Guid providerId, Stripe.SubscriptionGetOptions options = null);
|
||||||
|
|
||||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||||
|
@ -41,9 +41,18 @@ public class DeviceService : IDeviceService
|
|||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Device device)
|
public async Task DeactivateAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.DeleteAsync(device);
|
// already deactivated
|
||||||
|
if (!device.Active)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.Active = false;
|
||||||
|
device.RevisionDate = DateTime.UtcNow;
|
||||||
|
await _deviceRepository.UpsertAsync(device);
|
||||||
|
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +112,19 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendCannotDeleteManagedAccountEmailAsync(string email)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage("Delete Your Account", email);
|
||||||
|
var model = new CannotDeleteManagedAccountViewModel
|
||||||
|
{
|
||||||
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
|
SiteName = _globalSettings.SiteName,
|
||||||
|
};
|
||||||
|
await AddMessageContentAsync(message, "AdminConsole.CannotDeleteManagedAccount", model);
|
||||||
|
message.Category = "CannotDeleteManagedAccount";
|
||||||
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
|
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("Your Email Change", toEmail);
|
var message = CreateDefaultMessage("Your Email Change", toEmail);
|
||||||
|
@ -79,6 +79,20 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return _subscriptionService.GetAsync(id, options);
|
return _subscriptionService.GetAsync(id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Subscription> ProviderSubscriptionGetAsync(
|
||||||
|
string id,
|
||||||
|
Guid providerId,
|
||||||
|
SubscriptionGetOptions options = null)
|
||||||
|
{
|
||||||
|
var subscription = await _subscriptionService.GetAsync(id, options);
|
||||||
|
if (subscription.Metadata.TryGetValue("providerId", out var value) && value == providerId.ToString())
|
||||||
|
{
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Subscription does not belong to the provider.");
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
||||||
Stripe.SubscriptionUpdateOptions options = null)
|
Stripe.SubscriptionUpdateOptions options = null)
|
||||||
{
|
{
|
||||||
|
@ -792,19 +792,16 @@ public class StripePaymentService : IPaymentService
|
|||||||
var daysUntilDue = sub.DaysUntilDue;
|
var daysUntilDue = sub.DaysUntilDue;
|
||||||
var chargeNow = collectionMethod == "charge_automatically";
|
var chargeNow = collectionMethod == "charge_automatically";
|
||||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
|
||||||
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||||
|
|
||||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = updatedItemOptions,
|
Items = updatedItemOptions,
|
||||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations,
|
||||||
? Constants.AlwaysInvoice
|
|
||||||
: Constants.CreateProrations,
|
|
||||||
DaysUntilDue = daysUntilDue ?? 1,
|
DaysUntilDue = daysUntilDue ?? 1,
|
||||||
CollectionMethod = "send_invoice"
|
CollectionMethod = "send_invoice"
|
||||||
};
|
};
|
||||||
if (!invoiceNow && isAnnualPlan && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing")
|
||||||
{
|
{
|
||||||
subUpdateOptions.PendingInvoiceItemInterval =
|
subUpdateOptions.PendingInvoiceItemInterval =
|
||||||
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
||||||
@ -838,7 +835,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!isPm5864DollarThresholdEnabled && !invoiceNow)
|
if (invoiceNow)
|
||||||
{
|
{
|
||||||
if (chargeNow)
|
if (chargeNow)
|
||||||
{
|
{
|
||||||
|
@ -297,6 +297,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (await IsManagedByAnyOrganizationAsync(user.Id))
|
||||||
|
{
|
||||||
|
await _mailService.SendCannotDeleteManagedAccountEmailAsync(user.Email);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
|
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
|
||||||
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
|
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,11 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendCannotDeleteManagedAccountEmailAsync(string email)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
16
src/Core/Tools/Entities/PasswordHealthReportApplication.cs
Normal file
16
src/Core/Tools/Entities/PasswordHealthReportApplication.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public string Uri { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public void SetNewId()
|
||||||
|
{
|
||||||
|
Id = CoreHelpers.GenerateComb();
|
||||||
|
}
|
||||||
|
}
|
@ -8,4 +8,5 @@ public enum CipherType : byte
|
|||||||
SecureNote = 2,
|
SecureNote = 2,
|
||||||
Card = 3,
|
Card = 3,
|
||||||
Identity = 4,
|
Identity = 4,
|
||||||
|
SSHKey = 5,
|
||||||
}
|
}
|
||||||
|
10
src/Core/Vault/Models/Data/CipherSSHKeyData.cs
Normal file
10
src/Core/Vault/Models/Data/CipherSSHKeyData.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Core.Vault.Models.Data;
|
||||||
|
|
||||||
|
public class CipherSSHKeyData : CipherData
|
||||||
|
{
|
||||||
|
public CipherSSHKeyData() { }
|
||||||
|
|
||||||
|
public string PrivateKey { get; set; }
|
||||||
|
public string PublicKey { get; set; }
|
||||||
|
public string KeyFingerprint { get; set; }
|
||||||
|
}
|
@ -21,6 +21,10 @@ public class DeviceEntityTypeConfiguration : IEntityTypeConfiguration<Device>
|
|||||||
.HasIndex(d => d.Identifier)
|
.HasIndex(d => d.Identifier)
|
||||||
.IsClustered(false);
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder.Property(c => c.Active)
|
||||||
|
.ValueGeneratedNever()
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
builder.ToTable(nameof(Device));
|
builder.ToTable(nameof(Device));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.8.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL
|
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||||
|
@Active BIT = 1
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -26,7 +27,8 @@ BEGIN
|
|||||||
[RevisionDate],
|
[RevisionDate],
|
||||||
[EncryptedUserKey],
|
[EncryptedUserKey],
|
||||||
[EncryptedPublicKey],
|
[EncryptedPublicKey],
|
||||||
[EncryptedPrivateKey]
|
[EncryptedPrivateKey],
|
||||||
|
[Active]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -40,6 +42,7 @@ BEGIN
|
|||||||
@RevisionDate,
|
@RevisionDate,
|
||||||
@EncryptedUserKey,
|
@EncryptedUserKey,
|
||||||
@EncryptedPublicKey,
|
@EncryptedPublicKey,
|
||||||
@EncryptedPrivateKey
|
@EncryptedPrivateKey,
|
||||||
|
@Active
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
CREATE PROCEDURE [dbo].[Device_DeleteById]
|
|
||||||
@Id UNIQUEIDENTIFIER
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON
|
|
||||||
|
|
||||||
DELETE
|
|
||||||
FROM
|
|
||||||
[dbo].[Device]
|
|
||||||
WHERE
|
|
||||||
[Id] = @Id
|
|
||||||
END
|
|
@ -9,7 +9,8 @@
|
|||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL
|
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||||
|
@Active BIT = 1
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -26,7 +27,8 @@ BEGIN
|
|||||||
[RevisionDate] = @RevisionDate,
|
[RevisionDate] = @RevisionDate,
|
||||||
[EncryptedUserKey] = @EncryptedUserKey,
|
[EncryptedUserKey] = @EncryptedUserKey,
|
||||||
[EncryptedPublicKey] = @EncryptedPublicKey,
|
[EncryptedPublicKey] = @EncryptedPublicKey,
|
||||||
[EncryptedPrivateKey] = @EncryptedPrivateKey
|
[EncryptedPrivateKey] = @EncryptedPrivateKey,
|
||||||
|
[Active] = @Active
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
CREATE PROCEDURE dbo.PasswordHealthReportApplication_Create
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Uri nvarchar(max),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
INSERT INTO dbo.PasswordHealthReportApplication ( Id, OrganizationId, Uri, CreationDate, RevisionDate )
|
||||||
|
VALUES ( @Id, @OrganizationId, @Uri, @CreationDate, @RevisionDate )
|
@ -0,0 +1,10 @@
|
|||||||
|
CREATE PROCEDURE dbo.PasswordHealthReportApplication_DeleteById
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @Id IS NULL
|
||||||
|
THROW 50000, 'Id cannot be null', 1;
|
||||||
|
|
||||||
|
DELETE FROM [dbo].[PasswordHealthReportApplication]
|
||||||
|
WHERE [Id] = @Id
|
@ -0,0 +1,16 @@
|
|||||||
|
CREATE PROCEDURE dbo.PasswordHealthReportApplication_ReadById
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @Id IS NULL
|
||||||
|
THROW 50000, 'Id cannot be null', 1;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
Uri,
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate
|
||||||
|
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||||
|
WHERE Id = @Id;
|
@ -0,0 +1,16 @@
|
|||||||
|
CREATE PROCEDURE dbo.PasswordHealthReportApplication_ReadByOrganizationId
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @OrganizationId IS NULL
|
||||||
|
THROW 50000, 'OrganizationId cannot be null', 1;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
Uri,
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate
|
||||||
|
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||||
|
WHERE OrganizationId = @OrganizationId;
|
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROC dbo.PasswordHealthReportApplication_Update
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Uri nvarchar(max),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
UPDATE dbo.PasswordHealthReportApplication
|
||||||
|
SET OrganizationId = @OrganizationId,
|
||||||
|
Uri = @Uri,
|
||||||
|
RevisionDate = @RevisionDate
|
||||||
|
WHERE Id = @Id
|
@ -1,25 +1,24 @@
|
|||||||
CREATE TABLE [dbo].[Device] (
|
CREATE TABLE [dbo].[Device] (
|
||||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[Name] NVARCHAR (50) NOT NULL,
|
[Name] NVARCHAR (50) NOT NULL,
|
||||||
[Type] SMALLINT NOT NULL,
|
[Type] SMALLINT NOT NULL,
|
||||||
[Identifier] NVARCHAR (50) NOT NULL,
|
[Identifier] NVARCHAR (50) NOT NULL,
|
||||||
[PushToken] NVARCHAR (255) NULL,
|
[PushToken] NVARCHAR (255) NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
[EncryptedUserKey] VARCHAR (MAX) NULL,
|
[EncryptedUserKey] VARCHAR (MAX) NULL,
|
||||||
[EncryptedPublicKey] VARCHAR (MAX) NULL,
|
[EncryptedPublicKey] VARCHAR (MAX) NULL,
|
||||||
[EncryptedPrivateKey] VARCHAR (MAX) NULL,
|
[EncryptedPrivateKey] VARCHAR (MAX) NULL,
|
||||||
|
[Active] BIT NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1),
|
||||||
CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED ([Id] ASC),
|
CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
CONSTRAINT [FK_Device_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
|
CONSTRAINT [FK_Device_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
GO
|
GO
|
||||||
CREATE UNIQUE NONCLUSTERED INDEX [UX_Device_UserId_Identifier]
|
CREATE UNIQUE NONCLUSTERED INDEX [UX_Device_UserId_Identifier]
|
||||||
ON [dbo].[Device]([UserId] ASC, [Identifier] ASC);
|
ON [dbo].[Device]([UserId] ASC, [Identifier] ASC);
|
||||||
|
|
||||||
|
|
||||||
GO
|
GO
|
||||||
CREATE NONCLUSTERED INDEX [IX_Device_Identifier]
|
CREATE NONCLUSTERED INDEX [IX_Device_Identifier]
|
||||||
ON [dbo].[Device]([Identifier] ASC);
|
ON [dbo].[Device]([Identifier] ASC);
|
||||||
|
15
src/Sql/dbo/Tables/PasswordHealthReportApplication.sql
Normal file
15
src/Sql/dbo/Tables/PasswordHealthReportApplication.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE [dbo].[PasswordHealthReportApplication]
|
||||||
|
(
|
||||||
|
Id UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
OrganizationId UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
Uri nvarchar(max),
|
||||||
|
CreationDate DATETIME2(7) NOT NULL,
|
||||||
|
RevisionDate DATETIME2(7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_PasswordHealthReportApplication_OrganizationId]
|
||||||
|
ON [dbo].[PasswordHealthReportApplication] (OrganizationId);
|
||||||
|
GO
|
@ -0,0 +1,2 @@
|
|||||||
|
CREATE VIEW [dbo].[PasswordHealthReportApplicationView] AS
|
||||||
|
SELECT * FROM [dbo].[PasswordHealthReportApplication]
|
@ -1,7 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Api.AdminConsole.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
@ -273,17 +272,12 @@ public class OrganizationUsersControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
|
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
|
||||||
Guid orgId,
|
Guid orgId, Guid id, User currentUser, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
Guid id,
|
|
||||||
SecretVerificationRequestModel model,
|
|
||||||
User currentUser,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(true);
|
|
||||||
|
|
||||||
await sutProvider.Sut.DeleteAccount(orgId, id, model);
|
await sutProvider.Sut.DeleteAccount(orgId, id);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
|
await sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
|
||||||
.Received(1)
|
.Received(1)
|
||||||
@ -293,60 +287,34 @@ public class OrganizationUsersControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
|
public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
|
||||||
Guid orgId,
|
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
Guid id,
|
|
||||||
SecretVerificationRequestModel model,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() =>
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
sutProvider.Sut.DeleteAccount(orgId, id, model));
|
sutProvider.Sut.DeleteAccount(orgId, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||||
Guid orgId,
|
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
Guid id,
|
|
||||||
SecretVerificationRequestModel model,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
|
||||||
sutProvider.Sut.DeleteAccount(orgId, id, model));
|
sutProvider.Sut.DeleteAccount(orgId, id));
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task DeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException(
|
|
||||||
Guid orgId,
|
|
||||||
Guid id,
|
|
||||||
SecretVerificationRequestModel model,
|
|
||||||
User currentUser,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
|
||||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(false);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAccount(orgId, id, model));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success(
|
public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success(
|
||||||
Guid orgId,
|
Guid orgId, OrganizationUserBulkRequestModel model, User currentUser,
|
||||||
SecureOrganizationUserBulkRequestModel model,
|
List<(Guid, string)> deleteResults, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
User currentUser,
|
|
||||||
List<(Guid, string)> deleteResults,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(true);
|
|
||||||
sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
|
sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
|
||||||
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id)
|
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id)
|
||||||
.Returns(deleteResults);
|
.Returns(deleteResults);
|
||||||
@ -363,9 +331,7 @@ public class OrganizationUsersControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
|
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
|
||||||
Guid orgId,
|
Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
SecureOrganizationUserBulkRequestModel model,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
|
||||||
|
|
||||||
@ -376,9 +342,7 @@ public class OrganizationUsersControllerTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||||
Guid orgId,
|
Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)
|
||||||
SecureOrganizationUserBulkRequestModel model,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
||||||
@ -387,21 +351,6 @@ public class OrganizationUsersControllerTests
|
|||||||
sutProvider.Sut.BulkDeleteAccount(orgId, model));
|
sutProvider.Sut.BulkDeleteAccount(orgId, model));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task BulkDeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException(
|
|
||||||
Guid orgId,
|
|
||||||
SecureOrganizationUserBulkRequestModel model,
|
|
||||||
User currentUser,
|
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
|
||||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(false);
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.BulkDeleteAccount(orgId, model));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GetMany_Setup(OrganizationAbility organizationAbility,
|
private void GetMany_Setup(OrganizationAbility organizationAbility,
|
||||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||||
SutProvider<OrganizationUsersController> sutProvider)
|
SutProvider<OrganizationUsersController> sutProvider)
|
||||||
|
@ -534,6 +534,34 @@ public class AccountsControllerTests : IDisposable
|
|||||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
|
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException()
|
||||||
|
{
|
||||||
|
var user = GenerateExampleUser();
|
||||||
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
|
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||||
|
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||||
|
|
||||||
|
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
|
||||||
|
|
||||||
|
Assert.Equal("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var user = GenerateExampleUser();
|
||||||
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
|
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||||
|
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||||
|
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
|
||||||
|
|
||||||
|
await _sut.Delete(new SecretVerificationRequestModel());
|
||||||
|
|
||||||
|
await _userService.Received(1).DeleteAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
// Below are helper functions that currently belong to this
|
// Below are helper functions that currently belong to this
|
||||||
// test class, but ultimately may need to be split out into
|
// test class, but ultimately may need to be split out into
|
||||||
|
@ -37,7 +37,7 @@ public class OrganizationBillingControllerTests
|
|||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
SutProvider<OrganizationBillingController> sutProvider)
|
SutProvider<OrganizationBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId).Returns((OrganizationMetadata)null);
|
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId).Returns((OrganizationMetadata)null);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||||
@ -50,18 +50,20 @@ public class OrganizationBillingControllerTests
|
|||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
SutProvider<OrganizationBillingController> sutProvider)
|
SutProvider<OrganizationBillingController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||||
.Returns(new OrganizationMetadata(true, true));
|
.Returns(new OrganizationMetadata(true, true, true, true));
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||||
|
|
||||||
Assert.IsType<Ok<OrganizationMetadataResponse>>(result);
|
Assert.IsType<Ok<OrganizationMetadataResponse>>(result);
|
||||||
|
|
||||||
var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value;
|
var response = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||||
|
|
||||||
Assert.True(organizationMetadataResponse.IsEligibleForSelfHost);
|
Assert.True(response.IsEligibleForSelfHost);
|
||||||
Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone);
|
Assert.True(response.IsManaged);
|
||||||
|
Assert.True(response.IsOnSecretsManagerStandalone);
|
||||||
|
Assert.True(response.IsSubscriptionUnpaid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
13
util/Migrator/DbScripts/2024-10-22_00_AddSCIMToTeamsPlan.sql
Normal file
13
util/Migrator/DbScripts/2024-10-22_00_AddSCIMToTeamsPlan.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
SET DEADLOCK_PRIORITY HIGH
|
||||||
|
GO
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[UseScim] = 1
|
||||||
|
WHERE
|
||||||
|
[PlanType] IN (
|
||||||
|
17, -- Teams (Monthly)
|
||||||
|
18 -- Teams (Annually)
|
||||||
|
)
|
||||||
|
SET DEADLOCK_PRIORITY NORMAL
|
||||||
|
GO
|
@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
IF OBJECT_ID('dbo.PasswordHealthReportApplication') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[PasswordHealthReportApplication]
|
||||||
|
(
|
||||||
|
Id UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
OrganizationId UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
Uri nvarchar(max),
|
||||||
|
CreationDate DATETIME2(7) NOT NULL,
|
||||||
|
RevisionDate DATETIME2(7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_PasswordHealthReportApplication_OrganizationId]
|
||||||
|
ON [dbo].[PasswordHealthReportApplication] (OrganizationId);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.PasswordHealthReportApplicationView') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW [dbo].[PasswordHealthReportApplicationView]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE VIEW [dbo].[PasswordHealthReportApplicationView] AS
|
||||||
|
SELECT * FROM [dbo].[PasswordHealthReportApplication]
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_Create
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Uri nvarchar(max),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
INSERT INTO dbo.PasswordHealthReportApplication ( Id, OrganizationId, Uri, CreationDate, RevisionDate )
|
||||||
|
VALUES ( @Id, @OrganizationId, @Uri, @CreationDate, @RevisionDate )
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_ReadByOrganizationId
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @OrganizationId IS NULL
|
||||||
|
THROW 50000, 'OrganizationId cannot be null', 1;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
Uri,
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate
|
||||||
|
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||||
|
WHERE OrganizationId = @OrganizationId;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_ReadById
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @Id IS NULL
|
||||||
|
THROW 50000, 'Id cannot be null', 1;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
Id,
|
||||||
|
OrganizationId,
|
||||||
|
Uri,
|
||||||
|
CreationDate,
|
||||||
|
RevisionDate
|
||||||
|
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||||
|
WHERE Id = @Id;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_Update
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Uri nvarchar(max),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7)
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
UPDATE dbo.PasswordHealthReportApplication
|
||||||
|
SET OrganizationId = @OrganizationId,
|
||||||
|
Uri = @Uri,
|
||||||
|
RevisionDate = @RevisionDate
|
||||||
|
WHERE Id = @Id
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_DeleteById
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @Id IS NULL
|
||||||
|
THROW 50000, 'Id cannot be null', 1;
|
||||||
|
|
||||||
|
DELETE FROM [dbo].[PasswordHealthReportApplication]
|
||||||
|
WHERE [Id] = @Id
|
||||||
|
GO
|
118
util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql
Normal file
118
util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
SET DEADLOCK_PRIORITY HIGH
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- add column
|
||||||
|
IF COL_LENGTH('[dbo].[Device]', 'Active') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE
|
||||||
|
[dbo].[Device]
|
||||||
|
ADD
|
||||||
|
[Active] BIT NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- refresh view
|
||||||
|
CREATE OR ALTER VIEW [dbo].[DeviceView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[Device]
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- drop now-unused proc for deletion
|
||||||
|
IF OBJECT_ID('[dbo].[Device_DeleteById]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Device_DeleteById]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- refresh procs
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Device_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@Type TINYINT,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@PushToken NVARCHAR(255),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||||
|
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||||
|
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||||
|
@Active BIT = 1
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[Device]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[Name],
|
||||||
|
[Type],
|
||||||
|
[Identifier],
|
||||||
|
[PushToken],
|
||||||
|
[CreationDate],
|
||||||
|
[RevisionDate],
|
||||||
|
[EncryptedUserKey],
|
||||||
|
[EncryptedPublicKey],
|
||||||
|
[EncryptedPrivateKey],
|
||||||
|
[Active]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@UserId,
|
||||||
|
@Name,
|
||||||
|
@Type,
|
||||||
|
@Identifier,
|
||||||
|
@PushToken,
|
||||||
|
@CreationDate,
|
||||||
|
@RevisionDate,
|
||||||
|
@EncryptedUserKey,
|
||||||
|
@EncryptedPublicKey,
|
||||||
|
@EncryptedPrivateKey,
|
||||||
|
@Active
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Device_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@Type TINYINT,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@PushToken NVARCHAR(255),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||||
|
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||||
|
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||||
|
@Active BIT = 1
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Device]
|
||||||
|
SET
|
||||||
|
[UserId] = @UserId,
|
||||||
|
[Name] = @Name,
|
||||||
|
[Type] = @Type,
|
||||||
|
[Identifier] = @Identifier,
|
||||||
|
[PushToken] = @PushToken,
|
||||||
|
[CreationDate] = @CreationDate,
|
||||||
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[EncryptedUserKey] = @EncryptedUserKey,
|
||||||
|
[EncryptedPublicKey] = @EncryptedPublicKey,
|
||||||
|
[EncryptedPrivateKey] = @EncryptedPrivateKey,
|
||||||
|
[Active] = @Active
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
SET DEADLOCK_PRIORITY NORMAL
|
||||||
|
GO
|
2845
util/MySqlMigrations/Migrations/20241031154652_PasswordHealthReportApplication.Designer.cs
generated
Normal file
2845
util/MySqlMigrations/Migrations/20241031154652_PasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PasswordHealthReportApplication : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PasswordHealthReportApplication",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||||
|
Uri = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PasswordHealthReportApplication", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PasswordHealthReportApplication_Organization_OrganizationId",
|
||||||
|
column: x => x.OrganizationId,
|
||||||
|
principalTable: "Organization",
|
||||||
|
principalColumn: "Id");
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PasswordHealthReportApplication_OrganizationId",
|
||||||
|
table: "PasswordHealthReportApplication",
|
||||||
|
column: "OrganizationId");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(name: "PasswordHealthReportApplication");
|
||||||
|
}
|
||||||
|
}
|
2849
util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs
generated
Normal file
2849
util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DeviceActivation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Active",
|
||||||
|
table: "Device",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Active",
|
||||||
|
table: "Device");
|
||||||
|
}
|
||||||
|
}
|
@ -939,6 +939,10 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("tinyint(1)")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.Property<DateTime>("CreationDate")
|
b.Property<DateTime>("CreationDate")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
2851
util/PostgresMigrations/Migrations/20241031154656_PasswordHealthReportApplication.Designer.cs
generated
Normal file
2851
util/PostgresMigrations/Migrations/20241031154656_PasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PasswordHealthReportApplication : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PasswordHealthReportApplication",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
OrganizationId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
Uri = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PasswordHealthReportApplication", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PasswordHealthReportApplication_Organization_OrganizationId",
|
||||||
|
column: x => x.OrganizationId,
|
||||||
|
principalTable: "Organization",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PasswordHealthReportApplication_OrganizationId",
|
||||||
|
table: "PasswordHealthReportApplication",
|
||||||
|
column: "OrganizationId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(name: "PasswordHealthReportApplication");
|
||||||
|
}
|
||||||
|
}
|
2855
util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs
generated
Normal file
2855
util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DeviceActivation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Active",
|
||||||
|
table: "Device",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Active",
|
||||||
|
table: "Device");
|
||||||
|
}
|
||||||
|
}
|
@ -944,6 +944,10 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.Property<DateTime>("CreationDate")
|
b.Property<DateTime>("CreationDate")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
@ -51,6 +51,12 @@ public class EnvironmentFileBuilder
|
|||||||
_globalOverrideValues.Remove("globalSettings__pushRelayBaseUri");
|
_globalOverrideValues.Remove("globalSettings__pushRelayBaseUri");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_globalOverrideValues.TryGetValue("globalSettings__baseServiceUri__vault", out var vaultUri) && vaultUri != _context.Config.Url)
|
||||||
|
{
|
||||||
|
_globalOverrideValues["globalSettings__baseServiceUri__vault"] = _context.Config.Url;
|
||||||
|
Helpers.WriteLine(_context, "Updated globalSettings__baseServiceUri__vault to match value in config.yml");
|
||||||
|
}
|
||||||
|
|
||||||
Build();
|
Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2834
util/SqliteMigrations/Migrations/20241031154700_PasswordHealthReportApplication.Designer.cs
generated
Normal file
2834
util/SqliteMigrations/Migrations/20241031154700_PasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class PasswordHealthReportApplication : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PasswordHealthReportApplication",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
OrganizationId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||||
|
Uri = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PasswordHealthReportApplication", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PasswordHealthReportApplication_Organization_OrganizationId",
|
||||||
|
column: x => x.OrganizationId,
|
||||||
|
principalTable: "Organization",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PasswordHealthReportApplication_OrganizationId",
|
||||||
|
table: "PasswordHealthReportApplication",
|
||||||
|
column: "OrganizationId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(name: "PasswordHealthReportApplication");
|
||||||
|
}
|
||||||
|
}
|
2838
util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs
generated
Normal file
2838
util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DeviceActivation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Active",
|
||||||
|
table: "Device",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Active",
|
||||||
|
table: "Device");
|
||||||
|
}
|
||||||
|
}
|
@ -928,6 +928,10 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.Property<DateTime>("CreationDate")
|
b.Property<DateTime>("CreationDate")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user