mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
Merge branch 'refs/heads/main' into km/pm-10600
This commit is contained in:
commit
67aa2eb6d9
@ -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": {
|
||||||
|
@ -29,7 +29,7 @@ jobs:
|
|||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ jobs:
|
|||||||
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ jobs:
|
|||||||
devops-alerts-slack-webhook-url"
|
devops-alerts-slack-webhook-url"
|
||||||
|
|
||||||
- name: Import GPG keys
|
- name: Import GPG keys
|
||||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||||
|
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@ -18,10 +18,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Verify format
|
- name: Verify format
|
||||||
run: dotnet format --verify-no-changes
|
run: dotnet format --verify-no-changes
|
||||||
@ -67,13 +67,13 @@ jobs:
|
|||||||
node: true
|
node: true
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
with:
|
with:
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
@ -172,7 +172,7 @@ jobs:
|
|||||||
dotnet: true
|
dotnet: true
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Check branch to publish
|
- name: Check branch to publish
|
||||||
env:
|
env:
|
||||||
@ -274,14 +274,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Scan Docker image
|
- name: Scan Docker image
|
||||||
id: container-scan
|
id: container-scan
|
||||||
uses: anchore/scan-action@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1
|
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||||
fail-build: false
|
fail-build: false
|
||||||
output-format: sarif
|
output-format: sarif
|
||||||
|
|
||||||
- name: Upload Grype results to GitHub
|
- name: Upload Grype results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
with:
|
with:
|
||||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||||
|
|
||||||
@ -291,10 +291,10 @@ jobs:
|
|||||||
needs: build-docker
|
needs: build-docker
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Log in to Azure - production subscription
|
- name: Log in to Azure - production subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
@ -466,10 +466,10 @@ jobs:
|
|||||||
- win-x64
|
- win-x64
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
@ -12,7 +12,7 @@ jobs:
|
|||||||
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Validate config exists in path
|
- name: Validate config exists in path
|
||||||
id: validate-config
|
id: validate-config
|
||||||
|
2
.github/workflows/cleanup-rc-branch.yml
vendored
2
.github/workflows/cleanup-rc-branch.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Checkout main
|
- name: Checkout main
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Collect
|
- name: Collect
|
||||||
id: collect
|
id: collect
|
||||||
|
2
.github/workflows/protect-files.yml
vendored
2
.github/workflows/protect-files.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
label: "DB-migrations-changed"
|
label: "DB-migrations-changed"
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -98,7 +98,7 @@ jobs:
|
|||||||
echo "Github Release Option: $RELEASE_OPTION"
|
echo "Github Release Option: $RELEASE_OPTION"
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up project name
|
- name: Set up project name
|
||||||
id: setup
|
id: setup
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Check release version
|
- name: Check release version
|
||||||
id: version
|
id: version
|
||||||
|
6
.github/workflows/repository-management.yml
vendored
6
.github/workflows/repository-management.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out target ref
|
- name: Check out target ref
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.target_ref }}
|
ref: ${{ inputs.target_ref }}
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ jobs:
|
|||||||
version: ${{ inputs.version_number_override }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ jobs:
|
|||||||
needs: bump_version
|
needs: bump_version
|
||||||
steps:
|
steps:
|
||||||
- name: Check out main branch
|
- name: Check out main branch
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
|
10
.github/workflows/scan.yml
vendored
10
.github/workflows/scan.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
--output-path . ${{ env.INCREMENTAL }}
|
--output-path . ${{ env.INCREMENTAL }}
|
||||||
|
|
||||||
- name: Upload Checkmarx results to GitHub
|
- name: Upload Checkmarx results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
|
||||||
@ -60,19 +60,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Install SonarCloud scanner
|
- name: Install SonarCloud scanner
|
||||||
run: dotnet tool install dotnet-sonarscanner -g
|
run: dotnet tool install dotnet-sonarscanner -g
|
||||||
|
8
.github/workflows/test-database.yml
vendored
8
.github/workflows/test-database.yml
vendored
@ -35,10 +35,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Restore tools
|
- name: Restore tools
|
||||||
run: dotnet tool restore
|
run: dotnet tool restore
|
||||||
@ -146,10 +146,10 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -46,10 +46,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||||
|
|
||||||
- name: Print environment
|
- name: Print environment
|
||||||
run: |
|
run: |
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
@ -437,144 +438,142 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
|
@ -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)
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Admin.Enums;
|
|||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -24,6 +25,8 @@ public class UsersController : Controller
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IAccessControlService _accessControlService;
|
private readonly IAccessControlService _accessControlService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public UsersController(
|
public UsersController(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@ -31,7 +34,9 @@ public class UsersController : Controller
|
|||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IAccessControlService accessControlService,
|
IAccessControlService accessControlService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IUserService userService,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
@ -39,6 +44,8 @@ public class UsersController : Controller
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_accessControlService = accessControlService;
|
_accessControlService = accessControlService;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_userService = userService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequirePermission(Permission.User_List_View)]
|
[RequirePermission(Permission.User_List_View)]
|
||||||
@ -82,8 +89,8 @@ public class UsersController : Controller
|
|||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||||
|
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
|
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers));
|
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||||
}
|
}
|
||||||
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
@ -99,7 +106,8 @@ public class UsersController : Controller
|
|||||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings));
|
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||||
|
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -153,4 +161,12 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Feature flag to be removed in PM-14207
|
||||||
|
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||||
|
{
|
||||||
|
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||||
|
? await _userService.IsManagedByAnyOrganizationAsync(userId)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,11 @@ public class UserEditModel
|
|||||||
IEnumerable<Cipher> ciphers,
|
IEnumerable<Cipher> ciphers,
|
||||||
BillingInfo billingInfo,
|
BillingInfo billingInfo,
|
||||||
BillingHistoryInfo billingHistoryInfo,
|
BillingHistoryInfo billingHistoryInfo,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
bool? domainVerified
|
||||||
|
)
|
||||||
{
|
{
|
||||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers);
|
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified);
|
||||||
|
|
||||||
BillingInfo = billingInfo;
|
BillingInfo = billingInfo;
|
||||||
BillingHistoryInfo = billingHistoryInfo;
|
BillingHistoryInfo = billingHistoryInfo;
|
||||||
|
@ -14,6 +14,7 @@ public class UserViewModel
|
|||||||
public bool Premium { get; }
|
public bool Premium { get; }
|
||||||
public short? MaxStorageGb { get; }
|
public short? MaxStorageGb { get; }
|
||||||
public bool EmailVerified { get; }
|
public bool EmailVerified { get; }
|
||||||
|
public bool? DomainVerified { get; }
|
||||||
public bool TwoFactorEnabled { get; }
|
public bool TwoFactorEnabled { get; }
|
||||||
public DateTime AccountRevisionDate { get; }
|
public DateTime AccountRevisionDate { get; }
|
||||||
public DateTime RevisionDate { get; }
|
public DateTime RevisionDate { get; }
|
||||||
@ -35,6 +36,7 @@ public class UserViewModel
|
|||||||
bool premium,
|
bool premium,
|
||||||
short? maxStorageGb,
|
short? maxStorageGb,
|
||||||
bool emailVerified,
|
bool emailVerified,
|
||||||
|
bool? domainVerified,
|
||||||
bool twoFactorEnabled,
|
bool twoFactorEnabled,
|
||||||
DateTime accountRevisionDate,
|
DateTime accountRevisionDate,
|
||||||
DateTime revisionDate,
|
DateTime revisionDate,
|
||||||
@ -56,6 +58,7 @@ public class UserViewModel
|
|||||||
Premium = premium;
|
Premium = premium;
|
||||||
MaxStorageGb = maxStorageGb;
|
MaxStorageGb = maxStorageGb;
|
||||||
EmailVerified = emailVerified;
|
EmailVerified = emailVerified;
|
||||||
|
DomainVerified = domainVerified;
|
||||||
TwoFactorEnabled = twoFactorEnabled;
|
TwoFactorEnabled = twoFactorEnabled;
|
||||||
AccountRevisionDate = accountRevisionDate;
|
AccountRevisionDate = accountRevisionDate;
|
||||||
RevisionDate = revisionDate;
|
RevisionDate = revisionDate;
|
||||||
@ -73,10 +76,10 @@ public class UserViewModel
|
|||||||
public static IEnumerable<UserViewModel> MapViewModels(
|
public static IEnumerable<UserViewModel> MapViewModels(
|
||||||
IEnumerable<User> users,
|
IEnumerable<User> users,
|
||||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
||||||
users.Select(user => MapViewModel(user, lookup));
|
users.Select(user => MapViewModel(user, lookup, false));
|
||||||
|
|
||||||
public static UserViewModel MapViewModel(User user,
|
public static UserViewModel MapViewModel(User user,
|
||||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) =>
|
||||||
new(
|
new(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Name,
|
user.Name,
|
||||||
@ -86,6 +89,7 @@ public class UserViewModel
|
|||||||
user.Premium,
|
user.Premium,
|
||||||
user.MaxStorageGb,
|
user.MaxStorageGb,
|
||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
|
domainVerified,
|
||||||
IsTwoFactorEnabled(user, lookup),
|
IsTwoFactorEnabled(user, lookup),
|
||||||
user.AccountRevisionDate,
|
user.AccountRevisionDate,
|
||||||
user.RevisionDate,
|
user.RevisionDate,
|
||||||
@ -100,9 +104,9 @@ public class UserViewModel
|
|||||||
Array.Empty<Cipher>());
|
Array.Empty<Cipher>());
|
||||||
|
|
||||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
|
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
|
||||||
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>());
|
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
|
||||||
|
|
||||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers) =>
|
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) =>
|
||||||
new(
|
new(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Name,
|
user.Name,
|
||||||
@ -112,6 +116,7 @@ public class UserViewModel
|
|||||||
user.Premium,
|
user.Premium,
|
||||||
user.MaxStorageGb,
|
user.MaxStorageGb,
|
||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
|
domainVerified,
|
||||||
isTwoFactorEnabled,
|
isTwoFactorEnabled,
|
||||||
user.AccountRevisionDate,
|
user.AccountRevisionDate,
|
||||||
user.RevisionDate,
|
user.RevisionDate,
|
||||||
|
@ -110,6 +110,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.User_Licensing_View,
|
Permission.User_Licensing_View,
|
||||||
Permission.User_Billing_View,
|
Permission.User_Billing_View,
|
||||||
Permission.User_Billing_LaunchGateway,
|
Permission.User_Billing_LaunchGateway,
|
||||||
|
Permission.User_Delete,
|
||||||
Permission.Org_List_View,
|
Permission.Org_List_View,
|
||||||
Permission.Org_OrgInformation_View,
|
Permission.Org_OrgInformation_View,
|
||||||
Permission.Org_GeneralDetails_View,
|
Permission.Org_GeneralDetails_View,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@model UserViewModel
|
@model UserViewModel
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
|
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
|
||||||
@ -12,6 +12,11 @@
|
|||||||
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
|
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
|
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
|
||||||
|
|
||||||
|
@if(Model.DomainVerified.HasValue){
|
||||||
|
<dt class="col-sm-4 col-lg-3">Domain Verified</dt>
|
||||||
|
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd>
|
||||||
|
}
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>
|
<dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>
|
||||||
|
|
||||||
|
@ -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 =>
|
||||||
|
@ -16,6 +16,7 @@ using Bit.Core.Utilities;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Controllers;
|
namespace Bit.Api.AdminConsole.Controllers;
|
||||||
|
|
||||||
@ -55,17 +56,16 @@ public class PoliciesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{type}")]
|
[HttpGet("{type}")]
|
||||||
public async Task<PolicyResponseModel> Get(string orgId, int type)
|
public async Task<PolicyResponseModel> Get(Guid orgId, int type)
|
||||||
{
|
{
|
||||||
var orgIdGuid = new Guid(orgId);
|
if (!await _currentContext.ManagePolicies(orgId))
|
||||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgIdGuid, (PolicyType)type);
|
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
|
||||||
if (policy == null)
|
if (policy == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false });
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PolicyResponseModel(policy);
|
return new PolicyResponseModel(policy);
|
||||||
|
@ -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>
|
||||||
|
@ -148,6 +148,13 @@ public class AccountsController : Controller
|
|||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
|
}
|
||||||
|
|
||||||
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
|
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +172,13 @@ public class AccountsController : Controller
|
|||||||
throw new BadRequestException("You cannot change your email when using Key Connector.");
|
throw new BadRequestException("You cannot change your email when using Key Connector.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
|
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
|
||||||
model.NewMasterPasswordHash, model.Token, model.Key);
|
model.NewMasterPasswordHash, model.Token, model.Key);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
@ -566,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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,6 @@ public static class FeatureFlagKeys
|
|||||||
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";
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
||||||
<PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
|
<PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
|
||||||
@ -35,22 +35,22 @@
|
|||||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0" />
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0" />
|
||||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||||
<PackageReference Include="Quartz" Version="3.9.0" />
|
<PackageReference Include="Quartz" Version="3.9.0" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
<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" />
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<PackageReference Include="Stripe.net" Version="45.14.0" />
|
<PackageReference Include="Stripe.net" Version="45.14.0" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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]
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using Bit.Admin.Models;
|
using Bit.Admin.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
|
||||||
namespace Admin.Test.Models;
|
namespace Admin.Test.Models;
|
||||||
@ -79,7 +80,7 @@ public class UserViewModelTests
|
|||||||
{
|
{
|
||||||
var lookup = new List<(Guid, bool)> { (user.Id, true) };
|
var lookup = new List<(Guid, bool)> { (user.Id, true) };
|
||||||
|
|
||||||
var actual = UserViewModel.MapViewModel(user, lookup);
|
var actual = UserViewModel.MapViewModel(user, lookup, false);
|
||||||
|
|
||||||
Assert.True(actual.TwoFactorEnabled);
|
Assert.True(actual.TwoFactorEnabled);
|
||||||
}
|
}
|
||||||
@ -90,7 +91,7 @@ public class UserViewModelTests
|
|||||||
{
|
{
|
||||||
var lookup = new List<(Guid, bool)> { (user.Id, false) };
|
var lookup = new List<(Guid, bool)> { (user.Id, false) };
|
||||||
|
|
||||||
var actual = UserViewModel.MapViewModel(user, lookup);
|
var actual = UserViewModel.MapViewModel(user, lookup, false);
|
||||||
|
|
||||||
Assert.False(actual.TwoFactorEnabled);
|
Assert.False(actual.TwoFactorEnabled);
|
||||||
}
|
}
|
||||||
@ -101,8 +102,44 @@ public class UserViewModelTests
|
|||||||
{
|
{
|
||||||
var lookup = new List<(Guid, bool)> { (Guid.NewGuid(), true) };
|
var lookup = new List<(Guid, bool)> { (Guid.NewGuid(), true) };
|
||||||
|
|
||||||
var actual = UserViewModel.MapViewModel(user, lookup);
|
var actual = UserViewModel.MapViewModel(user, lookup, false);
|
||||||
|
|
||||||
Assert.False(actual.TwoFactorEnabled);
|
Assert.False(actual.TwoFactorEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public void MapUserViewModel_WithVerifiedDomain_ReturnsUserViewModel(User user)
|
||||||
|
{
|
||||||
|
|
||||||
|
var verifiedDomain = true;
|
||||||
|
|
||||||
|
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
|
||||||
|
|
||||||
|
Assert.True(actual.DomainVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user)
|
||||||
|
{
|
||||||
|
|
||||||
|
var verifiedDomain = false;
|
||||||
|
|
||||||
|
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
|
||||||
|
|
||||||
|
Assert.False(actual.DomainVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user)
|
||||||
|
{
|
||||||
|
|
||||||
|
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), null);
|
||||||
|
|
||||||
|
Assert.Null(actual.DomainVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.IntegrationTest.Controllers;
|
namespace Bit.Api.IntegrationTest.Controllers;
|
||||||
@ -35,4 +44,82 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
|||||||
Assert.Null(content.PrivateKey);
|
Assert.Null(content.PrivateKey);
|
||||||
Assert.NotNull(content.SecurityStamp);
|
Assert.NotNull(content.SecurityStamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostEmailToken_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest()
|
||||||
|
{
|
||||||
|
var email = await SetupOrganizationManagedAccount();
|
||||||
|
|
||||||
|
var tokens = await _factory.LoginAsync(email);
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
var model = new EmailTokenRequestModel
|
||||||
|
{
|
||||||
|
NewEmail = $"{Guid.NewGuid()}@example.com",
|
||||||
|
MasterPasswordHash = "master_password_hash"
|
||||||
|
};
|
||||||
|
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email-token")
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(model)
|
||||||
|
};
|
||||||
|
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||||
|
var response = await client.SendAsync(message);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Cannot change emails for accounts owned by an organization", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostEmail_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest()
|
||||||
|
{
|
||||||
|
var email = await SetupOrganizationManagedAccount();
|
||||||
|
|
||||||
|
var tokens = await _factory.LoginAsync(email);
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
var model = new EmailRequestModel
|
||||||
|
{
|
||||||
|
NewEmail = $"{Guid.NewGuid()}@example.com",
|
||||||
|
MasterPasswordHash = "master_password_hash",
|
||||||
|
NewMasterPasswordHash = "master_password_hash",
|
||||||
|
Token = "validtoken",
|
||||||
|
Key = "key"
|
||||||
|
};
|
||||||
|
|
||||||
|
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email")
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(model)
|
||||||
|
};
|
||||||
|
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||||
|
var response = await client.SendAsync(message);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Cannot change emails for accounts owned by an organization", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> SetupOrganizationManagedAccount()
|
||||||
|
{
|
||||||
|
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||||
|
featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true));
|
||||||
|
|
||||||
|
// Create the owner account
|
||||||
|
var ownerEmail = $"{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(ownerEmail);
|
||||||
|
|
||||||
|
// Create the organization
|
||||||
|
var (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||||
|
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||||
|
|
||||||
|
// Create a new organization member
|
||||||
|
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||||
|
OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });
|
||||||
|
|
||||||
|
// Add a verified domain
|
||||||
|
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
|
||||||
|
|
||||||
|
return email;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,4 +105,22 @@ public static class OrganizationTestHelpers
|
|||||||
|
|
||||||
return (email, organizationUser);
|
return (email, organizationUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a VerifiedDomain for the specified organization.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task CreateVerifiedDomainAsync(ApiApplicationFactory factory, Guid organizationId, string domain)
|
||||||
|
{
|
||||||
|
var organizationDomainRepository = factory.GetService<IOrganizationDomainRepository>();
|
||||||
|
|
||||||
|
var verifiedDomain = new OrganizationDomain
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
DomainName = domain,
|
||||||
|
Txt = "btw+test18383838383"
|
||||||
|
};
|
||||||
|
verifiedDomain.SetVerifiedDate();
|
||||||
|
|
||||||
|
await organizationDomainRepository.CreateAsync(verifiedDomain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Request.WebAuthn;
|
|||||||
using Bit.Api.Auth.Validators;
|
using Bit.Api.Auth.Validators;
|
||||||
using Bit.Api.Tools.Models.Request;
|
using Bit.Api.Tools.Models.Request;
|
||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
@ -143,6 +144,21 @@ public class AccountsControllerTests : IDisposable
|
|||||||
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
|
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldInitiateEmailChange()
|
||||||
|
{
|
||||||
|
var user = GenerateExampleUser();
|
||||||
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
|
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||||
|
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||||
|
var newEmail = "example@user.com";
|
||||||
|
|
||||||
|
await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });
|
||||||
|
|
||||||
|
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
|
public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
|
||||||
{
|
{
|
||||||
@ -165,6 +181,22 @@ public class AccountsControllerTests : IDisposable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException()
|
||||||
|
{
|
||||||
|
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.PostEmailToken(new EmailTokenRequestModel())
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PostEmail_ShouldChangeUserEmail()
|
public async Task PostEmail_ShouldChangeUserEmail()
|
||||||
{
|
{
|
||||||
@ -178,6 +210,21 @@ public class AccountsControllerTests : IDisposable
|
|||||||
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail()
|
||||||
|
{
|
||||||
|
var user = GenerateExampleUser();
|
||||||
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
|
_userService.ChangeEmailAsync(user, default, default, default, default, default)
|
||||||
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||||
|
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||||
|
|
||||||
|
await _sut.PostEmail(new EmailRequestModel());
|
||||||
|
|
||||||
|
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
|
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
|
||||||
{
|
{
|
||||||
@ -201,6 +248,21 @@ public class AccountsControllerTests : IDisposable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException()
|
||||||
|
{
|
||||||
|
var user = GenerateExampleUser();
|
||||||
|
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||||
|
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||||
|
|
||||||
|
var result = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => _sut.PostEmail(new EmailRequestModel())
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PostVerifyEmail_ShouldSendEmailVerification()
|
public async Task PostVerifyEmail_ShouldSendEmailVerification()
|
||||||
{
|
{
|
||||||
@ -472,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]
|
||||||
|
@ -3,8 +3,10 @@ using System.Text.Json;
|
|||||||
using Bit.Api.AdminConsole.Controllers;
|
using Bit.Api.AdminConsole.Controllers;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
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.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -132,4 +134,71 @@ public class PoliciesControllerTests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
|
||||||
|
SutProvider<PoliciesController> sutProvider, Guid orgId, Policy policy, int type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.ManagePolicies(orgId)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
policy.Type = (PolicyType)type;
|
||||||
|
policy.Enabled = true;
|
||||||
|
policy.Data = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
||||||
|
.Returns(policy);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.Get(orgId, type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<PolicyResponseModel>(result);
|
||||||
|
Assert.Equal(policy.Id, result.Id);
|
||||||
|
Assert.Equal(policy.Type, result.Type);
|
||||||
|
Assert.Equal(policy.Enabled, result.Enabled);
|
||||||
|
Assert.Equal(policy.OrganizationId, result.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefaultPolicy(
|
||||||
|
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.ManagePolicies(orgId)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
||||||
|
.Returns((Policy)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.Get(orgId, type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<PolicyResponseModel>(result);
|
||||||
|
Assert.Equal(result.Type, (PolicyType)type);
|
||||||
|
Assert.False(result.Enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(
|
||||||
|
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.ManagePolicies(orgId)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Get(orgId, type));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.9.1" />
|
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
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
|
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="DbScripts\**\*.sql" />
|
<EmbeddedResource Include="DbScripts\**\*.sql" />
|
||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="dbup-sqlserver" Version="5.0.41" />
|
<PackageReference Include="dbup-sqlserver" Version="5.0.41" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Migrator\Migrator.csproj" />
|
<ProjectReference Include="..\Migrator\Migrator.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandDotNet" Version="7.0.4" />
|
<PackageReference Include="CommandDotNet" Version="7.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
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
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user